diff --git a/OAuth2/Autoloader.php b/OAuth2/Autoloader.php index ecfb6ba..4ec08cb 100644 --- a/OAuth2/Autoloader.php +++ b/OAuth2/Autoloader.php @@ -10,8 +10,14 @@ */ class Autoloader { + /** + * @var string + */ private $dir; + /** + * @param string $dir + */ public function __construct($dir = null) { if (is_null($dir)) { @@ -19,6 +25,7 @@ public function __construct($dir = null) } $this->dir = $dir; } + /** * Registers OAuth2\Autoloader as an SPL autoloader. */ @@ -31,9 +38,8 @@ public static function register($dir = null) /** * Handles autoloading of classes. * - * @param string $class A class name. - * - * @return boolean Returns true if the class has been loaded + * @param string $class - A class name. + * @return boolean - Returns true if the class has been loaded */ public function autoload($class) { diff --git a/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php index 29c7171..6a167da 100644 --- a/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php +++ b/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php @@ -10,6 +10,19 @@ */ interface ClientAssertionTypeInterface { + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ public function validateRequest(RequestInterface $request, ResponseInterface $response); + + /** + * Get the client id + * + * @return mixed + */ public function getClientId(); } diff --git a/OAuth2/ClientAssertionType/HttpBasic.php b/OAuth2/ClientAssertionType/HttpBasic.php index 0ecb7e1..ef61203 100644 --- a/OAuth2/ClientAssertionType/HttpBasic.php +++ b/OAuth2/ClientAssertionType/HttpBasic.php @@ -5,6 +5,7 @@ use OAuth2\Storage\ClientCredentialsInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use LogicException; /** * Validate a client via Http Basic authentication @@ -19,14 +20,16 @@ class HttpBasic implements ClientAssertionTypeInterface protected $config; /** - * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header - * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated - * ); - * + * Config array $config should look as follows: + * @code + * $config = array( + * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header + * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated + * ); + * @endcode + * + * @param ClientCredentialsInterface $storage Storage + * @param array $config Configuration options for the server */ public function __construct(ClientCredentialsInterface $storage, array $config = array()) { @@ -37,6 +40,14 @@ public function __construct(ClientCredentialsInterface $storage, array $config = ), $config); } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed + * @throws LogicException + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$clientData = $this->getClientCredentials($request, $response)) { @@ -44,7 +55,7 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re } if (!isset($clientData['client_id'])) { - throw new \LogicException('the clientData array must have "client_id" set'); + throw new LogicException('the clientData array must have "client_id" set'); } if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { @@ -70,6 +81,11 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return true; } + /** + * Get the client id + * + * @return mixed + */ public function getClientId() { return $this->clientData['client_id']; @@ -82,13 +98,14 @@ public function getClientId() * According to the spec (draft 20), the client_id can be provided in * the Basic Authorization header (recommended) or via GET/POST. * - * @return - * A list containing the client identifier and password, for example + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array|null A list containing the client identifier and password, for example: * @code - * return array( - * "client_id" => CLIENT_ID, // REQUIRED the client id - * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) - * ); + * return array( + * "client_id" => CLIENT_ID, // REQUIRED the client id + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) + * ); * @endcode * * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 @@ -108,7 +125,6 @@ public function getClientCredentials(RequestInterface $request, ResponseInterfac * client_secret can be null if the client's password is an empty string * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 */ - return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); } } diff --git a/OAuth2/Controller/AuthorizeController.php b/OAuth2/Controller/AuthorizeController.php index 6d6481e..4bafb1d 100644 --- a/OAuth2/Controller/AuthorizeController.php +++ b/OAuth2/Controller/AuthorizeController.php @@ -7,37 +7,76 @@ use OAuth2\RequestInterface; use OAuth2\ResponseInterface; use OAuth2\Scope; +use InvalidArgumentException; /** - * @see OAuth2\Controller\AuthorizeControllerInterface + * @see AuthorizeControllerInterface */ class AuthorizeController implements AuthorizeControllerInterface { + /** + * @var string + */ private $scope; + + /** + * @var int + */ private $state; + + /** + * @var mixed + */ private $client_id; + + /** + * @var string + */ private $redirect_uri; + + /** + * The response type + * + * @var string + */ private $response_type; + /** + * @var ClientInterface + */ protected $clientStorage; + + /** + * @var array + */ protected $responseTypes; + + /** + * @var array + */ protected $config; + + /** + * @var ScopeInterface + */ protected $scopeUtil; /** - * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information - * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array - * keys are "code" and "token" - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type - * 'enforce_state' => true // if the controller should require the "state" parameter - * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter - * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses - * ); - * - * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + * Constructor + * + * @param ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information + * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array + * keys are "code" and "token" + * @param array $config OPTIONAL Configuration options for the server: + * @param ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + * @code + * $config = array( + * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type + * 'enforce_state' => true // if the controller should require the "state" parameter + * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter + * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses + * ); + * @endcode */ public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) { @@ -56,10 +95,20 @@ public function __construct(ClientInterface $clientStorage, array $responseTypes $this->scopeUtil = $scopeUtil; } + /** + * Handle the authorization request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param boolean $is_authorized + * @param mixed $user_id + * @return mixed|void + * @throws InvalidArgumentException + */ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) { if (!is_bool($is_authorized)) { - throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); + throw new InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); } // We repeat this, because we need to re-validate. The request could be POSTed @@ -101,6 +150,14 @@ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterf $response->setRedirect($this->config['redirect_status_code'], $uri); } + /** + * Set not authorized response + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param string $redirect_uri + * @param mixed $user_id + */ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) { $error = 'access_denied'; @@ -108,9 +165,16 @@ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseI $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message); } - /* + /** * We have made this protected so this class can be extended to add/modify * these parameters + * + * @TODO: add dependency injection for the parameters in this method + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param mixed $user_id + * @return array */ protected function buildAuthorizeParameters($request, $response, $user_id) { @@ -126,10 +190,17 @@ protected function buildAuthorizeParameters($request, $response, $user_id) return $params; } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) { // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) - if (!$client_id = $request->query("client_id")) { + if (!$client_id = $request->query('client_id', $request->request('client_id'))) { // We don't have a good URI to use $response->setError(400, 'invalid_client', "No client id supplied"); @@ -149,7 +220,7 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte // @see http://tools.ietf.org/html/rfc6749#section-3.1.2 // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1 // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1 - if ($supplied_redirect_uri = $request->query('redirect_uri')) { + if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) { // validate there is no fragment supplied $parts = parse_url($supplied_redirect_uri); if (isset($parts['fragment']) && $parts['fragment']) { @@ -181,9 +252,17 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte $redirect_uri = $registered_redirect_uri; } - // Select the redirect URI - $response_type = $request->query('response_type'); - $state = $request->query('state'); + // Select the response type + $response_type = $request->query('response_type', $request->request('response_type')); + + // for multiple-valued response types - make them alphabetical + if (false !== strpos($response_type, ' ')) { + $types = explode(' ', $response_type); + sort($types); + $response_type = ltrim(implode(' ', $types)); + } + + $state = $request->query('state', $request->request('state')); // type and client_id are required if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) { @@ -191,6 +270,7 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte return false; } + if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { if (!isset($this->responseTypes['code'])) { $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); @@ -227,8 +307,8 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte // restrict scope by client specific scope if applicable, // otherwise verify the scope exists $clientScope = $this->clientStorage->getClientScope($client_id); - if ((is_null($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) - || ($clientScope && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { + if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) + || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); return false; @@ -267,10 +347,10 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte /** * Build the absolute URI based on supplied URI and parameters. * - * @param $uri An absolute URI. - * @param $params Parameters to be append as GET. + * @param string $uri An absolute URI. + * @param array $params Parameters to be append as GET. * - * @return + * @return string * An absolute URI with supplied parameters. * * @ingroup oauth2_section_4 @@ -282,15 +362,15 @@ private function buildUri($uri, $params) // Add our params to the parsed uri foreach ($params as $k => $v) { if (isset($parse_url[$k])) { - $parse_url[$k] .= "&" . http_build_query($v); + $parse_url[$k] .= "&" . http_build_query($v, '', '&'); } else { - $parse_url[$k] = http_build_query($v); + $parse_url[$k] = http_build_query($v, '', '&'); } } - // Put humpty dumpty back together + // Put the uri back together return - ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") . ((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") . ((isset($parse_url["host"])) ? $parse_url["host"] : "") @@ -312,19 +392,19 @@ protected function getValidResponseTypes() /** * Internal method for validating redirect URI supplied * - * @param string $inputUri The submitted URI to be validated + * @param string $inputUri The submitted URI to be validated * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to * allow for multiple URIs - * + * @return bool * @see http://tools.ietf.org/html/rfc6749#section-3.1.2 */ - private function validateRedirectUri($inputUri, $registeredUriString) + protected function validateRedirectUri($inputUri, $registeredUriString) { if (!$inputUri || !$registeredUriString) { return false; // if either one is missing, assume INVALID } - $registered_uris = explode(' ', $registeredUriString); + $registered_uris = preg_split('/\s+/', $registeredUriString); foreach ($registered_uris as $registered_uri) { if ($this->config['require_exact_redirect_uri']) { // the input uri is validated against the registered uri using exact match @@ -332,48 +412,67 @@ private function validateRedirectUri($inputUri, $registeredUriString) return true; } } else { + $registered_uri_length = strlen($registered_uri); + if ($registered_uri_length === 0) { + return false; + } + // the input uri is validated against the registered uri using case-insensitive match of the initial string // i.e. additional query parameters may be applied - if (strcasecmp(substr($inputUri, 0, strlen($registered_uri)), $registered_uri) === 0) { + if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) { return true; } } } - //XTEC *********** AFEGIT Check also http - //@pferre22 20.02.2015 - $nossl = preg_replace("/^https:/i", "http:", $inputUri); - if ($inputUri != $nossl) { - return $this->validateRedirectUri($nossl, $registeredUriString); - } - //FI PATCH return false; } /** - * Convenience methods to access the parameters derived from the validated request + * Convenience method to access the scope + * + * @return string */ - public function getScope() { return $this->scope; } + /** + * Convenience method to access the state + * + * @return int + */ public function getState() { return $this->state; } + /** + * Convenience method to access the client id + * + * @return mixed + */ public function getClientId() { return $this->client_id; } + /** + * Convenience method to access the redirect url + * + * @return string + */ public function getRedirectUri() { return $this->redirect_uri; } + /** + * Convenience method to access the response type + * + * @return string + */ public function getResponseType() { return $this->response_type; diff --git a/OAuth2/Controller/AuthorizeControllerInterface.php b/OAuth2/Controller/AuthorizeControllerInterface.php index fa07ae8..f758f97 100644 --- a/OAuth2/Controller/AuthorizeControllerInterface.php +++ b/OAuth2/Controller/AuthorizeControllerInterface.php @@ -11,17 +11,18 @@ * authorization directly, this controller ensures the request is valid, but * requires the application to determine the value of $is_authorized * - * ex: - * > $user_id = $this->somehowDetermineUserId(); - * > $is_authorized = $this->somehowDetermineUserAuthorization(); - * > $response = new OAuth2\Response(); - * > $authorizeController->handleAuthorizeRequest( - * > OAuth2\Request::createFromGlobals(), - * > $response, - * > $is_authorized, - * > $user_id); - * > $response->send(); - * + * @code + * $user_id = $this->somehowDetermineUserId(); + * $is_authorized = $this->somehowDetermineUserAuthorization(); + * $response = new OAuth2\Response(); + * $authorizeController->handleAuthorizeRequest( + * OAuth2\Request::createFromGlobals(), + * $response, + * $is_authorized, + * $user_id + * ); + * $response->send(); + * @endcode */ interface AuthorizeControllerInterface { @@ -37,7 +38,21 @@ interface AuthorizeControllerInterface const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; + /** + * Handle the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param $is_authorized + * @param null $user_id + * @return mixed + */ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); } diff --git a/OAuth2/Controller/ResourceController.php b/OAuth2/Controller/ResourceController.php index e858818..926f90f 100644 --- a/OAuth2/Controller/ResourceController.php +++ b/OAuth2/Controller/ResourceController.php @@ -10,17 +10,43 @@ use OAuth2\Scope; /** - * @see OAuth2\Controller\ResourceControllerInterface + * @see ResourceControllerInterface */ class ResourceController implements ResourceControllerInterface { + /** + * @var array + */ private $token; + /** + * @var TokenTypeInterface + */ protected $tokenType; + + /** + * @var AccessTokenInterface + */ protected $tokenStorage; + + /** + * @var array + */ protected $config; + + /** + * @var ScopeInterface + */ protected $scopeUtil; + /** + * Constructor + * + * @param TokenTypeInterface $tokenType + * @param AccessTokenInterface $tokenStorage + * @param array $config + * @param ScopeInterface $scopeUtil + */ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null) { $this->tokenType = $tokenType; @@ -36,6 +62,14 @@ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $this->scopeUtil = $scopeUtil; } + /** + * Verify the resource request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param null $scope + * @return bool + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) { $token = $this->getAccessTokenData($request, $response); @@ -71,6 +105,13 @@ public function verifyResourceRequest(RequestInterface $request, ResponseInterfa return (bool) $token; } + /** + * Get access token data. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array|null + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) { // Get the token parameter @@ -83,7 +124,7 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")'); } elseif (time() > $token["expires"]) { - $response->setError(401, 'expired_token', 'The access token provided has expired'); + $response->setError(401, 'invalid_token', 'The access token provided has expired'); } else { return $token; } @@ -103,7 +144,11 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface return null; } - // convenience method to allow retrieval of the token + /** + * convenience method to allow retrieval of the token. + * + * @return array + */ public function getToken() { return $this->token; diff --git a/OAuth2/Controller/ResourceControllerInterface.php b/OAuth2/Controller/ResourceControllerInterface.php index 6114219..0e847ca 100644 --- a/OAuth2/Controller/ResourceControllerInterface.php +++ b/OAuth2/Controller/ResourceControllerInterface.php @@ -10,17 +10,32 @@ * call verifyResourceRequest in order to determine if the request * contains a valid token. * - * ex: - * > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { - * > $response->send(); // authorization failed - * > die(); - * > } - * > return json_encode($resource); // valid token! Send the stuff! - * + * @code + * if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { + * $response->send(); // authorization failed + * die(); + * } + * return json_encode($resource); // valid token! Send the stuff! + * @endcode */ interface ResourceControllerInterface { + /** + * Verify the resource request + * + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @param string $scope + * @return mixed + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); + /** + * Get access token data. + * + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return mixed + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); } diff --git a/OAuth2/Controller/TokenController.php b/OAuth2/Controller/TokenController.php index 46cfb17..7fdaf85 100644 --- a/OAuth2/Controller/TokenController.php +++ b/OAuth2/Controller/TokenController.php @@ -10,24 +10,56 @@ use OAuth2\Storage\ClientInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use InvalidArgumentException; +use LogicException; +use RuntimeException; /** - * @see OAuth2\Controller\TokenControllerInterface + * @see TokenControllerInterface */ class TokenController implements TokenControllerInterface { + /** + * @var AccessTokenInterface + */ protected $accessToken; + + /** + * @var array + */ protected $grantTypes; + + /** + * @var ClientAssertionTypeInterface + */ protected $clientAssertionType; + + /** + * @var ScopeInterface + */ protected $scopeUtil; + + /** + * @var ClientInterface + */ protected $clientStorage; + /** + * Constructor + * + * @param AccessTokenInterface $accessToken + * @param ClientInterface $clientStorage + * @param array $grantTypes + * @param ClientAssertionTypeInterface $clientAssertionType + * @param ScopeInterface $scopeUtil + * @throws InvalidArgumentException + */ public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null) { if (is_null($clientAssertionType)) { foreach ($grantTypes as $grantType) { if (!$grantType instanceof ClientAssertionTypeInterface) { - throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface'); + throw new InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface'); } } } @@ -44,6 +76,12 @@ public function __construct(AccessTokenInterface $accessToken, ClientInterface $ $this->scopeUtil = $scopeUtil; } + /** + * Handle the token request. + * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + */ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response) { if ($token = $this->grantAccessToken($request, $response)) { @@ -51,7 +89,11 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface // server MUST disable caching in headers when tokens are involved $response->setStatusCode(200); $response->addParameters($token); - $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); + $response->addHttpHeaders(array( + 'Cache-Control' => 'no-store', + 'Pragma' => 'no-cache', + 'Content-Type' => 'application/json' + )); } } @@ -60,11 +102,13 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface * This would be called from the "/token" endpoint as defined in the spec. * You can call your endpoint whatever you want. * - * @param $request - RequestInterface - * Request object to grant access token + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object * - * @throws InvalidArgumentException - * @throws LogicException + * @return bool|null|array + * + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-10.6 @@ -74,9 +118,15 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response) { - if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + if (strtolower($request->server('REQUEST_METHOD')) === 'options') { + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); + + return null; + } + + if (strtolower($request->server('REQUEST_METHOD')) !== 'post') { $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2'); - $response->addHttpHeaders(array('Allow' => 'POST')); + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); return null; } @@ -98,6 +148,7 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r return null; } + /** @var GrantTypeInterface $grantType */ $grantType = $this->grantTypes[$grantTypeIdentifier]; /** @@ -105,8 +156,8 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r * ClientAssertionTypes allow for grant types which also assert the client data * in which case ClientAssertion is handled in the validateRequest method * - * @see OAuth2\GrantType\JWTBearer - * @see OAuth2\GrantType\ClientCredentials + * @see \OAuth2\GrantType\JWTBearer + * @see \OAuth2\GrantType\ClientCredentials */ if (!$grantType instanceof ClientAssertionTypeInterface) { if (!$this->clientAssertionType->validateRequest($request, $response)) { @@ -155,7 +206,6 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r * * @see http://tools.ietf.org/html/rfc6749#section-3.3 */ - $requestedScope = $this->scopeUtil->getScopeFromRequest($request); $availableScope = $grantType->getScope(); @@ -202,19 +252,82 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r } /** - * addGrantType + * Add grant type * - * @param grantType - OAuth2\GrantTypeInterface - * the grant type to add for the specified identifier - * @param identifier - string - * a string passed in as "grant_type" in the response that will call this grantType + * @param GrantTypeInterface $grantType - the grant type to add for the specified identifier + * @param string|null $identifier - a string passed in as "grant_type" in the response that will call this grantType */ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { if (is_null($identifier) || is_numeric($identifier)) { - $identifier = $grantType->getQuerystringIdentifier(); + $identifier = $grantType->getQueryStringIdentifier(); } $this->grantTypes[$identifier] = $grantType; } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + */ + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response) + { + if ($this->revokeToken($request, $response)) { + $response->setStatusCode(200); + $response->addParameters(array('revoked' => true)); + } + } + + /** + * Revoke a refresh or access token. Returns true on success and when tokens are invalid + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @throws RuntimeException + * @return bool|null + */ + public function revokeToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) === 'options') { + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); + + return null; + } + + if (strtolower($request->server('REQUEST_METHOD')) !== 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS')); + + return null; + } + + $token_type_hint = $request->request('token_type_hint'); + if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) { + $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\''); + + return null; + } + + $token = $request->request('token'); + if ($token === null) { + $response->setError(400, 'invalid_request', 'Missing token parameter to revoke'); + + return null; + } + + // @todo remove this check for v2.0 + if (!method_exists($this->accessToken, 'revokeToken')) { + $class = get_class($this->accessToken); + throw new RuntimeException("AccessToken {$class} does not implement required revokeToken method"); + } + + $this->accessToken->revokeToken($token, $token_type_hint); + + return true; + } } diff --git a/OAuth2/Controller/TokenControllerInterface.php b/OAuth2/Controller/TokenControllerInterface.php index 72d7257..2f83ce4 100644 --- a/OAuth2/Controller/TokenControllerInterface.php +++ b/OAuth2/Controller/TokenControllerInterface.php @@ -10,23 +10,30 @@ * it is called to handle all grant types the application supports. * It also validates the client's credentials * - * ex: - * > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); - * > $response->send(); - * + * @code + * $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); + * $response->send(); + * @endcode */ interface TokenControllerInterface { /** - * handleTokenRequest - * - * @param $request - * OAuth2\RequestInterface - The current http request - * @param $response - * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data + * Handle the token request * + * @param RequestInterface $request - The current http request + * @param ResponseInterface $response - An instance of OAuth2\ResponseInterface to contain the response data */ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * You can call your endpoint whatever you want. + * + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + * + * @return mixed + */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response); } diff --git a/OAuth2/Encryption/EncryptionInterface.php b/OAuth2/Encryption/EncryptionInterface.php index 2d336c6..8dc720a 100644 --- a/OAuth2/Encryption/EncryptionInterface.php +++ b/OAuth2/Encryption/EncryptionInterface.php @@ -4,8 +4,31 @@ interface EncryptionInterface { + /** + * @param $payload + * @param $key + * @param null $algorithm + * @return mixed + */ public function encode($payload, $key, $algorithm = null); + + /** + * @param $payload + * @param $key + * @param null $algorithm + * @return mixed + */ public function decode($payload, $key, $algorithm = null); + + /** + * @param $data + * @return mixed + */ public function urlSafeB64Encode($data); + + /** + * @param $b64 + * @return mixed + */ public function urlSafeB64Decode($b64); } diff --git a/OAuth2/Encryption/FirebaseJwt.php b/OAuth2/Encryption/FirebaseJwt.php new file mode 100644 index 0000000..1b527e0 --- /dev/null +++ b/OAuth2/Encryption/FirebaseJwt.php @@ -0,0 +1,47 @@ + + */ +class FirebaseJwt implements EncryptionInterface +{ + public function __construct() + { + if (!class_exists('\JWT')) { + throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); + } + } + + public function encode($payload, $key, $alg = 'HS256', $keyId = null) + { + return \JWT::encode($payload, $key, $alg, $keyId); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = null) + { + try { + + //Maintain BC: Do not verify if no algorithms are passed in. + if (!$allowedAlgorithms) { + $key = null; + } + + return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); + } catch (\Exception $e) { + return false; + } + } + + public function urlSafeB64Encode($data) + { + return \JWT::urlsafeB64Encode($data); + } + + public function urlSafeB64Decode($b64) + { + return \JWT::urlsafeB64Decode($b64); + } +} diff --git a/OAuth2/Encryption/Jwt.php b/OAuth2/Encryption/Jwt.php index 824ac13..c258b8f 100644 --- a/OAuth2/Encryption/Jwt.php +++ b/OAuth2/Encryption/Jwt.php @@ -2,12 +2,21 @@ namespace OAuth2\Encryption; +use Exception; +use InvalidArgumentException; + /** * @link https://github.com/F21/jwt * @author F21 */ class Jwt implements EncryptionInterface { + /** + * @param $payload + * @param $key + * @param string $algo + * @return string + */ public function encode($payload, $key, $algo = 'HS256') { $header = $this->generateJwtHeader($payload, $algo); @@ -25,7 +34,13 @@ public function encode($payload, $key, $algo = 'HS256') return implode('.', $segments); } - public function decode($jwt, $key = null, $verify = true) + /** + * @param string $jwt + * @param null $key + * @param array|bool $allowedAlgorithms + * @return bool|mixed + */ + public function decode($jwt, $key = null, $allowedAlgorithms = true) { if (!strpos($jwt, '.')) { return false; @@ -49,11 +64,16 @@ public function decode($jwt, $key = null, $verify = true) $sig = $this->urlSafeB64Decode($cryptob64); - if ($verify) { + if ((bool) $allowedAlgorithms) { if (!isset($header['alg'])) { return false; } + // check if bool arg supplied here to maintain BC + if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) { + return false; + } + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { return false; } @@ -62,6 +82,14 @@ public function decode($jwt, $key = null, $verify = true) return $payload; } + /** + * @param $signature + * @param $input + * @param $key + * @param string $algo + * @return bool + * @throws InvalidArgumentException + */ private function verifySignature($signature, $input, $key, $algo = 'HS256') { // use constants when possible, for HipHop support @@ -69,7 +97,10 @@ private function verifySignature($signature, $input, $key, $algo = 'HS256') case'HS256': case'HS384': case'HS512': - return $this->sign($input, $key, $algo) === $signature; + return $this->hash_equals( + $this->sign($input, $key, $algo), + $signature + ); case 'RS256': return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; @@ -81,10 +112,17 @@ private function verifySignature($signature, $input, $key, $algo = 'HS256') return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; default: - throw new \InvalidArgumentException("Unsupported or invalid signing algorithm."); + throw new InvalidArgumentException("Unsupported or invalid signing algorithm."); } } + /** + * @param $input + * @param $key + * @param string $algo + * @return string + * @throws Exception + */ private function sign($input, $key, $algo = 'HS256') { switch ($algo) { @@ -107,19 +145,30 @@ private function sign($input, $key, $algo = 'HS256') return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); default: - throw new \Exception("Unsupported or invalid signing algorithm."); + throw new Exception("Unsupported or invalid signing algorithm."); } } + /** + * @param $input + * @param $key + * @param string $algo + * @return mixed + * @throws Exception + */ private function generateRSASignature($input, $key, $algo) { if (!openssl_sign($input, $signature, $key, $algo)) { - throw new \Exception("Unable to sign data."); + throw new Exception("Unable to sign data."); } return $signature; } + /** + * @param string $data + * @return string + */ public function urlSafeB64Encode($data) { $b64 = base64_encode($data); @@ -130,6 +179,10 @@ public function urlSafeB64Encode($data) return $b64; } + /** + * @param string $b64 + * @return mixed|string + */ public function urlSafeB64Decode($b64) { $b64 = str_replace(array('-', '_'), @@ -149,4 +202,22 @@ protected function generateJwtHeader($payload, $algorithm) 'alg' => $algorithm, ); } -} + + /** + * @param string $a + * @param string $b + * @return bool + */ + protected function hash_equals($a, $b) + { + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + $diff = strlen($a) ^ strlen($b); + for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + + return $diff === 0; + } +} \ No newline at end of file diff --git a/OAuth2/GrantType/AuthorizationCode.php b/OAuth2/GrantType/AuthorizationCode.php index 19145c8..784f6b3 100644 --- a/OAuth2/GrantType/AuthorizationCode.php +++ b/OAuth2/GrantType/AuthorizationCode.php @@ -6,29 +6,47 @@ use OAuth2\ResponseType\AccessTokenInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use Exception; /** - * * @author Brent Shaffer */ class AuthorizationCode implements GrantTypeInterface { + /** + * @var AuthorizationCodeInterface + */ protected $storage; + + /** + * @var array + */ protected $authCode; /** - * @param OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information + * @param AuthorizationCodeInterface $storage - REQUIRED Storage class for retrieving authorization code information */ public function __construct(AuthorizationCodeInterface $storage) { $this->storage = $storage; } - public function getQuerystringIdentifier() + /** + * @return string + */ + public function getQueryStringIdentifier() { return 'authorization_code'; } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + * @throws Exception + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$request->request('code')) { @@ -48,13 +66,8 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 */ - $our_uri = $authCode['redirect_uri']; - $request_uri = $request->request('redirect_uri'); - // Compare URI without parameters (state, code, ...) - // $our_uri is the configured path in the application, so use strncmp - // to assure both strings are equal up to before the query parameters begin - if (isset($authCode['redirect_uri']) && $our_uri) { - if (!$request_uri || strncmp(urldecode($request_uri), $our_uri, strlen($our_uri))) { + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != urldecode($authCode['redirect_uri'])) { $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); return false; @@ -80,21 +93,45 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return true; } + /** + * Get the client id + * + * @return mixed + */ public function getClientId() { return $this->authCode['client_id']; } + /** + * Get the scope + * + * @return string + */ public function getScope() { return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; } + /** + * Get the user id + * + * @return mixed + */ public function getUserId() { return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { $token = $accessToken->createAccessToken($client_id, $user_id, $scope); diff --git a/OAuth2/GrantType/ClientCredentials.php b/OAuth2/GrantType/ClientCredentials.php index f953e4e..e135c2d 100644 --- a/OAuth2/GrantType/ClientCredentials.php +++ b/OAuth2/GrantType/ClientCredentials.php @@ -9,12 +9,19 @@ /** * @author Brent Shaffer * - * @see OAuth2\ClientAssertionType_HttpBasic + * @see HttpBasic */ class ClientCredentials extends HttpBasic implements GrantTypeInterface { + /** + * @var array + */ private $clientData; + /** + * @param ClientCredentialsInterface $storage + * @param array $config + */ public function __construct(ClientCredentialsInterface $storage, array $config = array()) { /** @@ -27,11 +34,21 @@ public function __construct(ClientCredentialsInterface $storage, array $config = parent::__construct($storage, $config); } - public function getQuerystringIdentifier() + /** + * Get query string identifier + * + * @return string + */ + public function getQueryStringIdentifier() { return 'client_credentials'; } + /** + * Get scope + * + * @return string|null + */ public function getScope() { $this->loadClientData(); @@ -39,6 +56,11 @@ public function getScope() return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; } + /** + * Get user id + * + * @return mixed + */ public function getUserId() { $this->loadClientData(); @@ -46,6 +68,15 @@ public function getUserId() return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { /** diff --git a/OAuth2/GrantType/GrantTypeInterface.php b/OAuth2/GrantType/GrantTypeInterface.php old mode 100755 new mode 100644 index 3c14820..f45786f --- a/OAuth2/GrantType/GrantTypeInterface.php +++ b/OAuth2/GrantType/GrantTypeInterface.php @@ -11,10 +11,49 @@ */ interface GrantTypeInterface { - public function getQuerystringIdentifier(); - //public function validateRequest(RequestInterface $request, ResponseInterface $response); - //public function getClientId(); + /** + * Get query string identifier + * + * @return string + */ + public function getQueryStringIdentifier(); + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response); + + /** + * Get client id + * + * @return mixed + */ + public function getClientId(); + + /** + * Get user id + * + * @return mixed + */ public function getUserId(); + + /** + * Get scope + * + * @return string|null + */ public function getScope(); + + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope); } diff --git a/OAuth2/GrantType/JwtBearer.php b/OAuth2/GrantType/JwtBearer.php index ebc5d5a..62c1efa 100644 --- a/OAuth2/GrantType/JwtBearer.php +++ b/OAuth2/GrantType/JwtBearer.php @@ -25,15 +25,19 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface protected $storage; protected $audience; protected $jwtUtil; + protected $allowedAlgorithms; /** * Creates an instance of the JWT bearer grant type. * - * @param OAuth2\Storage\JWTBearerInterface $storage A valid storage interface that implements storage hooks for the JWT bearer grant type. - * @param string $audience The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint. - * @param OAuth2\Encryption\JWT $jwtUtil OPTONAL The class used to decode, encode and verify JWTs. + * @param JwtBearerInterface $storage - A valid storage interface that implements storage hooks for the JWT + * bearer grant type. + * @param string $audience - The audience to validate the token against. This is usually the full + * URI of the OAuth token requests endpoint. + * @param EncryptionInterface|JWT $jwtUtil - OPTONAL The class used to decode, encode and verify JWTs. + * @param array $config */ - public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null) + public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array()) { $this->storage = $storage; $this->audience = $audience; @@ -42,18 +46,23 @@ public function __construct(JwtBearerInterface $storage, $audience, EncryptionIn $jwtUtil = new Jwt(); } + $this->config = array_merge(array( + 'allowed_algorithms' => array('RS256', 'RS384', 'RS512') + ), $config); + $this->jwtUtil = $jwtUtil; + + $this->allowedAlgorithms = $this->config['allowed_algorithms']; } /** * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant. * - * @return - * The string identifier for grant_type. + * @return string - The string identifier for grant_type. * - * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier() + * @see GrantTypeInterface::getQueryStringIdentifier() */ - public function getQuerystringIdentifier() + public function getQueryStringIdentifier() { return 'urn:ietf:params:oauth:grant-type:jwt-bearer'; } @@ -61,10 +70,9 @@ public function getQuerystringIdentifier() /** * Validates the data from the decoded JWT. * - * @return - * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned. - * - * @see OAuth2\GrantType\GrantTypeInterface::getTokenData() + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.@see GrantTypeInterface::getTokenData() */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { @@ -177,7 +185,7 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re } // Verify the JWT - if (!$this->jwtUtil->decode($undecodedJWT, $key, true)) { + if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) { $response->setError(400, 'invalid_grant', "JWT failed signature verification"); return null; @@ -188,16 +196,31 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return true; } + /** + * Get client id + * + * @return mixed + */ public function getClientId() { return $this->jwt['iss']; } + /** + * Get user id + * + * @return mixed + */ public function getUserId() { return $this->jwt['sub']; } + /** + * Get scope + * + * @return null + */ public function getScope() { return null; @@ -207,7 +230,13 @@ public function getScope() * Creates an access token that is NOT associated with a refresh token. * If a subject (sub) the name of the user/account we are accessing data on behalf of. * - * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken() + * @see GrantTypeInterface::createAccessToken() + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { diff --git a/OAuth2/GrantType/RefreshToken.php b/OAuth2/GrantType/RefreshToken.php index d6691df..75c611f 100644 --- a/OAuth2/GrantType/RefreshToken.php +++ b/OAuth2/GrantType/RefreshToken.php @@ -8,38 +8,67 @@ use OAuth2\ResponseInterface; /** - * * @author Brent Shaffer */ class RefreshToken implements GrantTypeInterface { + /** + * @var array + */ private $refreshToken; + /** + * @var RefreshTokenInterface + */ protected $storage; + + /** + * @var array + */ protected $config; /** - * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request - * ); - * + * @param RefreshTokenInterface $storage - REQUIRED Storage class for retrieving refresh token information + * @param array $config - OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request + * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using + * ); + * @endcode */ public function __construct(RefreshTokenInterface $storage, $config = array()) { $this->config = array_merge(array( - 'always_issue_new_refresh_token' => false + 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true ), $config); + + // to preserve B.C. with v1.6 + // @see https://github.com/bshaffer/oauth2-server-php/pull/580 + // @todo - remove in v2.0 + if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) { + $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token']; + } + $this->storage = $storage; } - public function getQuerystringIdentifier() + /** + * @return string + */ + public function getQueryStringIdentifier() { return 'refresh_token'; } + /** + * Validate the OAuth request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$request->request("refresh_token")) { @@ -66,21 +95,45 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return true; } + /** + * Get client id + * + * @return mixed + */ public function getClientId() { return $this->refreshToken['client_id']; } + /** + * Get user id + * + * @return mixed|null + */ public function getUserId() { return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; } + /** + * Get scope + * + * @return null|string + */ public function getScope() { return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { /* @@ -89,9 +142,10 @@ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, * @see http://tools.ietf.org/html/rfc6749#section-6 */ $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; + $unsetRefreshToken = $this->config['unset_refresh_token_after_use']; $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); - if ($issueNewRefreshToken) { + if ($unsetRefreshToken) { $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); } diff --git a/OAuth2/GrantType/UserCredentials.php b/OAuth2/GrantType/UserCredentials.php index f165538..b10c2dd 100644 --- a/OAuth2/GrantType/UserCredentials.php +++ b/OAuth2/GrantType/UserCredentials.php @@ -6,30 +6,46 @@ use OAuth2\ResponseType\AccessTokenInterface; use OAuth2\RequestInterface; use OAuth2\ResponseInterface; +use LogicException; /** - * * @author Brent Shaffer */ class UserCredentials implements GrantTypeInterface { + /** + * @var array + */ private $userInfo; + /** + * @var UserCredentialsInterface + */ protected $storage; /** - * @param OAuth2\Storage\UserCredentialsInterface $storage REQUIRED Storage class for retrieving user credentials information + * @param UserCredentialsInterface $storage - REQUIRED Storage class for retrieving user credentials information */ public function __construct(UserCredentialsInterface $storage) { $this->storage = $storage; } - public function getQuerystringIdentifier() + /** + * @return string + */ + public function getQueryStringIdentifier() { return 'password'; } + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|mixed|null + * + * @throws LogicException + */ public function validateRequest(RequestInterface $request, ResponseInterface $response) { if (!$request->request("password") || !$request->request("username")) { @@ -61,21 +77,45 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return true; } + /** + * Get client id + * + * @return mixed|null + */ public function getClientId() { return null; } + /** + * Get user id + * + * @return mixed + */ public function getUserId() { return $this->userInfo['user_id']; } + /** + * Get scope + * + * @return null|string + */ public function getScope() { return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; } + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { return $accessToken->createAccessToken($client_id, $user_id, $scope); diff --git a/OAuth2/OpenID/Controller/AuthorizeController.php b/OAuth2/OpenID/Controller/AuthorizeController.php index c6fe4f9..54c5f9a 100644 --- a/OAuth2/OpenID/Controller/AuthorizeController.php +++ b/OAuth2/OpenID/Controller/AuthorizeController.php @@ -11,8 +11,19 @@ */ class AuthorizeController extends BaseAuthorizeController implements AuthorizeControllerInterface { + /** + * @var mixed + */ private $nonce; + /** + * Set not authorized response + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param string $redirect_uri + * @param null $user_id + */ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) { $prompt = $request->query('prompt', 'consent'); @@ -32,6 +43,14 @@ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseI $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message); } + /** + * @TODO: add dependency injection for the parameters in this method + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param mixed $user_id + * @return array + */ protected function buildAuthorizeParameters($request, $response, $user_id) { if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) { @@ -49,6 +68,11 @@ protected function buildAuthorizeParameters($request, $response, $user_id) return $params; } + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) { if (!parent::validateAuthorizeRequest($request, $response)) { @@ -57,8 +81,8 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte $nonce = $request->query('nonce'); - // Validate required nonce for "id_token" and "token id_token" - if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_TOKEN_ID_TOKEN))) { + // Validate required nonce for "id_token" and "id_token token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); return false; @@ -69,13 +93,19 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte return true; } + /** + * Array of valid response types + * + * @return array + */ protected function getValidResponseTypes() { return array( self::RESPONSE_TYPE_ACCESS_TOKEN, self::RESPONSE_TYPE_AUTHORIZATION_CODE, self::RESPONSE_TYPE_ID_TOKEN, - self::RESPONSE_TYPE_TOKEN_ID_TOKEN, + self::RESPONSE_TYPE_ID_TOKEN_TOKEN, + self::RESPONSE_TYPE_CODE_ID_TOKEN, ); } @@ -86,11 +116,8 @@ protected function getValidResponseTypes() * method checks whether OpenID Connect is enabled in the server settings * and whether the openid scope was requested. * - * @param $request_scope - * A space-separated string of scopes. - * - * @return - * TRUE if an id token is needed, FALSE otherwise. + * @param string $request_scope - A space-separated string of scopes. + * @return boolean - TRUE if an id token is needed, FALSE otherwise. */ public function needsIdToken($request_scope) { @@ -98,6 +125,9 @@ public function needsIdToken($request_scope) return $this->scopeUtil->checkScope('openid', $request_scope); } + /** + * @return mixed + */ public function getNonce() { return $this->nonce; diff --git a/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php index 62704e7..b4967c3 100644 --- a/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php +++ b/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -5,5 +5,8 @@ interface AuthorizeControllerInterface { const RESPONSE_TYPE_ID_TOKEN = 'id_token'; - const RESPONSE_TYPE_TOKEN_ID_TOKEN = 'token id_token'; + + const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token'; + + const RESPONSE_TYPE_CODE_ID_TOKEN = 'code id_token'; } diff --git a/OAuth2/OpenID/Controller/UserInfoController.php b/OAuth2/OpenID/Controller/UserInfoController.php index 30cb942..c489b7a 100644 --- a/OAuth2/OpenID/Controller/UserInfoController.php +++ b/OAuth2/OpenID/Controller/UserInfoController.php @@ -16,30 +16,34 @@ */ class UserInfoController extends ResourceController implements UserInfoControllerInterface { - private $token; - - protected $tokenType; - protected $tokenStorage; + /** + * @var UserClaimsInterface + */ protected $userClaimsStorage; - protected $config; - protected $scopeUtil; + /** + * Constructor + * + * @param TokenTypeInterface $tokenType + * @param AccessTokenInterface $tokenStorage + * @param UserClaimsInterface $userClaimsStorage + * @param array $config + * @param ScopeInterface $scopeUtil + */ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null) { - $this->tokenType = $tokenType; - $this->tokenStorage = $tokenStorage; - $this->userClaimsStorage = $userClaimsStorage; - - $this->config = array_merge(array( - 'www_realm' => 'Service', - ), $config); + parent::__construct($tokenType, $tokenStorage, $config, $scopeUtil); - if (is_null($scopeUtil)) { - $scopeUtil = new Scope(); - } - $this->scopeUtil = $scopeUtil; + $this->userClaimsStorage = $userClaimsStorage; } + /** + * Handle the user info request + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) { if (!$this->verifyResourceRequest($request, $response, 'openid')) { @@ -55,4 +59,4 @@ public function handleUserInfoRequest(RequestInterface $request, ResponseInterfa ); $response->addParameters($claims); } -} +} \ No newline at end of file diff --git a/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/OAuth2/OpenID/Controller/UserInfoControllerInterface.php index a89049d..88e9228 100644 --- a/OAuth2/OpenID/Controller/UserInfoControllerInterface.php +++ b/OAuth2/OpenID/Controller/UserInfoControllerInterface.php @@ -9,15 +9,22 @@ * This controller is called when the user claims for OpenID Connect's * UserInfo endpoint should be returned. * - * ex: - * > $response = new OAuth2\Response(); - * > $userInfoController->handleUserInfoRequest( - * > OAuth2\Request::createFromGlobals(), - * > $response; - * > $response->send(); - * + * @code + * $response = new OAuth2\Response(); + * $userInfoController->handleUserInfoRequest( + * OAuth2\Request::createFromGlobals(), + * $response + * ); + * $response->send(); + * @endcode */ interface UserInfoControllerInterface { + /** + * Handle user info request + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); } diff --git a/OAuth2/OpenID/GrantType/AuthorizationCode.php b/OAuth2/OpenID/GrantType/AuthorizationCode.php index 8ed1edc..ee113a0 100644 --- a/OAuth2/OpenID/GrantType/AuthorizationCode.php +++ b/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -6,11 +6,19 @@ use OAuth2\ResponseType\AccessTokenInterface; /** - * * @author Brent Shaffer */ class AuthorizationCode extends BaseAuthorizationCode { + /** + * Create access token + * + * @param AccessTokenInterface $accessToken + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user id associated with the access token + * @param string $scope - scopes to be stored in space-separated string. + * @return array + */ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { $includeRefreshToken = true; diff --git a/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/OAuth2/OpenID/ResponseType/AuthorizationCode.php index 8971954..b8ad41f 100644 --- a/OAuth2/OpenID/ResponseType/AuthorizationCode.php +++ b/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -6,16 +6,26 @@ use OAuth2\OpenID\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface; /** - * * @author Brent Shaffer */ class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface { + /** + * Constructor + * + * @param AuthorizationCodeStorageInterface $storage + * @param array $config + */ public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) { parent::__construct($storage, $config); } + /** + * @param $params + * @param null $user_id + * @return array + */ public function getAuthorizeResponse($params, $user_id = null) { // build the URL to redirect to @@ -35,18 +45,14 @@ public function getAuthorizeResponse($params, $user_id = null) /** * Handle the creation of the authorization code. * - * @param $client_id - * Client identifier related to the authorization code - * @param $user_id - * User ID associated with the authorization code - * @param $redirect_uri - * An absolute URI to which the authorization server will redirect the - * user-agent to when the end-user authorization step is completed. - * @param $scope - * (optional) Scopes to be stored in space-separated string. - * @param $id_token - * (optional) The OpenID Connect id_token. + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @param string $id_token - OPTIONAL The OpenID Connect id_token. * + * @return string * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 */ diff --git a/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php index ea47792..eb94ef0 100644 --- a/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php +++ b/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php @@ -5,7 +5,6 @@ use OAuth2\ResponseType\AuthorizationCodeInterface as BaseAuthorizationCodeInterface; /** - * * @author Brent Shaffer */ interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface @@ -13,12 +12,13 @@ interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface /** * Handle the creation of the authorization code. * - * @param $client_id Client identifier related to the authorization code - * @param $user_id User ID associated with the authorization code - * @param $redirect_uri An absolute URI to which the authorization server will redirect the - * user-agent to when the end-user authorization step is completed. - * @param $scope OPTIONAL Scopes to be stored in space-separated string. - * @param $id_token OPTIONAL The OpenID Connect id_token. + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @param string $id_token - OPTIONAL The OpenID Connect id_token. + * @return string * * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 diff --git a/OAuth2/OpenID/ResponseType/CodeIdToken.php b/OAuth2/OpenID/ResponseType/CodeIdToken.php new file mode 100644 index 0000000..2696ada --- /dev/null +++ b/OAuth2/OpenID/ResponseType/CodeIdToken.php @@ -0,0 +1,40 @@ +authCode = $authCode; + $this->idToken = $idToken; + } + + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->authCode->getAuthorizeResponse($params, $user_id); + $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id); + $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token']; + + return $result; + } +} diff --git a/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php b/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php similarity index 61% rename from OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php rename to OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php index 1b4c513..629adcc 100644 --- a/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php +++ b/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php @@ -4,6 +4,6 @@ use OAuth2\ResponseType\ResponseTypeInterface; -interface TokenIdTokenInterface extends ResponseTypeInterface +interface CodeIdTokenInterface extends ResponseTypeInterface { } diff --git a/OAuth2/OpenID/ResponseType/IdToken.php b/OAuth2/OpenID/ResponseType/IdToken.php index 4696155..55e4460 100644 --- a/OAuth2/OpenID/ResponseType/IdToken.php +++ b/OAuth2/OpenID/ResponseType/IdToken.php @@ -6,14 +6,38 @@ use OAuth2\Encryption\Jwt; use OAuth2\Storage\PublicKeyInterface; use OAuth2\OpenID\Storage\UserClaimsInterface; +use LogicException; class IdToken implements IdTokenInterface { + /** + * @var UserClaimsInterface + */ protected $userClaimsStorage; + /** + * @var PublicKeyInterface + */ protected $publicKeyStorage; + + /** + * @var array + */ protected $config; + + /** + * @var EncryptionInterface + */ protected $encryptionUtil; + /** + * Constructor + * + * @param UserClaimsInterface $userClaimsStorage + * @param PublicKeyInterface $publicKeyStorage + * @param array $config + * @param EncryptionInterface $encryptionUtil + * @throws LogicException + */ public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null) { $this->userClaimsStorage = $userClaimsStorage; @@ -24,13 +48,18 @@ public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInt $this->encryptionUtil = $encryptionUtil; if (!isset($config['issuer'])) { - throw new \LogicException('config parameter "issuer" must be set'); + throw new LogicException('config parameter "issuer" must be set'); } $this->config = array_merge(array( 'id_lifetime' => 3600, ), $config); } + /** + * @param array $params + * @param null $userInfo + * @return array|mixed + */ public function getAuthorizeResponse($params, $userInfo = null) { // build the URL to redirect to @@ -50,6 +79,16 @@ public function getAuthorizeResponse($params, $userInfo = null) return array($params['redirect_uri'], $result); } + /** + * Create id token + * + * @param string $client_id + * @param mixed $userInfo + * @param mixed $nonce + * @param mixed $userClaims + * @param mixed $access_token + * @return mixed|string + */ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) { // pull auth_time from user info if supplied @@ -79,17 +118,27 @@ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims return $this->encodeToken($token, $client_id); } + /** + * @param $access_token + * @param null $client_id + * @return mixed|string + */ protected function createAtHash($access_token, $client_id = null) { // maps HS256 and RS256 to sha256, etc. $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); $hash_algorithm = 'sha' . substr($algorithm, 2); - $hash = hash($hash_algorithm, $access_token); + $hash = hash($hash_algorithm, $access_token, true); $at_hash = substr($hash, 0, strlen($hash) / 2); return $this->encryptionUtil->urlSafeB64Encode($at_hash); } + /** + * @param array $token + * @param null $client_id + * @return mixed|string + */ protected function encodeToken(array $token, $client_id = null) { $private_key = $this->publicKeyStorage->getPrivateKey($client_id); @@ -98,6 +147,11 @@ protected function encodeToken(array $token, $client_id = null) return $this->encryptionUtil->encode($token, $private_key, $algorithm); } + /** + * @param $userInfo + * @return array + * @throws LogicException + */ private function getUserIdAndAuthTime($userInfo) { $auth_time = null; @@ -105,7 +159,7 @@ private function getUserIdAndAuthTime($userInfo) // support an array for user_id / auth_time if (is_array($userInfo)) { if (!isset($userInfo['user_id'])) { - throw new \LogicException('if $user_id argument is an array, user_id index must be set'); + throw new LogicException('if $user_id argument is an array, user_id index must be set'); } $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; diff --git a/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/OAuth2/OpenID/ResponseType/IdTokenInterface.php index 0bd2f83..226a3bc 100644 --- a/OAuth2/OpenID/ResponseType/IdTokenInterface.php +++ b/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -15,12 +15,13 @@ interface IdTokenInterface extends ResponseTypeInterface * If the Implicit Flow is used, the token and id_token are generated and * returned together. * - * @param string $client_id The client id. - * @param string $user_id The user id. - * @param string $nonce OPTIONAL The nonce. - * @param string $userClaims OPTIONAL Claims about the user. - * @param string $access_token OPTIONAL The access token, if known. - * + * @param string $client_id - The client id. + * @param mixed $userInfo - User info + * @param string $nonce - OPTIONAL The nonce. + * @param string $userClaims - OPTIONAL Claims about the user. + * @param string $access_token - OPTIONAL The access token, if known. + + * @internal param string $user_id - The user id. * @return string The ID Token represented as a JSON Web Token (JWT). * * @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken diff --git a/OAuth2/OpenID/ResponseType/TokenIdToken.php b/OAuth2/OpenID/ResponseType/IdTokenToken.php similarity index 64% rename from OAuth2/OpenID/ResponseType/TokenIdToken.php rename to OAuth2/OpenID/ResponseType/IdTokenToken.php index 5567569..94c51ae 100644 --- a/OAuth2/OpenID/ResponseType/TokenIdToken.php +++ b/OAuth2/OpenID/ResponseType/IdTokenToken.php @@ -4,17 +4,35 @@ use OAuth2\ResponseType\AccessTokenInterface; -class TokenIdToken implements TokenIdTokenInterface +class IdTokenToken implements IdTokenTokenInterface { + /** + * @var AccessTokenInterface + */ protected $accessToken; + + /** + * @var IdTokenInterface + */ protected $idToken; - public function __construct(AccessTokenInterface $accessToken, IdToken $idToken) + /** + * Constructor + * + * @param AccessTokenInterface $accessToken + * @param IdTokenInterface $idToken + */ + public function __construct(AccessTokenInterface $accessToken, IdTokenInterface $idToken) { $this->accessToken = $accessToken; $this->idToken = $idToken; } + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ public function getAuthorizeResponse($params, $user_id = null) { $result = $this->accessToken->getAuthorizeResponse($params, $user_id); diff --git a/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php b/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php new file mode 100644 index 0000000..ac13e20 --- /dev/null +++ b/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php @@ -0,0 +1,9 @@ + value format. + * @return array - An array in the claim => value format. * * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims */ diff --git a/OAuth2/Request.php b/OAuth2/Request.php index faec0ce..f547bf6 100644 --- a/OAuth2/Request.php +++ b/OAuth2/Request.php @@ -2,6 +2,8 @@ namespace OAuth2; +use LogicException; + /** * OAuth2\Request * This class is taken from the Symfony2 Framework and is part of the Symfony package. @@ -21,13 +23,14 @@ class Request implements RequestInterface /** * Constructor. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string $content The raw body data + * @param array $query - The GET parameters + * @param array $request - The POST parameters + * @param array $attributes - The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies - The COOKIE parameters + * @param array $files - The FILES parameters + * @param array $server - The SERVER parameters + * @param string $content - The raw body data + * @param array $headers - The headers * * @api */ @@ -41,13 +44,14 @@ public function __construct(array $query = array(), array $request = array(), ar * * This method also re-initializes all properties. * - * @param array $query The GET parameters - * @param array $request The POST parameters - * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) - * @param array $cookies The COOKIE parameters - * @param array $files The FILES parameters - * @param array $server The SERVER parameters - * @param string $content The raw body data + * @param array $query - The GET parameters + * @param array $request - The POST parameters + * @param array $attributes - The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies - The COOKIE parameters + * @param array $files - The FILES parameters + * @param array $server - The SERVER parameters + * @param string $content - The raw body data + * @param array $headers - The headers * * @api */ @@ -60,24 +64,49 @@ public function initialize(array $query = array(), array $request = array(), arr $this->files = $files; $this->server = $server; $this->content = $content; - $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; + + if ($headers === null) { + $headers = array(); + } + + $this->headers = $headers + $this->getHeadersFromServer($this->server); } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function query($name, $default = null) { return isset($this->query[$name]) ? $this->query[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function request($name, $default = null) { return isset($this->request[$name]) ? $this->request[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function server($name, $default = null) { return isset($this->server[$name]) ? $this->server[$name] : $default; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function headers($name, $default = null) { $headers = array_change_key_case($this->headers); @@ -86,6 +115,9 @@ public function headers($name, $default = null) return isset($headers[$name]) ? $headers[$name] : $default; } + /** + * @return array + */ public function getAllQueryParameters() { return $this->query; @@ -94,14 +126,15 @@ public function getAllQueryParameters() /** * Returns the request body content. * - * @param Boolean $asResource If true, a resource will be returned + * @param boolean $asResource - If true, a resource will be returned + * @return string|resource - The request body content or a resource to read the body stream. * - * @return string|resource The request body content or a resource to read the body stream. + * @throws LogicException */ public function getContent($asResource = false) { if (false === $this->content || (true === $asResource && null !== $this->content)) { - throw new \LogicException('getContent() can only be called once when using the resource return type.'); + throw new LogicException('getContent() can only be called once when using the resource return type.'); } if (true === $asResource) { @@ -117,6 +150,10 @@ public function getContent($asResource = false) return $this->content; } + /** + * @param array $server + * @return array + */ private function getHeadersFromServer($server) { $headers = array(); @@ -185,13 +222,15 @@ private function getHeadersFromServer($server) /** * Creates a new request with values from PHP's super globals. * - * @return Request A new request + * @return Request - A new request * * @api */ public static function createFromGlobals() { - $class = __CLASS__; + $class = get_called_class(); + + /** @var Request $request */ $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); $contentType = $request->server('CONTENT_TYPE', ''); diff --git a/OAuth2/RequestInterface.php b/OAuth2/RequestInterface.php index 8a70d5f..1d036b7 100644 --- a/OAuth2/RequestInterface.php +++ b/OAuth2/RequestInterface.php @@ -4,13 +4,36 @@ interface RequestInterface { + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function query($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function request($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function server($name, $default = null); + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function headers($name, $default = null); + /** + * @return mixed + */ public function getAllQueryParameters(); } diff --git a/OAuth2/Response.php b/OAuth2/Response.php index d8eabe7..88c1ad5 100644 --- a/OAuth2/Response.php +++ b/OAuth2/Response.php @@ -2,6 +2,8 @@ namespace OAuth2; +use InvalidArgumentException; + /** * Class to handle OAuth2 Responses in a graceful way. Use this interface * to output the proper OAuth2 responses. @@ -13,12 +15,34 @@ */ class Response implements ResponseInterface { + /** + * @var string + */ public $version; + + /** + * @var int + */ protected $statusCode = 200; + + /** + * @var string + */ protected $statusText; + + /** + * @var array + */ protected $parameters = array(); + + /** + * @var array + */ protected $httpHeaders = array(); + /** + * @var array + */ public static $statusTexts = array( 100 => 'Continue', 101 => 'Switching Protocols', @@ -63,6 +87,11 @@ class Response implements ResponseInterface 505 => 'HTTP Version Not Supported', ); + /** + * @param array $parameters + * @param int $statusCode + * @param array $headers + */ public function __construct($parameters = array(), $statusCode = 200, $headers = array()) { $this->setParameters($parameters); @@ -102,81 +131,133 @@ protected function buildHeader($name, $value) return sprintf("%s: %s\n", $name, $value); } + /** + * @return int + */ public function getStatusCode() { return $this->statusCode; } + /** + * @param int $statusCode + * @param string $text + * @throws InvalidArgumentException + */ public function setStatusCode($statusCode, $text = null) { $this->statusCode = (int) $statusCode; if ($this->isInvalid()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); } $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); } + /** + * @return string + */ public function getStatusText() { return $this->statusText; } + /** + * @return array + */ public function getParameters() { return $this->parameters; } + /** + * @param array $parameters + */ public function setParameters(array $parameters) { $this->parameters = $parameters; } + /** + * @param array $parameters + */ public function addParameters(array $parameters) { $this->parameters = array_merge($this->parameters, $parameters); } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getParameter($name, $default = null) { return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; } + /** + * @param string $name + * @param mixed $value + */ public function setParameter($name, $value) { $this->parameters[$name] = $value; } + /** + * @param array $httpHeaders + */ public function setHttpHeaders(array $httpHeaders) { $this->httpHeaders = $httpHeaders; } + /** + * @param string $name + * @param mixed $value + */ public function setHttpHeader($name, $value) { $this->httpHeaders[$name] = $value; } + /** + * @param array $httpHeaders + */ public function addHttpHeaders(array $httpHeaders) { $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); } + /** + * @return array + */ public function getHttpHeaders() { return $this->httpHeaders; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getHttpHeader($name, $default = null) { return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default; } + /** + * @param string $format + * @return mixed + * @throws InvalidArgumentException + */ public function getResponseBody($format = 'json') { switch ($format) { case 'json': - return json_encode($this->parameters); + return $this->parameters ? json_encode($this->parameters) : ''; case 'xml': // this only works for single-level arrays $xml = new \SimpleXMLElement(''); @@ -187,10 +268,13 @@ public function getResponseBody($format = 'json') return $xml->asXML(); } - throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format)); + throw new InvalidArgumentException(sprintf('The format %s is not supported', $format)); } + /** + * @param string $format + */ public function send($format = 'json') { // headers have already been sent by the developer @@ -215,6 +299,14 @@ public function send($format = 'json') echo $this->getResponseBody($format); } + /** + * @param int $statusCode + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + * @throws InvalidArgumentException + */ public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) { $parameters = array( @@ -239,14 +331,24 @@ public function setError($statusCode, $error, $errorDescription = null, $errorUr $this->addHttpHeaders($httpHeaders); if (!$this->isClientError() && !$this->isServerError()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); } } + /** + * @param int $statusCode + * @param string $url + * @param string $state + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + * @throws InvalidArgumentException + */ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) { if (empty($url)) { - throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + throw new InvalidArgumentException('Cannot redirect to an empty URL.'); } $parameters = array(); @@ -264,22 +366,23 @@ public function setRedirect($statusCode, $url, $state = null, $error = null, $er if (count($this->parameters) > 0) { // add parameters to URL redirection $parts = parse_url($url); - $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; + $sep = isset($parts['query']) && !empty($parts['query']) ? '&' : '?'; $url .= $sep . http_build_query($this->parameters); } $this->addHttpHeaders(array('Location' => $url)); if (!$this->isRedirection()) { - throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); + throw new InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); } } - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html /** * @return Boolean * * @api + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html */ public function isInvalid() { @@ -336,8 +439,11 @@ public function isServerError() return $this->statusCode >= 500 && $this->statusCode < 600; } - /* - * Functions from Symfony2 HttpFoundation - output pretty header + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param array $headers + * @return string */ private function getHttpHeadersAsString($headers) { @@ -357,11 +463,23 @@ private function getHttpHeadersAsString($headers) return $content; } + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param string $name + * @return mixed + */ private function beautifyHeaderName($name) { return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name)); } + /** + * Function from Symfony2 HttpFoundation - output pretty header + * + * @param array $match + * @return string + */ private function beautifyCallback($match) { return '-'.strtoupper($match[1]); diff --git a/OAuth2/ResponseInterface.php b/OAuth2/ResponseInterface.php index c99b5f7..fe92086 100644 --- a/OAuth2/ResponseInterface.php +++ b/OAuth2/ResponseInterface.php @@ -6,19 +6,48 @@ * Interface which represents an object response. Meant to handle and display the proper OAuth2 Responses * for errors and successes * - * @see OAuth2\Response + * @see \OAuth2\Response */ interface ResponseInterface { + /** + * @param array $parameters + */ public function addParameters(array $parameters); + /** + * @param array $httpHeaders + */ public function addHttpHeaders(array $httpHeaders); + /** + * @param int $statusCode + */ public function setStatusCode($statusCode); + /** + * @param int $statusCode + * @param string $name + * @param string $description + * @param string $uri + * @return mixed + */ public function setError($statusCode, $name, $description = null, $uri = null); + /** + * @param int $statusCode + * @param string $url + * @param string $state + * @param string $error + * @param string $errorDescription + * @param string $errorUri + * @return mixed + */ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null); + /** + * @param string $name + * @return mixed + */ public function getParameter($name); } diff --git a/OAuth2/ResponseType/AccessToken.php b/OAuth2/ResponseType/AccessToken.php index b05bf87..e836a34 100644 --- a/OAuth2/ResponseType/AccessToken.php +++ b/OAuth2/ResponseType/AccessToken.php @@ -4,28 +4,39 @@ use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface; use OAuth2\Storage\RefreshTokenInterface; +use RuntimeException; /** - * * @author Brent Shaffer */ class AccessToken implements AccessTokenInterface { + /** + * @var AccessTokenInterface + */ protected $tokenStorage; + + /** + * @var RefreshTokenInterface + */ protected $refreshStorage; + + /** + * @var array + */ protected $config; /** - * @param OAuth2\Storage\AccessTokenInterface $tokenStorage REQUIRED Storage class for saving access token information - * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information - * @param array $config OPTIONAL Configuration options for the server - * - * $config = array( - * 'token_type' => 'bearer', // token type identifier - * 'access_lifetime' => 3600, // time before access token expires - * 'refresh_token_lifetime' => 1209600, // time before refresh token expires - * ); - * + * @param AccessTokenStorageInterface $tokenStorage - REQUIRED Storage class for saving access token information + * @param RefreshTokenInterface $refreshStorage - OPTIONAL Storage class for saving refresh token information + * @param array $config - OPTIONAL Configuration options for the server + * @code + * $config = array( + * 'token_type' => 'bearer', // token type identifier + * 'access_lifetime' => 3600, // time before access token expires + * 'refresh_token_lifetime' => 1209600, // time before refresh token expires + * ); + * @endcode */ public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) { @@ -39,6 +50,13 @@ public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTo ), $config); } + /** + * Get authorize response + * + * @param array $params + * @param mixed $user_id + * @return array + */ public function getAuthorizeResponse($params, $user_id = null) { // build the URL to redirect to @@ -64,10 +82,11 @@ public function getAuthorizeResponse($params, $user_id = null) /** * Handle the creation of access token, also issue refresh token if supported / desirable. * - * @param $client_id client identifier related to the access token. - * @param $user_id user ID associated with the access token - * @param $scope OPTIONAL scopes to be stored in space-separated string. - * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user ID associated with the access token + * @param string $scope - OPTIONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - if true, a new refresh_token will be added to the response + * @return array * * @see http://tools.ietf.org/html/rfc6749#section-5 * @ingroup oauth2_section_5 @@ -107,25 +126,40 @@ public function createAccessToken($client_id, $user_id, $scope = null, $includeR * Implementing classes may want to override this function to implement * other access token generation schemes. * - * @return - * An unique access token. + * @return string - A unique access token. * * @ingroup oauth2_section_4 */ protected function generateAccessToken() { - $tokenLen = 40; + if (function_exists('random_bytes')) { + $randomData = random_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } if (function_exists('mcrypt_create_iv')) { - $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - $randomData = openssl_random_pseudo_bytes(100); - } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data - $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); - } else { - $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } } + if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + // Last resort which you probably should just get rid of: + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); - return substr(hash('sha512', $randomData), 0, $tokenLen); + return substr(hash('sha512', $randomData), 0, 40); } /** @@ -134,8 +168,7 @@ protected function generateAccessToken() * Implementing classes may want to override this function to implement * other refresh token generation schemes. * - * @return - * An unique refresh. + * @return string - A unique refresh token. * * @ingroup oauth2_section_4 * @see OAuth2::generateAccessToken() @@ -144,4 +177,42 @@ protected function generateRefreshToken() { return $this->generateAccessToken(); // let's reuse the same scheme for token generation } + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + * + * @param $token + * @param null $tokenTypeHint + * @throws RuntimeException + * @return boolean + */ + public function revokeToken($token, $tokenTypeHint = null) + { + if ($tokenTypeHint == 'refresh_token') { + if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) { + return true; + } + } + + /** @TODO remove in v2 */ + if (!method_exists($this->tokenStorage, 'unsetAccessToken')) { + throw new RuntimeException( + sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage) + )); + } + + $revoked = $this->tokenStorage->unsetAccessToken($token); + + // if a typehint is supplied and fails, try other storages + // @see https://tools.ietf.org/html/rfc7009#section-2.1 + if (!$revoked && $tokenTypeHint != 'refresh_token') { + if ($this->refreshStorage) { + $revoked = $this->refreshStorage->unsetRefreshToken($token); + } + } + + return $revoked; + } } diff --git a/OAuth2/ResponseType/AccessTokenInterface.php b/OAuth2/ResponseType/AccessTokenInterface.php index 2ba4a4f..0e576df 100644 --- a/OAuth2/ResponseType/AccessTokenInterface.php +++ b/OAuth2/ResponseType/AccessTokenInterface.php @@ -3,7 +3,6 @@ namespace OAuth2\ResponseType; /** - * * @author Brent Shaffer */ interface AccessTokenInterface extends ResponseTypeInterface @@ -11,13 +10,24 @@ interface AccessTokenInterface extends ResponseTypeInterface /** * Handle the creation of access token, also issue refresh token if supported / desirable. * - * @param $client_id client identifier related to the access token. - * @param $user_id user ID associated with the access token - * @param $scope OPTONAL scopes to be stored in space-separated string. - * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * @param mixed $client_id - client identifier related to the access token. + * @param mixed $user_id - user ID associated with the access token + * @param string $scope - OPTONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - if true, a new refresh_token will be added to the response * * @see http://tools.ietf.org/html/rfc6749#section-5 * @ingroup oauth2_section_5 */ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); -} + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * + * @param $token + * @param $tokenTypeHint + * @return mixed + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function revokeToken($token, $tokenTypeHint); +} \ No newline at end of file diff --git a/OAuth2/ResponseType/AuthorizationCode.php b/OAuth2/ResponseType/AuthorizationCode.php index 6a305fd..b92c73c 100644 --- a/OAuth2/ResponseType/AuthorizationCode.php +++ b/OAuth2/ResponseType/AuthorizationCode.php @@ -5,7 +5,6 @@ use OAuth2\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface; /** - * * @author Brent Shaffer */ class AuthorizationCode implements AuthorizationCodeInterface @@ -85,10 +84,12 @@ public function enforceRedirect() protected function generateAuthorizationCode() { $tokenLen = 40; - if (function_exists('mcrypt_create_iv')) { - $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); + if (function_exists('random_bytes')) { + $randomData = random_bytes(100); } elseif (function_exists('openssl_random_pseudo_bytes')) { $randomData = openssl_random_pseudo_bytes(100); + } elseif (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); } else { diff --git a/OAuth2/ResponseType/AuthorizationCodeInterface.php b/OAuth2/ResponseType/AuthorizationCodeInterface.php index df777e2..4f0a29d 100644 --- a/OAuth2/ResponseType/AuthorizationCodeInterface.php +++ b/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -3,7 +3,6 @@ namespace OAuth2\ResponseType; /** - * * @author Brent Shaffer */ interface AuthorizationCodeInterface extends ResponseTypeInterface @@ -17,11 +16,12 @@ public function enforceRedirect(); /** * Handle the creation of the authorization code. * - * @param $client_id client identifier related to the authorization code - * @param $user_id user id associated with the authorization code - * @param $redirect_uri an absolute URI to which the authorization server will redirect the - * user-agent to when the end-user authorization step is completed. - * @param $scope OPTIONAL scopes to be stored in space-separated string. + * @param mixed $client_id - Client identifier related to the authorization code + * @param mixed $user_id - User ID associated with the authorization code + * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. + * @return string * * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 diff --git a/OAuth2/ResponseType/CryptoToken.php b/OAuth2/ResponseType/JwtAccessToken.php similarity index 53% rename from OAuth2/ResponseType/CryptoToken.php rename to OAuth2/ResponseType/JwtAccessToken.php index 08ff853..0ee3708 100644 --- a/OAuth2/ResponseType/CryptoToken.php +++ b/OAuth2/ResponseType/JwtAccessToken.php @@ -10,25 +10,28 @@ use OAuth2\Storage\Memory; /** - * * @author Brent Shaffer */ -class CryptoToken extends AccessToken +class JwtAccessToken extends AccessToken { protected $publicKeyStorage; protected $encryptionUtil; /** - * @param $config - * - store_encrypted_token_string (bool true) - * whether the entire encrypted string is stored, - * or just the token ID is stored + * @param PublicKeyInterface $publicKeyStorage - + * @param AccessTokenStorageInterface $tokenStorage - + * @param RefreshTokenInterface $refreshStorage - + * @param array $config - array with key store_encrypted_token_string (bool true) + * whether the entire encrypted string is stored, + * or just the token ID is stored + * @param EncryptionInterface $encryptionUtil - */ public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) { $this->publicKeyStorage = $publicKeyStorage; $config = array_merge(array( 'store_encrypted_token_string' => true, + 'issuer' => '' ), $config); if (is_null($tokenStorage)) { // a pass-thru, so we can call the parent constructor @@ -44,42 +47,31 @@ public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessT /** * Handle the creation of access token, also issue refresh token if supported / desirable. * - * @param $client_id - * Client identifier related to the access token. - * @param $user_id - * User ID associated with the access token - * @param $scope - * (optional) Scopes to be stored in space-separated string. - * @param bool $includeRefreshToken - * If true, a new refresh_token will be added to the response + * @param mixed $client_id - Client identifier related to the access token. + * @param mixed $user_id - User ID associated with the access token + * @param string $scope - (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken - If true, a new refresh_token will be added to the response + * @return array - The access token * * @see http://tools.ietf.org/html/rfc6749#section-5 * @ingroup oauth2_section_5 */ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) { - // token to encrypt - $expires = time() + $this->config['access_lifetime']; - $cryptoToken = array( - 'id' => $this->generateAccessToken(), - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'token_type' => $this->config['token_type'], - 'scope' => $scope - ); + // payload to encrypt + $payload = $this->createPayload($client_id, $user_id, $scope); /* - * Encode the token data into a single access_token string + * Encode the payload data into a single JWT access_token string */ - $access_token = $this->encodeToken($cryptoToken, $client_id); + $access_token = $this->encodeToken($payload, $client_id); /* * Save the token to a secondary storage. This is implemented on the - * OAuth2\Storage\CryptoToken side, and will not actually store anything, + * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, * if no secondary storage has been supplied */ - $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $cryptoToken['id']; + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $payload['id']; $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); // token to return to the client @@ -109,6 +101,11 @@ public function createAccessToken($client_id, $user_id, $scope = null, $includeR return $token; } + /** + * @param array $token + * @param mixed $client_id + * @return mixed + */ protected function encodeToken(array $token, $client_id = null) { $private_key = $this->publicKeyStorage->getPrivateKey($client_id); @@ -116,4 +113,47 @@ protected function encodeToken(array $token, $client_id = null) return $this->encryptionUtil->encode($token, $private_key, $algorithm); } + + /** + * This function can be used to create custom JWT payloads + * + * @param mixed $client_id - Client identifier related to the access token. + * @param mixed $user_id - User ID associated with the access token + * @param string $scope - (optional) Scopes to be stored in space-separated string. + * @return array - The access token + */ + protected function createPayload($client_id, $user_id, $scope = null) + { + // token to encrypt + $expires = time() + $this->config['access_lifetime']; + $id = $this->generateAccessToken(); + + $payload = array( + 'id' => $id, // for BC (see #591) + 'jti' => $id, + 'iss' => $this->config['issuer'], + 'aud' => $client_id, + 'sub' => $user_id, + 'exp' => $expires, + 'iat' => time(), + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + if (isset($this->config['jwt_extra_payload_callable'])) { + if (!is_callable($this->config['jwt_extra_payload_callable'])) { + throw new \InvalidArgumentException('jwt_extra_payload_callable is not callable'); + } + + $extra = call_user_func($this->config['jwt_extra_payload_callable'], $client_id, $user_id, $scope); + + if (!is_array($extra)) { + throw new \InvalidArgumentException('jwt_extra_payload_callable must return array'); + } + + $payload = array_merge($extra, $payload); + } + + return $payload; + } } diff --git a/OAuth2/ResponseType/ResponseTypeInterface.php b/OAuth2/ResponseType/ResponseTypeInterface.php index f8e26a5..a271565 100644 --- a/OAuth2/ResponseType/ResponseTypeInterface.php +++ b/OAuth2/ResponseType/ResponseTypeInterface.php @@ -4,5 +4,10 @@ interface ResponseTypeInterface { + /** + * @param array $params + * @param mixed $user_id + * @return mixed + */ public function getAuthorizeResponse($params, $user_id = null); } diff --git a/OAuth2/Scope.php b/OAuth2/Scope.php index c44350b..3ba6e53 100644 --- a/OAuth2/Scope.php +++ b/OAuth2/Scope.php @@ -2,19 +2,23 @@ namespace OAuth2; +use InvalidArgumentException; use OAuth2\Storage\Memory; use OAuth2\Storage\ScopeInterface as ScopeStorageInterface; /** -* @see OAuth2\ScopeInterface +* @see ScopeInterface */ class Scope implements ScopeInterface { protected $storage; /** - * @param mixed @storage - * Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface + * Constructor + * + * @param mixed $storage - Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface + * + * @throws InvalidArgumentException */ public function __construct($storage = null) { @@ -23,7 +27,7 @@ public function __construct($storage = null) } if (!$storage instanceof ScopeStorageInterface) { - throw new \InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface"); + throw new InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface"); } $this->storage = $storage; @@ -32,12 +36,10 @@ public function __construct($storage = null) /** * Check if everything in required scope is contained in available scope. * - * @param $required_scope - * A space-separated string of scopes. - * - * @return - * TRUE if everything in required scope is contained in available scope, - * and FALSE if it isn't. + * @param string $required_scope - A space-separated string of scopes. + * @param string $available_scope - A space-separated string of scopes. + * @return bool - TRUE if everything in required scope is contained in available scope and FALSE + * if it isn't. * * @see http://tools.ietf.org/html/rfc6749#section-7 * @@ -54,11 +56,8 @@ public function checkScope($required_scope, $available_scope) /** * Check if the provided scope exists in storage. * - * @param $scope - * A space-separated string of scopes. - * - * @return - * TRUE if it exists, FALSE otherwise. + * @param string $scope - A space-separated string of scopes. + * @return bool - TRUE if it exists, FALSE otherwise. */ public function scopeExists($scope) { @@ -76,12 +75,20 @@ public function scopeExists($scope) } } + /** + * @param RequestInterface $request + * @return string + */ public function getScopeFromRequest(RequestInterface $request) { // "scope" is valid if passed in either POST or QUERY return $request->request('scope', $request->query('scope')); } + /** + * @param null $client_id + * @return mixed + */ public function getDefaultScope($client_id = null) { return $this->storage->getDefaultScope($client_id); @@ -93,8 +100,7 @@ public function getDefaultScope($client_id = null) * In case OpenID Connect is used, these scopes must include: * 'openid', offline_access'. * - * @return - * An array of reserved scopes. + * @return array - An array of reserved scopes. */ public function getReservedScopes() { diff --git a/OAuth2/ScopeInterface.php b/OAuth2/ScopeInterface.php index 5b60f9a..f65cfa7 100644 --- a/OAuth2/ScopeInterface.php +++ b/OAuth2/ScopeInterface.php @@ -7,19 +7,17 @@ /** * Class to handle scope implementation logic * - * @see OAuth2\Storage\ScopeInterface + * @see \OAuth2\Storage\ScopeInterface */ interface ScopeInterface extends ScopeStorageInterface { /** * Check if everything in required scope is contained in available scope. * - * @param $required_scope - * A space-separated string of scopes. - * - * @return - * TRUE if everything in required scope is contained in available scope, - * and FALSE if it isn't. + * @param string $required_scope - A space-separated string of scopes. + * @param string $available_scope - A space-separated string of scopes. + * @return boolean - TRUE if everything in required scope is contained in available scope and FALSE + * if it isn't. * * @see http://tools.ietf.org/html/rfc6749#section-7 * @@ -30,11 +28,8 @@ public function checkScope($required_scope, $available_scope); /** * Return scope info from request * - * @param OAuth2\RequestInterface - * Request object to check - * - * @return - * string representation of requested scope + * @param RequestInterface $request - Request object to check + * @return string - representation of requested scope */ public function getScopeFromRequest(RequestInterface $request); } diff --git a/OAuth2/Server.php b/OAuth2/Server.php index 3434a7b..cf040c2 100644 --- a/OAuth2/Server.php +++ b/OAuth2/Server.php @@ -19,9 +19,10 @@ use OAuth2\ResponseType\ResponseTypeInterface; use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType; use OAuth2\ResponseType\AccessToken; -use OAuth2\ResponseType\CryptoToken; +use OAuth2\ResponseType\JwtAccessToken; +use OAuth2\OpenID\ResponseType\CodeIdToken; use OAuth2\OpenID\ResponseType\IdToken; -use OAuth2\OpenID\ResponseType\TokenIdToken; +use OAuth2\OpenID\ResponseType\IdTokenToken; use OAuth2\TokenType\TokenTypeInterface; use OAuth2\TokenType\Bearer; use OAuth2\GrantType\GrantTypeInterface; @@ -29,40 +30,89 @@ use OAuth2\GrantType\ClientCredentials; use OAuth2\GrantType\RefreshToken; use OAuth2\GrantType\AuthorizationCode; -use OAuth2\Storage\CryptoToken as CryptoTokenStorage; -use OAuth2\Storage\CryptoTokenInterface; +use OAuth2\Storage\ClientCredentialsInterface; +use OAuth2\Storage\ClientInterface; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; +use OAuth2\Storage\JwtAccessTokenInterface; +use InvalidArgumentException; +use LogicException; /** * Server class for OAuth2 * This class serves as a convience class which wraps the other Controller classes * -* @see OAuth2\Controller\ResourceController -* @see OAuth2\Controller\AuthorizeController -* @see OAuth2\Controller\TokenController +* @see \OAuth2\Controller\ResourceController +* @see \OAuth2\Controller\AuthorizeController +* @see \OAuth2\Controller\TokenController */ class Server implements ResourceControllerInterface, AuthorizeControllerInterface, TokenControllerInterface, UserInfoControllerInterface { - // misc properties + /** + * @var ResponseInterface + */ protected $response; + + /** + * @var array + */ protected $config; + + /** + * @var array + */ protected $storages; - // servers + /** + * @var AuthorizeControllerInterface + */ protected $authorizeController; + + /** + * @var TokenControllerInterface + */ protected $tokenController; + + /** + * @var ResourceControllerInterface + */ protected $resourceController; + + /** + * @var UserInfoControllerInterface + */ protected $userInfoController; - // config classes - protected $grantTypes; - protected $responseTypes; + /** + * @var array + */ + protected $grantTypes = array(); + + /** + * @var array + */ + protected $responseTypes = array(); + + /** + * @var TokenTypeInterface + */ protected $tokenType; + + /** + * @var ScopeInterface + */ protected $scopeUtil; + + /** + * @var ClientAssertionTypeInterface + */ protected $clientAssertionType; + /** + * @var array + */ protected $storageMap = array( 'access_token' => 'OAuth2\Storage\AccessTokenInterface', 'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface', @@ -75,23 +125,28 @@ class Server implements ResourceControllerInterface, 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', 'scope' => 'OAuth2\Storage\ScopeInterface', ); + + /** + * @var array + */ protected $responseTypeMap = array( 'token' => 'OAuth2\ResponseType\AccessTokenInterface', 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', - 'token id_token' => 'OAuth2\OpenID\ResponseType\TokenIdTokenInterface', + 'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface', + 'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface', ); /** - * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the - * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) - * @param array $config specify a different token lifetime, token header name, etc - * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens - * @param array $responseTypes Response types to use. array keys should be "code" and and "token" for - * Access Token and Authorization Code response types - * @param OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" - * @param OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope - * @param OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic + * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the + * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) + * @param array $config specify a different token lifetime, token header name, etc + * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens + * @param array $responseTypes Response types to use. array keys should be "code" and "token" for + * Access Token and Authorization Code response types + * @param TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" + * @param ScopeInterface $scopeUtil The scope utility class to use to validate scope + * @param ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic * * @ingroup oauth2_section_7 */ @@ -105,7 +160,8 @@ public function __construct($storage = array(), array $config = array(), array $ // merge all config values. These get passed to our controller objects $this->config = array_merge(array( - 'use_crypto_tokens' => false, + 'use_jwt_access_tokens' => false, + 'jwt_extra_payload_callable' => null, 'store_encrypted_token_string' => true, 'use_openid_connect' => false, 'id_lifetime' => 3600, @@ -119,19 +175,29 @@ public function __construct($storage = array(), array $config = array(), array $ 'allow_credentials_in_request_body' => true, 'allow_public_clients' => true, 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true, ), $config); foreach ($grantTypes as $key => $grantType) { $this->addGrantType($grantType, $key); } + foreach ($responseTypes as $key => $responseType) { $this->addResponseType($responseType, $key); } + $this->tokenType = $tokenType; $this->scopeUtil = $scopeUtil; $this->clientAssertionType = $clientAssertionType; + + if ($this->config['use_openid_connect']) { + $this->validateOpenIdConnect(); + } } + /** + * @return AuthorizeControllerInterface + */ public function getAuthorizeController() { if (is_null($this->authorizeController)) { @@ -141,6 +207,9 @@ public function getAuthorizeController() return $this->authorizeController; } + /** + * @return TokenController + */ public function getTokenController() { if (is_null($this->tokenController)) { @@ -150,6 +219,9 @@ public function getTokenController() return $this->tokenController; } + /** + * @return ResourceControllerInterface + */ public function getResourceController() { if (is_null($this->resourceController)) { @@ -159,6 +231,9 @@ public function getResourceController() return $this->resourceController; } + /** + * @return UserInfoControllerInterface + */ public function getUserInfoController() { if (is_null($this->userInfoController)) { @@ -169,7 +244,7 @@ public function getUserInfoController() } /** - * every getter deserves a setter + * @param AuthorizeControllerInterface $authorizeController */ public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) { @@ -177,7 +252,7 @@ public function setAuthorizeController(AuthorizeControllerInterface $authorizeCo } /** - * every getter deserves a setter + * @param TokenControllerInterface $tokenController */ public function setTokenController(TokenControllerInterface $tokenController) { @@ -185,7 +260,7 @@ public function setTokenController(TokenControllerInterface $tokenController) } /** - * every getter deserves a setter + * @param ResourceControllerInterface $resourceController */ public function setResourceController(ResourceControllerInterface $resourceController) { @@ -193,7 +268,7 @@ public function setResourceController(ResourceControllerInterface $resourceContr } /** - * every getter deserves a setter + * @param UserInfoControllerInterface $userInfoController */ public function setUserInfoController(UserInfoControllerInterface $userInfoController) { @@ -204,14 +279,12 @@ public function setUserInfoController(UserInfoControllerInterface $userInfoContr * Return claims about the authenticated end-user. * This would be called from the "/UserInfo" endpoint as defined in the spec. * - * @param $request - OAuth2\RequestInterface - * Request object to grant access token + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object containing error messages (failure) or user claims (success) + * @return ResponseInterface * - * @param $response - OAuth2\ResponseInterface - * Response object containing error messages (failure) or user claims (success) - * - * @throws InvalidArgumentException - * @throws LogicException + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo */ @@ -228,14 +301,12 @@ public function handleUserInfoRequest(RequestInterface $request, ResponseInterfa * This would be called from the "/token" endpoint as defined in the spec. * Obviously, you can call your endpoint whatever you want. * - * @param $request - OAuth2\RequestInterface - * Request object to grant access token + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object containing error messages (failure) or access token (success) + * @return ResponseInterface * - * @param $response - OAuth2\ResponseInterface - * Response object containing error messages (failure) or access token (success) - * - * @throws InvalidArgumentException - * @throws LogicException + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-10.6 @@ -251,6 +322,11 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface return $this->response; } + /** + * @param RequestInterface $request - Request object to grant access token + * @param ResponseInterface $response - Response object + * @return mixed + */ public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) { $this->response = is_null($response) ? new Response() : $response; @@ -259,6 +335,24 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r return $value; } + /** + * Handle a revoke token request + * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec + * + * @see https://tools.ietf.org/html/rfc7009#section-2 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return Response|ResponseInterface + */ + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleRevokeRequest($request, $this->response); + + return $this->response; + } + /** * Redirect the user appropriately after approval. * @@ -266,22 +360,18 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r * authorization server should call this function to redirect the user * appropriately. * - * @param $request - * The request should have the follow parameters set in the querystring: - * - response_type: The requested response: an access token, an - * authorization code, or both. + * @param RequestInterface $request - The request should have the follow parameters set in the querystring: + * - response_type: The requested response: an access token, an authorization code, or both. * - client_id: The client identifier as described in Section 2. - * - redirect_uri: An absolute URI to which the authorization server - * will redirect the user-agent to when the end-user authorization - * step is completed. - * - scope: (optional) The scope of the resource request expressed as a - * list of space-delimited strings. - * - state: (optional) An opaque value used by the client to maintain - * state between the request and callback. - * @param $is_authorized - * TRUE or FALSE depending on whether the user authorized the access. - * @param $user_id - * Identifier of user who authorized the client + * - redirect_uri: An absolute URI to which the authorization server will redirect the user-agent to when the + * end-user authorization step is completed. + * - scope: (optional) The scope of the resource request expressed as a list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain state between the request and callback. + * + * @param ResponseInterface $response - Response object + * @param bool $is_authorized - TRUE or FALSE depending on whether the user authorized the access. + * @param mixed $user_id - Identifier of user who authorized the client + * @return ResponseInterface * * @see http://tools.ietf.org/html/rfc6749#section-4 * @@ -298,14 +388,17 @@ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterf /** * Pull the authorization request data out of the HTTP request. * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it - * by setting $config['enforce_redirect'] to true. + * by setting $config['enforce_redirect'] to true. * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that - * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. + * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. * * The draft specifies that the parameters should be retrieved from GET, override the Response * object to change this * - * @return + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return bool + * * The authorization parameters so the authorization server can prompt * the user for approval if valid. * @@ -322,6 +415,12 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte return $value; } + /** + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @param string $scope - Scope + * @return mixed + */ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) { $this->response = is_null($response) ? new Response() : $response; @@ -330,6 +429,11 @@ public function verifyResourceRequest(RequestInterface $request, ResponseInterfa return $value; } + /** + * @param RequestInterface $request - Request object + * @param ResponseInterface $response - Response object + * @return mixed + */ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) { $this->response = is_null($response) ? new Response() : $response; @@ -338,28 +442,31 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface return $value; } - public function addGrantType(GrantTypeInterface $grantType, $key = null) + /** + * @param GrantTypeInterface $grantType + * @param mixed $identifier + */ + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { - if (is_string($key)) { - $this->grantTypes[$key] = $grantType; - } else { - $this->grantTypes[$grantType->getQuerystringIdentifier()] = $grantType; + if (!is_string($identifier)) { + $identifier = $grantType->getQueryStringIdentifier(); } + $this->grantTypes[$identifier] = $grantType; + // persist added grant type down to TokenController if (!is_null($this->tokenController)) { - $this->getTokenController()->addGrantType($grantType); + $this->getTokenController()->addGrantType($grantType, $identifier); } } /** * Set a storage object for the server * - * @param $storage - * An object implementing one of the Storage interfaces - * @param $key - * If null, the storage is set to the key of each storage interface it implements + * @param object $storage - An object implementing one of the Storage interfaces + * @param mixed $key - If null, the storage is set to the key of each storage interface it implements * + * @throws InvalidArgumentException * @see storageMap */ public function addStorage($storage, $key = null) @@ -373,11 +480,11 @@ public function addStorage($storage, $key = null) // special logic to handle "client" and "client_credentials" strangeness if ($key === 'client' && !isset($this->storages['client_credentials'])) { - if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) { + if ($storage instanceof ClientCredentialsInterface) { $this->storages['client_credentials'] = $storage; } } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) { - if ($storage instanceof \OAuth2\Storage\ClientInterface) { + if ($storage instanceof ClientInterface) { $this->storages['client'] = $storage; } } @@ -398,8 +505,16 @@ public function addStorage($storage, $key = null) } } + /** + * @param ResponseTypeInterface $responseType + * @param mixed $key + * + * @throws InvalidArgumentException + */ public function addResponseType(ResponseTypeInterface $responseType, $key = null) { + $key = $this->normalizeResponseType($key); + if (isset($this->responseTypeMap[$key])) { if (!$responseType instanceof $this->responseTypeMap[$key]) { throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key])); @@ -422,6 +537,9 @@ public function addResponseType(ResponseTypeInterface $responseType, $key = null } } + /** + * @return ScopeInterface + */ public function getScopeUtil() { if (!$this->scopeUtil) { @@ -433,17 +551,21 @@ public function getScopeUtil() } /** - * every getter deserves a setter + * @param ScopeInterface $scopeUtil */ public function setScopeUtil($scopeUtil) { $this->scopeUtil = $scopeUtil; } + /** + * @return AuthorizeControllerInterface + * @throws LogicException + */ protected function createDefaultAuthorizeController() { if (!isset($this->storages['client'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the authorize server"); + throw new \LogicException('You must supply a storage object implementing \OAuth2\Storage\ClientInterface to use the authorize server'); } if (0 == count($this->responseTypes)) { $this->responseTypes = $this->getDefaultResponseTypes(); @@ -451,7 +573,7 @@ protected function createDefaultAuthorizeController() if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); if ($this->config['allow_implicit']) { - $this->responseTypes['token id_token'] = $this->createDefaultTokenIdTokenResponseType(); + $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType(); } } @@ -464,6 +586,10 @@ protected function createDefaultAuthorizeController() return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); } + /** + * @return TokenControllerInterface + * @throws LogicException + */ protected function createDefaultTokenController() { if (0 == count($this->grantTypes)) { @@ -475,7 +601,7 @@ protected function createDefaultTokenController() foreach ($this->grantTypes as $grantType) { if (!$grantType instanceof ClientAssertionTypeInterface) { if (!isset($this->storages['client_credentials'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server'); } $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); @@ -485,7 +611,7 @@ protected function createDefaultTokenController() } if (!isset($this->storages['client'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); + throw new LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); } $accessTokenResponseType = $this->getAccessTokenResponseType(); @@ -493,15 +619,19 @@ protected function createDefaultTokenController() return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); } + /** + * @return ResourceControllerInterface + * @throws LogicException + */ protected function createDefaultResourceController() { - if ($this->config['use_crypto_tokens']) { - // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set - if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { - $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); } } elseif (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the resource server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server'); } if (!$this->tokenType) { @@ -513,19 +643,23 @@ protected function createDefaultResourceController() return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); } + /** + * @return UserInfoControllerInterface + * @throws LogicException + */ protected function createDefaultUserInfoController() { - if ($this->config['use_crypto_tokens']) { - // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set - if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { - $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); } } elseif (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the UserInfo server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server'); } if (!isset($this->storages['user_claims'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server'); } if (!$this->tokenType) { @@ -537,6 +671,9 @@ protected function createDefaultUserInfoController() return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil()); } + /** + * @return Bearer + */ protected function getDefaultTokenType() { $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); @@ -544,6 +681,10 @@ protected function getDefaultTokenType() return new Bearer($config); } + /** + * @return array + * @throws LogicException + */ protected function getDefaultResponseTypes() { $responseTypes = array(); @@ -555,7 +696,7 @@ protected function getDefaultResponseTypes() if ($this->config['use_openid_connect']) { $responseTypes['id_token'] = $this->getIdTokenResponseType(); if ($this->config['allow_implicit']) { - $responseTypes['token id_token'] = $this->getTokenIdTokenResponseType(); + $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType(); } } @@ -563,21 +704,26 @@ protected function getDefaultResponseTypes() $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); if ($this->config['use_openid_connect']) { if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { - throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); } $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); + $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']); } else { $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); } } if (count($responseTypes) == 0) { - throw new \LogicException("You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set 'allow_implicit' to true and implement a OAuth2\Storage\AccessTokenInterface storage object"); + throw new \LogicException('You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set "allow_implicit" to true and implement a OAuth2\Storage\AccessTokenInterface storage object'); } return $responseTypes; } + /** + * @return array + * @throws LogicException + */ protected function getDefaultGrantTypes() { $grantTypes = array(); @@ -592,14 +738,14 @@ protected function getDefaultGrantTypes() } if (isset($this->storages['refresh_token'])) { - $config = array_intersect_key($this->config, array('always_issue_new_refresh_token' => '')); + $config = array_intersect_key($this->config, array_flip(explode(' ', 'always_issue_new_refresh_token unset_refresh_token_after_use'))); $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config); } if (isset($this->storages['authorization_code'])) { if ($this->config['use_openid_connect']) { if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { - throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); } $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); } else { @@ -608,25 +754,31 @@ protected function getDefaultGrantTypes() } if (count($grantTypes) == 0) { - throw new \LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); + throw new \LogicException('Unable to build default grant types - You must supply an array of grant_types in the constructor'); } return $grantTypes; } + /** + * @return AccessToken + */ protected function getAccessTokenResponseType() { if (isset($this->responseTypes['token'])) { return $this->responseTypes['token']; } - if ($this->config['use_crypto_tokens']) { - return $this->createDefaultCryptoTokenResponseType(); + if ($this->config['use_jwt_access_tokens']) { + return $this->createDefaultJwtAccessTokenResponseType(); } return $this->createDefaultAccessTokenResponseType(); } + /** + * @return IdToken + */ protected function getIdTokenResponseType() { if (isset($this->responseTypes['id_token'])) { @@ -636,38 +788,47 @@ protected function getIdTokenResponseType() return $this->createDefaultIdTokenResponseType(); } - protected function getTokenIdTokenResponseType() + /** + * @return IdTokenToken + */ + protected function getIdTokenTokenResponseType() { - if (isset($this->responseTypes['token id_token'])) { - return $this->responseTypes['token id_token']; + if (isset($this->responseTypes['id_token token'])) { + return $this->responseTypes['id_token token']; } - return $this->createDefaultTokenIdTokenResponseType(); + return $this->createDefaultIdTokenTokenResponseType(); } /** * For Resource Controller + * + * @return JwtAccessTokenStorage + * @throws LogicException */ - protected function createDefaultCryptoTokenStorage() + protected function createDefaultJwtAccessTokenStorage() { if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); } $tokenStorage = null; if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { $tokenStorage = $this->storages['access_token']; } // wrap the access token storage as required. - return new CryptoTokenStorage($this->storages['public_key'], $tokenStorage); + return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage); } /** * For Authorize and Token Controllers + * + * @return JwtAccessToken + * @throws LogicException */ - protected function createDefaultCryptoTokenResponseType() + protected function createDefaultJwtAccessTokenResponseType() { if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); } $tokenStorage = null; @@ -680,15 +841,19 @@ protected function createDefaultCryptoTokenResponseType() $refreshStorage = $this->storages['refresh_token']; } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string'))); + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime jwt_extra_payload_callable'))); - return new CryptoToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); } + /** + * @return AccessToken + * @throws LogicException + */ protected function createDefaultAccessTokenResponseType() { if (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); + throw new LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); } $refreshStorage = null; @@ -702,13 +867,17 @@ protected function createDefaultAccessTokenResponseType() return new AccessToken($this->storages['access_token'], $refreshStorage, $config); } + /** + * @return IdToken + * @throws LogicException + */ protected function createDefaultIdTokenResponseType() { if (!isset($this->storages['user_claims'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); + throw new LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); } if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); + throw new LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); } $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); @@ -716,61 +885,133 @@ protected function createDefaultIdTokenResponseType() return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); } - protected function createDefaultTokenIdTokenResponseType() + /** + * @return IdTokenToken + */ + protected function createDefaultIdTokenTokenResponseType() + { + return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + } + + /** + * @throws InvalidArgumentException + */ + protected function validateOpenIdConnect() + { + $authCodeGrant = $this->getGrantType('authorization_code'); + if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) { + throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.'); + } + } + + /** + * @param string $name + * @return string + */ + protected function normalizeResponseType($name) { - return new TokenIdToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + // for multiple-valued response types - make them alphabetical + if (!empty($name) && false !== strpos($name, ' ')) { + $types = explode(' ', $name); + sort($types); + $name = implode(' ', $types); + } + + return $name; } + /** + * @return mixed + */ public function getResponse() { return $this->response; } + /** + * @return array + */ public function getStorages() { return $this->storages; } + /** + * @param string $name + * @return object|null + */ public function getStorage($name) { return isset($this->storages[$name]) ? $this->storages[$name] : null; } + /** + * @return array + */ public function getGrantTypes() { return $this->grantTypes; } + /** + * @param string $name + * @return object|null + */ public function getGrantType($name) { return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null; } + /** + * @return array + */ public function getResponseTypes() { return $this->responseTypes; } + /** + * @param string $name + * @return object|null + */ public function getResponseType($name) { + // for multiple-valued response types - make them alphabetical + $name = $this->normalizeResponseType($name); + return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; } + /** + * @return TokenTypeInterface + */ public function getTokenType() { return $this->tokenType; } + /** + * @return ClientAssertionTypeInterface + */ public function getClientAssertionType() { return $this->clientAssertionType; } + /** + * @param string $name + * @param mixed $value + */ public function setConfig($name, $value) { $this->config[$name] = $value; } + /** + * @param string $name + * @param mixed $default + * @return mixed + */ public function getConfig($name, $default = null) { return isset($this->config[$name]) ? $this->config[$name] : $default; diff --git a/OAuth2/Storage/AccessTokenInterface.php b/OAuth2/Storage/AccessTokenInterface.php index d382f89..22428f2 100644 --- a/OAuth2/Storage/AccessTokenInterface.php +++ b/OAuth2/Storage/AccessTokenInterface.php @@ -15,17 +15,18 @@ interface AccessTokenInterface * * We need to retrieve access token data as we create and verify tokens. * - * @param $oauth_token - * oauth_token to be check with. - * - * @return - * An associative array as below, and return NULL if the supplied oauth_token - * is invalid: - * - expires: Stored expiration in unix timestamp. - * - client_id: (optional) Stored client identifier. - * - user_id: (optional) Stored user identifier. - * - scope: (optional) Stored scope values in space-separated string. - * - id_token: (optional) Stored id_token (if "use_openid_connect" is true). + * @param string $oauth_token - oauth_token to be check with. + * + * @return array|null - An associative array as below, and return NULL if the supplied oauth_token is invalid: + * @code + * array( + * 'expires' => $expires, // Stored expiration in unix timestamp. + * 'client_id' => $client_id, // (optional) Stored client identifier. + * 'user_id' => $user_id, // (optional) Stored user identifier. + * 'scope' => $scope, // (optional) Stored scope values in space-separated string. + * 'id_token' => $id_token // (optional) Stored id_token (if "use_openid_connect" is true). + * ); + * @endcode * * @ingroup oauth2_section_7 */ @@ -36,13 +37,29 @@ public function getAccessToken($oauth_token); * * We need to store access token data as we create and verify tokens. * - * @param $oauth_token oauth_token to be stored. - * @param $client_id client identifier to be stored. - * @param $user_id user identifier to be stored. - * @param int $expires expiration to be stored as a Unix timestamp. - * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * @param string $oauth_token - oauth_token to be stored. + * @param mixed $client_id - client identifier to be stored. + * @param mixed $user_id - user identifier to be stored. + * @param int $expires - expiration to be stored as a Unix timestamp. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. * * @ingroup oauth2_section_4 */ public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); -} + + /** + * Expire an access token. + * + * This is not explicitly required in the spec, but if defined in a draft RFC for token + * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009 + * + * @param $access_token + * Access token to be expired. + * + * @return BOOL true if an access token was unset, false if not + * @ingroup oauth2_section_6 + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function unsetAccessToken($access_token); +} \ No newline at end of file diff --git a/OAuth2/Storage/AuthorizationCodeInterface.php b/OAuth2/Storage/AuthorizationCodeInterface.php index 3beb0e4..2dbc138 100644 --- a/OAuth2/Storage/AuthorizationCodeInterface.php +++ b/OAuth2/Storage/AuthorizationCodeInterface.php @@ -59,19 +59,19 @@ public function getAuthorizationCode($code); * * Required for OAuth2::GRANT_TYPE_AUTH_CODE. * - * @param string $code Authorization code to be stored. - * @param mixed $client_id Client identifier to be stored. - * @param mixed $user_id User identifier to be stored. - * @param string $redirect_uri Redirect URI(s) to be stored in a space-separated string. - * @param int $expires Expiration to be stored as a Unix timestamp. - * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * @param string $code - Authorization code to be stored. + * @param mixed $client_id - Client identifier to be stored. + * @param mixed $user_id - User identifier to be stored. + * @param string $redirect_uri - Redirect URI(s) to be stored in a space-separated string. + * @param int $expires - Expiration to be stored as a Unix timestamp. + * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. * * @ingroup oauth2_section_4 */ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null); /** - * once an Authorization Code is used, it must be exipired + * once an Authorization Code is used, it must be expired * * @see http://tools.ietf.org/html/rfc6749#section-4.1.2 * diff --git a/OAuth2/Storage/Cassandra.php b/OAuth2/Storage/Cassandra.php index 9587b4c..e60e9d3 100644 --- a/OAuth2/Storage/Cassandra.php +++ b/OAuth2/Storage/Cassandra.php @@ -5,25 +5,27 @@ use phpcassa\ColumnFamily; use phpcassa\ColumnSlice; use phpcassa\Connection\ConnectionPool; +use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use InvalidArgumentException; /** * Cassandra storage for all storage types * - * To use, install "thobbs/phpcassa" via composer + * To use, install "thobbs/phpcassa" via composer: * - * composer require thobbs/phpcassa:dev-master + * composer require thobbs/phpcassa:dev-master * * - * Once this is done, instantiate the + * Once this is done, instantiate the connection: * - * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); * * * Then, register the storage client: * - * $storage = new OAuth2\Storage\Cassandra($cassandra); - * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); * * * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage @@ -35,22 +37,30 @@ class Cassandra implements AuthorizationCodeInterface, RefreshTokenInterface, JwtBearerInterface, ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, OpenIDAuthorizationCodeInterface { private $cache; - /* The cassandra client */ + /** + * @var ConnectionPool + */ protected $cassandra; - /* Configuration array */ + /** + * @var array + */ protected $config; /** * Cassandra Storage! uses phpCassa * - * @param \phpcassa\ConnectionPool $cassandra - * @param array $config + * @param ConnectionPool|array $connection + * @param array $config + * + * @throws InvalidArgumentException */ public function __construct($connection = array(), array $config = array()) { @@ -58,7 +68,7 @@ public function __construct($connection = array(), array $config = array()) $this->cassandra = $connection; } else { if (!is_array($connection)) { - throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + throw new InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array'); } $connection = array_merge(array( 'keyspace' => 'oauth2', @@ -80,19 +90,24 @@ public function __construct($connection = array(), array $config = array()) 'user_key' => 'oauth_users:', 'jwt_key' => 'oauth_jwt:', 'scope_key' => 'oauth_scopes:', + 'public_key_key' => 'oauth_public_keys:', ), $config); } + /** + * @param $key + * @return bool|mixed + */ protected function getValue($key) { if (isset($this->cache[$key])) { return $this->cache[$key]; } - $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); try { - $value = array_shift($cf->get($key, new ColumnSlice("", ""))); + $value = $cf->get($key, new ColumnSlice("", "")); + $value = array_shift($value); } catch (\cassandra\NotFoundException $e) { return false; } @@ -100,6 +115,12 @@ protected function getValue($key) return json_decode($value, true); } + /** + * @param $key + * @param $value + * @param int $expire + * @return bool + */ protected function setValue($key, $value, $expire = 0) { $this->cache[$key] = $value; @@ -127,27 +148,49 @@ protected function setValue($key, $value, $expire = 0) return true; } + /** + * @param $key + * @return bool + */ protected function expireValue($key) { unset($this->cache[$key]); $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); - try { - // __data key set as C* requires a field - $cf->remove($key, array('__data')); - } catch (\Exception $e) { - return false; + + if ($cf->get_count($key) > 0) { + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch (\Exception $e) { + return false; + } + + return true; } - return true; + return false; } - /* AuthorizationCodeInterface */ + /** + * @param string $code + * @return bool|mixed + */ public function getAuthorizationCode($code) { return $this->getValue($this->config['code_key'] . $code); } + /** + * @param string $authorization_code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param int $expires + * @param string $scope + * @param string $id_token + * @return bool + */ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { return $this->setValue( @@ -157,6 +200,10 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, ); } + /** + * @param string $code + * @return bool + */ public function expireAuthorizationCode($code) { $key = $this->config['code_key'] . $code; @@ -165,19 +212,51 @@ public function expireAuthorizationCode($code) return $this->expireValue($key); } - /* UserCredentialsInterface */ + /** + * @param string $username + * @param string $password + * @return bool + */ public function checkUserCredentials($username, $password) { - $user = $this->getUserDetails($username); + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } - return $user && $user['password'] === $password; + return false; } + /** + * plaintext passwords are bad! Override this for your application + * + * @param array $user + * @param string $password + * @return bool + */ + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + /** + * @param string $username + * @return array|bool|false + */ public function getUserDetails($username) { return $this->getUser($username); } + /** + * @param string $username + * @return array|bool + */ public function getUser($username) { if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { @@ -190,15 +269,28 @@ public function getUser($username) ), $userInfo); } + /** + * @param string $username + * @param string $password + * @param string $first_name + * @param string $last_name + * @return bool + */ public function setUser($username, $password, $first_name = null, $last_name = null) { + $password = $this->hashPassword($password); + return $this->setValue( $this->config['user_key'] . $username, compact('username', 'password', 'first_name', 'last_name') ); } - /* ClientCredentialsInterface */ + /** + * @param mixed $client_id + * @param string $client_secret + * @return bool + */ public function checkClientCredentials($client_id, $client_secret = null) { if (!$client = $this->getClientDetails($client_id)) { @@ -209,21 +301,37 @@ public function checkClientCredentials($client_id, $client_secret = null) && $client['client_secret'] == $client_secret; } + /** + * @param $client_id + * @return bool + */ public function isPublicClient($client_id) { if (!$client = $this->getClientDetails($client_id)) { return false; } - return empty($result['client_secret']);; + return empty($client['client_secret']); } - /* ClientInterface */ + /** + * @param $client_id + * @return array|bool|mixed + */ public function getClientDetails($client_id) { return $this->getValue($this->config['client_key'] . $client_id); } + /** + * @param $client_id + * @param null $client_secret + * @param null $redirect_uri + * @param null $grant_types + * @param null $scope + * @param null $user_id + * @return bool + */ public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { return $this->setValue( @@ -232,6 +340,11 @@ public function setClientDetails($client_id, $client_secret = null, $redirect_ur ); } + /** + * @param $client_id + * @param $grant_type + * @return bool + */ public function checkRestrictedGrantType($client_id, $grant_type) { $details = $this->getClientDetails($client_id); @@ -245,12 +358,23 @@ public function checkRestrictedGrantType($client_id, $grant_type) return true; } - /* RefreshTokenInterface */ + /** + * @param $refresh_token + * @return bool|mixed + */ public function getRefreshToken($refresh_token) { return $this->getValue($this->config['refresh_token_key'] . $refresh_token); } + /** + * @param $refresh_token + * @param $client_id + * @param $user_id + * @param $expires + * @param null $scope + * @return bool + */ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { return $this->setValue( @@ -260,17 +384,32 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, ); } + /** + * @param $refresh_token + * @return bool + */ public function unsetRefreshToken($refresh_token) { return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); } - /* AccessTokenInterface */ + /** + * @param string $access_token + * @return array|bool|mixed|null + */ public function getAccessToken($access_token) { return $this->getValue($this->config['access_token_key'].$access_token); } + /** + * @param string $access_token + * @param mixed $client_id + * @param mixed $user_id + * @param int $expires + * @param null $scope + * @return bool + */ public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { return $this->setValue( @@ -280,7 +419,19 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s ); } - /* ScopeInterface */ + /** + * @param $access_token + * @return bool + */ + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + + /** + * @param $scope + * @return bool + */ public function scopeExists($scope) { $scope = explode(' ', $scope); @@ -292,6 +443,10 @@ public function scopeExists($scope) return (count(array_diff($scope, $supportedScope)) == 0); } + /** + * @param null $client_id + * @return bool|mixed + */ public function getDefaultScope($client_id = null) { if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { @@ -301,6 +456,13 @@ public function getDefaultScope($client_id = null) return $result; } + /** + * @param $scope + * @param null $client_id + * @param string $type + * @return bool + * @throws \InvalidArgumentException + */ public function setScope($scope, $client_id = null, $type = 'supported') { if (!in_array($type, array('default', 'supported'))) { @@ -316,7 +478,11 @@ public function setScope($scope, $client_id = null, $type = 'supported') return $this->setValue($key, $scope); } - /*JWTBearerInterface */ + /** + * @param $client_id + * @param $subject + * @return bool|null + */ public function getClientKey($client_id, $subject) { if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { @@ -330,6 +496,12 @@ public function getClientKey($client_id, $subject) return null; } + /** + * @param $client_id + * @param $key + * @param null $subject + * @return bool + */ public function setClientKey($client_id, $key, $subject = null) { return $this->setValue($this->config['jwt_key'] . $client_id, array( @@ -338,7 +510,10 @@ public function setClientKey($client_id, $key, $subject = null) )); } - /*ScopeInterface */ + /** + * @param $client_id + * @return bool|null + */ public function getClientScope($client_id) { if (!$clientDetails = $this->getClientDetails($client_id)) { @@ -352,15 +527,134 @@ public function getClientScope($client_id) return null; } + /** + * @param $client_id + * @param $subject + * @param $audience + * @param $expiration + * @param $jti + * @throws \Exception + */ public function getJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs cassandra implementation. throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); } + /** + * @param $client_id + * @param $subject + * @param $audience + * @param $expiration + * @param $jti + * @throws \Exception + */ public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs cassandra implementation. throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); } -} + + /** + * @param string $client_id + * @return mixed + */ + public function getPublicKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['public_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['public_key']; + } + } + + /** + * @param string $client_id + * @return mixed + */ + public function getPrivateKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['private_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['private_key']; + } + } + + /** + * @param null $client_id + * @return mixed|string + */ + public function getEncryptionAlgorithm($client_id = null) + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + + return 'RS256'; + } + + /** + * @param mixed $user_id + * @param string $claims + * @return array|bool + */ + public function getUserClaims($user_id, $claims) + { + $userDetails = $this->getUserDetails($user_id); + if (!is_array($userDetails)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + /** + * @param $claim + * @param $userDetails + * @return array + */ + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } +} \ No newline at end of file diff --git a/OAuth2/Storage/CouchbaseDB.php b/OAuth2/Storage/CouchbaseDB.php new file mode 100644 index 0000000..9e8148b --- /dev/null +++ b/OAuth2/Storage/CouchbaseDB.php @@ -0,0 +1,331 @@ + + */ +class CouchbaseDB implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \Couchbase) { + $this->db = $connection; + } else { + if (!is_array($connection) || !is_array($connection['servers'])) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); + } + + $this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access couchbase item by type: + protected function getObjectByType($name,$id) + { + return json_decode($this->db->get($this->config[$name].'-'.$id),true); + } + + // Helper function to set couchbase item by type: + protected function setObjectByType($name,$id,$array) + { + $array['type'] = $name; + + return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); + } + + // Helper function to delete couchbase item by type, wait for persist to at least 1 node + protected function deleteObjectByType($name,$id) + { + $this->db->delete($this->config[$name].'-'.$id,"",1); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->getObjectByType('client_table',$client_id)) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->getObjectByType('client_table',$client_id)) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->getObjectByType('client_table',$client_id); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } else { + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->getObjectByType('access_token_table',$access_token); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } else { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } + + return true; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->getObjectByType('code_table',$code); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->setObjectByType('code_table',$code, array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } else { + $this->setObjectByType('code_table',$code,array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->deleteObjectByType('code_table',$code); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->getObjectByType('refresh_token_table',$refresh_token); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->setObjectByType('refresh_token_table',$refresh_token, array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->deleteObjectByType('refresh_token_table',$refresh_token); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->getObjectByType('user_table',$username); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } else { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('getJti() for the Couchbase driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); + } +} \ No newline at end of file diff --git a/OAuth2/Storage/DynamoDB.php b/OAuth2/Storage/DynamoDB.php index fe06428..a54cb37 100644 --- a/OAuth2/Storage/DynamoDB.php +++ b/OAuth2/Storage/DynamoDB.php @@ -124,7 +124,7 @@ public function getClientDetails($client_id) public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['client_table'], @@ -171,7 +171,7 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['access_token_table'], @@ -182,6 +182,17 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s } + public function unsetAccessToken($access_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['access_token_table'], + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)), + 'ReturnValues' => 'ALL_OLD', + )); + + return null !== $result->get('Attributes'); + } + /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { @@ -208,7 +219,7 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['code_table'], @@ -309,7 +320,7 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['refresh_token_table'], @@ -332,7 +343,13 @@ public function unsetRefreshToken($refresh_token) // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { - return $user['password'] == sha1($password); + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); } public function getUser($username) @@ -353,10 +370,10 @@ public function getUser($username) public function setUser($username, $password, $first_name = null, $last_name = null) { // do not store in plaintext - $password = sha1($password); + $password = $this->hashPassword($password); $clientData = compact('username', 'password', 'first_name', 'last_name'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['user_table'], @@ -411,7 +428,7 @@ public function getDefaultScope($client_id = null) $defaultScope[] = $item['scope']['S']; } - return implode(' ', $defaultScope); + return empty($defaultScope) ? null : implode(' ', $defaultScope); } return null; @@ -515,4 +532,9 @@ private function dynamo2array($dynamodbResult) return $result; } -} + + private static function isNotEmpty($value) + { + return null !== $value && '' !== $value; + } +} \ No newline at end of file diff --git a/OAuth2/Storage/CryptoToken.php b/OAuth2/Storage/JwtAccessToken.php similarity index 68% rename from OAuth2/Storage/CryptoToken.php rename to OAuth2/Storage/JwtAccessToken.php index 31ebb65..6ccacd6 100644 --- a/OAuth2/Storage/CryptoToken.php +++ b/OAuth2/Storage/JwtAccessToken.php @@ -6,10 +6,9 @@ use OAuth2\Encryption\Jwt; /** - * * @author Brent Shaffer */ -class CryptoToken implements CryptoTokenInterface +class JwtAccessToken implements JwtAccessTokenInterface { protected $publicKeyStorage; protected $tokenStorage; @@ -39,16 +38,17 @@ public function getAccessToken($oauth_token) return false; } - $client_id = isset($tokenData['client_id']) ? $tokenData['client_id'] : null; + $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; $public_key = $this->publicKeyStorage->getPublicKey($client_id); $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); // now that we have the client_id, verify the token - if (false === $this->encryptionUtil->decode($oauth_token, $public_key, true)) { + if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) { return false; } - return $tokenData; + // normalize the JWT claims to the format expected by other components in this library + return $this->convertJwtToOAuth2($tokenData); } public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) @@ -57,4 +57,31 @@ public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $sc return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); } } -} + + public function unsetAccessToken($access_token) + { + if ($this->tokenStorage) { + return $this->tokenStorage->unsetAccessToken($access_token); + } + } + + + // converts a JWT access token into an OAuth2-friendly format + protected function convertJwtToOAuth2($tokenData) + { + $keyMapping = array( + 'aud' => 'client_id', + 'exp' => 'expires', + 'sub' => 'user_id' + ); + + foreach ($keyMapping as $jwtKey => $oauth2Key) { + if (isset($tokenData[$jwtKey])) { + $tokenData[$oauth2Key] = $tokenData[$jwtKey]; + unset($tokenData[$jwtKey]); + } + } + + return $tokenData; + } +} \ No newline at end of file diff --git a/OAuth2/Storage/CryptoTokenInterface.php b/OAuth2/Storage/JwtAccessTokenInterface.php similarity index 76% rename from OAuth2/Storage/CryptoTokenInterface.php rename to OAuth2/Storage/JwtAccessTokenInterface.php index 867710c..3abb2aa 100644 --- a/OAuth2/Storage/CryptoTokenInterface.php +++ b/OAuth2/Storage/JwtAccessTokenInterface.php @@ -8,7 +8,7 @@ * * @author Brent Shaffer */ -interface CryptoTokenInterface extends AccessTokenInterface +interface JwtAccessTokenInterface extends AccessTokenInterface { } diff --git a/OAuth2/Storage/Memory.php b/OAuth2/Storage/Memory.php index 35e6cba..2c60b71 100644 --- a/OAuth2/Storage/Memory.php +++ b/OAuth2/Storage/Memory.php @@ -142,7 +142,7 @@ public function getUserClaims($user_id, $claims) // address is an object with subfields $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); } else { - $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails)); + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); } } } @@ -236,7 +236,13 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - unset($this->refreshTokens[$refresh_token]); + if (isset($this->refreshTokens[$refresh_token])) { + unset($this->refreshTokens[$refresh_token]); + + return true; + } + + return false; } public function setRefreshTokens($refresh_tokens) @@ -257,6 +263,17 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return true; } + public function unsetAccessToken($access_token) + { + if (isset($this->accessTokens[$access_token])) { + unset($this->accessTokens[$access_token]); + + return true; + } + + return false; + } + public function scopeExists($scope) { $scope = explode(' ', trim($scope)); @@ -361,4 +378,4 @@ public function getEncryptionAlgorithm($client_id = null) return 'RS256'; } -} +} \ No newline at end of file diff --git a/OAuth2/Storage/Mongo.php b/OAuth2/Storage/Mongo.php index 87935fe..eea06e3 100644 --- a/OAuth2/Storage/Mongo.php +++ b/OAuth2/Storage/Mongo.php @@ -22,6 +22,7 @@ class Mongo implements AuthorizationCodeInterface, UserCredentialsInterface, RefreshTokenInterface, JwtBearerInterface, + PublicKeyInterface, OpenIDAuthorizationCodeInterface { protected $db; @@ -40,16 +41,13 @@ public function __construct($connection, $config = array()) $this->db = $m->{$connection['database']}; } - // Unix timestamps might get larger than 32 bits, - // so let's add native support for 64 bit ints. - ini_set('mongo.native_long', 1); - $this->config = array_merge(array( 'client_table' => 'oauth_clients', 'access_token_table' => 'oauth_access_tokens', 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', + 'key_table' => 'oauth_keys', 'jwt_table' => 'oauth_jwt', ), $config); } @@ -101,16 +99,15 @@ public function setClientDetails($client_id, $client_secret = null, $redirect_ur )) ); } else { - $this->collection('client_table')->insert( - array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - ) + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, ); + $this->collection('client_table')->insert($client); } return true; @@ -151,20 +148,28 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s )) ); } else { - $this->collection('access_token_table')->insert( - array( - 'access_token' => $access_token, - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - ) + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope ); + $this->collection('access_token_table')->insert($token); } return true; } + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->remove(array( + 'access_token' => $access_token + ), array('w' => 1)); + + return $result['n'] > 0; + } + /* AuthorizationCodeInterface */ public function getAuthorizationCode($code) @@ -190,17 +195,16 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, )) ); } else { - $this->collection('code_table')->insert( - array( - 'authorization_code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - ) + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, ); + $this->collection('code_table')->insert($token); } return true; @@ -213,7 +217,6 @@ public function expireAuthorizationCode($code) return true; } - /* UserCredentialsInterface */ public function checkUserCredentials($username, $password) { @@ -243,27 +246,27 @@ public function getRefreshToken($refresh_token) public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { - $this->collection('refresh_token_table')->insert( - array( - 'refresh_token' => $refresh_token, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'scope' => $scope - ) + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope ); + $this->collection('refresh_token_table')->insert($token); return true; } public function unsetRefreshToken($refresh_token) { - $this->collection('refresh_token_table')->remove(array('refresh_token' => $refresh_token)); + $result = $this->collection('refresh_token_table')->remove(array( + 'refresh_token' => $refresh_token + ), array('w' => 1)); - return true; + return $result['n'] > 0; } - // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { @@ -289,14 +292,13 @@ public function setUser($username, $password, $firstName = null, $lastName = nul )) ); } else { - $this->collection('user_table')->insert( - array( - 'username' => $username, - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - ) + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName ); + $this->collection('user_table')->insert($user); } return true; @@ -336,4 +338,55 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti) //TODO: Needs mongodb implementation. throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } } diff --git a/OAuth2/Storage/MongoDB.php b/OAuth2/Storage/MongoDB.php new file mode 100644 index 0000000..64f740f --- /dev/null +++ b/OAuth2/Storage/MongoDB.php @@ -0,0 +1,380 @@ + + */ +class MongoDB implements AuthorizationCodeInterface, + UserCredentialsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof Database) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new Client($server); + $this->db = $m->selectDatabase($connection['database']); + } + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'key_table' => 'oauth_keys', + ), $config); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $result = $this->collection('client_table')->updateOne( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + return $result->getMatchedCount() > 0; + } + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $result = $this->collection('client_table')->insertOne($client); + return $result->getInsertedCount() > 0; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + return in_array($grant_type, $grant_types); + } + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $result = $this->collection('access_token_table')->updateOne( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $result = $this->collection('access_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->deleteOne(array( + 'access_token' => $access_token + )); + return $result->getDeletedCount() > 0; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array( + 'authorization_code' => $code + )); + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $result = $this->collection('code_table')->updateOne( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $result = $this->collection('code_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function expireAuthorizationCode($code) + { + $result = $this->collection('code_table')->deleteOne(array( + 'authorization_code' => $code + )); + return $result->getDeletedCount() > 0; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array( + 'refresh_token' => $refresh_token + )); + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $result = $this->collection('refresh_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->deleteOne(array( + 'refresh_token' => $refresh_token + )); + return $result->getDeletedCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $result = $this->collection('user_table')->updateOne( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + + return $result->getMatchedCount() > 0; + } + + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $result = $this->collection('user_table')->insertOne($user); + return $result->getInsertedCount() > 0; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } +} \ No newline at end of file diff --git a/OAuth2/Storage/Pdo.php b/OAuth2/Storage/Pdo.php index 17016e7..074cee4 100644 --- a/OAuth2/Storage/Pdo.php +++ b/OAuth2/Storage/Pdo.php @@ -4,6 +4,7 @@ use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use InvalidArgumentException; /** * Simple PDO storage for all storage types @@ -29,9 +30,22 @@ class Pdo implements UserClaimsInterface, OpenIDAuthorizationCodeInterface { + /** + * @var \PDO + */ protected $db; + + /** + * @var array + */ protected $config; + /** + * @param mixed $connection + * @param array $config + * + * @throws InvalidArgumentException + */ public function __construct($connection, $config = array()) { if (!$connection instanceof \PDO) { @@ -64,43 +78,64 @@ public function __construct($connection, $config = array()) 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', 'scope_table' => 'oauth_scopes', 'public_key_table' => 'oauth_public_keys', ), $config); } - /* OAuth2\Storage\ClientCredentialsInterface */ + /** + * @param string $client_id + * @param null|string $client_secret + * @return bool + */ public function checkClientCredentials($client_id, $client_secret = null) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - $result = $stmt->fetch(); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); // make this extensible return $result && $result['client_secret'] == $client_secret; } + /** + * @param string $client_id + * @return bool + */ public function isPublicClient($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - if (!$result = $stmt->fetch()) { + if (!$result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return false; } - return empty($result['client_secret']);; + return empty($result['client_secret']); } - /* OAuth2\Storage\ClientInterface */ + /** + * @param string $client_id + * @return array|mixed + */ public function getClientDetails($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - return $stmt->fetch(); + return $stmt->fetch(\PDO::FETCH_ASSOC); } + /** + * @param string $client_id + * @param null|string $client_secret + * @param null|string $redirect_uri + * @param null|array $grant_types + * @param null|string $scope + * @param null|string $user_id + * @return bool + */ public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { // if it exists, update it. @@ -113,6 +148,11 @@ public function setClientDetails($client_id, $client_secret = null, $redirect_ur return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')); } + /** + * @param $client_id + * @param $grant_type + * @return bool + */ public function checkRestrictedGrantType($client_id, $grant_type) { $details = $this->getClientDetails($client_id); @@ -126,13 +166,16 @@ public function checkRestrictedGrantType($client_id, $grant_type) return true; } - /* OAuth2\Storage\AccessTokenInterface */ + /** + * @param string $access_token + * @return array|bool|mixed|null + */ public function getAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); $token = $stmt->execute(compact('access_token')); - if ($token = $stmt->fetch()) { + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { // convert date string back to timestamp $token['expires'] = strtotime($token['expires']); } @@ -140,6 +183,14 @@ public function getAccessToken($access_token) return $token; } + /** + * @param string $access_token + * @param mixed $client_id + * @param mixed $user_id + * @param int $expires + * @param string $scope + * @return bool + */ public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring @@ -155,13 +206,30 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } + /** + * @param $access_token + * @return bool + */ + public function unsetAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table'])); + + $stmt->execute(compact('access_token')); + + return $stmt->rowCount() > 0; + } + /* OAuth2\Storage\AuthorizationCodeInterface */ + /** + * @param string $code + * @return mixed + */ public function getAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); $stmt->execute(compact('code')); - if ($code = $stmt->fetch()) { + if ($code = $stmt->fetch(\PDO::FETCH_ASSOC)) { // convert date string back to timestamp $code['expires'] = strtotime($code['expires']); } @@ -169,6 +237,16 @@ public function getAuthorizationCode($code) return $code; } + /** + * @param string $code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param int $expires + * @param string $scope + * @param string $id_token + * @return bool|mixed + */ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { if (func_num_args() > 6) { @@ -189,6 +267,16 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); } + /** + * @param string $code + * @param mixed $client_id + * @param mixed $user_id + * @param string $redirect_uri + * @param string $expires + * @param string $scope + * @param string $id_token + * @return bool + */ private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { // convert expires to datestring @@ -204,6 +292,10 @@ private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $r return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); } + /** + * @param string $code + * @return bool + */ public function expireAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table'])); @@ -211,7 +303,11 @@ public function expireAuthorizationCode($code) return $stmt->execute(compact('code')); } - /* OAuth2\Storage\UserCredentialsInterface */ + /** + * @param string $username + * @param string $password + * @return bool + */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { @@ -221,12 +317,20 @@ public function checkUserCredentials($username, $password) return false; } + /** + * @param string $username + * @return array|bool + */ public function getUserDetails($username) { return $this->getUser($username); } - /* UserClaimsInterface */ + /** + * @param mixed $user_id + * @param string $claims + * @return array|bool + */ public function getUserClaims($user_id, $claims) { if (!$userDetails = $this->getUserDetails($user_id)) { @@ -252,6 +356,11 @@ public function getUserClaims($user_id, $claims) return $userClaims; } + /** + * @param string $claim + * @param array $userDetails + * @return array + */ protected function getUserClaim($claim, $userDetails) { $userClaims = array(); @@ -265,13 +374,16 @@ protected function getUserClaim($claim, $userDetails) return $userClaims; } - /* OAuth2\Storage\RefreshTokenInterface */ + /** + * @param string $refresh_token + * @return bool|mixed + */ public function getRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); $token = $stmt->execute(compact('refresh_token')); - if ($token = $stmt->fetch()) { + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { // convert expires to epoch time $token['expires'] = strtotime($token['expires']); } @@ -279,6 +391,14 @@ public function getRefreshToken($refresh_token) return $token; } + /** + * @param string $refresh_token + * @param mixed $client_id + * @param mixed $user_id + * @param string $expires + * @param string $scope + * @return bool + */ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring @@ -289,25 +409,47 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope')); } + /** + * @param string $refresh_token + * @return bool + */ public function unsetRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); - return $stmt->execute(compact('refresh_token')); + $stmt->execute(compact('refresh_token')); + + return $stmt->rowCount() > 0; } - // plaintext passwords are bad! Override this for your application + /** + * plaintext passwords are bad! Override this for your application + * + * @param array $user + * @param string $password + * @return bool + */ protected function checkPassword($user, $password) { - return $user['password'] == sha1($password); + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); } + /** + * @param string $username + * @return array|bool + */ public function getUser($username) { $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); $stmt->execute(array('username' => $username)); - if (!$userInfo = $stmt->fetch()) { + if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) { return false; } @@ -317,10 +459,19 @@ public function getUser($username) ), $userInfo); } + /** + * plaintext passwords are bad! Override this for your application + * + * @param string $username + * @param string $password + * @param string $firstName + * @param string $lastName + * @return bool + */ public function setUser($username, $password, $firstName = null, $lastName = null) { // do not store in plaintext - $password = sha1($password); + $password = $this->hashPassword($password); // if it exists, update it. if ($this->getUser($username)) { @@ -332,7 +483,10 @@ public function setUser($username, $password, $firstName = null, $lastName = nul return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); } - /* ScopeInterface */ + /** + * @param string $scope + * @return bool + */ public function scopeExists($scope) { $scope = explode(' ', $scope); @@ -340,19 +494,23 @@ public function scopeExists($scope) $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn)); $stmt->execute($scope); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return $result['count'] == count($scope); } return false; } + /** + * @param mixed $client_id + * @return null|string + */ public function getDefaultScope($client_id = null) { $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); $stmt->execute(array('is_default' => true)); - if ($result = $stmt->fetchAll()) { + if ($result = $stmt->fetchAll(\PDO::FETCH_ASSOC)) { $defaultScope = array_map(function ($row) { return $row['scope']; }, $result); @@ -363,7 +521,11 @@ public function getDefaultScope($client_id = null) return null; } - /* JWTBearerInterface */ + /** + * @param mixed $client_id + * @param $subject + * @return string + */ public function getClientKey($client_id, $subject) { $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table'])); @@ -373,6 +535,10 @@ public function getClientKey($client_id, $subject) return $stmt->fetchColumn(); } + /** + * @param mixed $client_id + * @return bool|null + */ public function getClientScope($client_id) { if (!$clientDetails = $this->getClientDetails($client_id)) { @@ -386,13 +552,21 @@ public function getClientScope($client_id) return null; } + /** + * @param mixed $client_id + * @param $subject + * @param $audience + * @param $expires + * @param $jti + * @return array|null + */ public function getJti($client_id, $subject, $audience, $expires, $jti) { $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table'])); $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return array( 'issuer' => $result['issuer'], 'subject' => $result['subject'], @@ -405,6 +579,14 @@ public function getJti($client_id, $subject, $audience, $expires, $jti) return null; } + /** + * @param mixed $client_id + * @param $subject + * @param $audience + * @param $expires + * @param $jti + * @return bool + */ public function setJti($client_id, $subject, $audience, $expires, $jti) { $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table'])); @@ -412,33 +594,44 @@ public function setJti($client_id, $subject, $audience, $expires, $jti) return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); } - /* PublicKeyInterface */ + /** + * @param mixed $client_id + * @return mixed + */ public function getPublicKey($client_id = null) { $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return $result['public_key']; } } + /** + * @param mixed $client_id + * @return mixed + */ public function getPrivateKey($client_id = null) { $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return $result['private_key']; } } + /** + * @param mixed $client_id + * @return string + */ public function getEncryptionAlgorithm($client_id = null) { $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { return $result['encryption_algorithm']; } @@ -449,13 +642,16 @@ public function getEncryptionAlgorithm($client_id = null) * DDL to create OAuth2 database and tables for PDO storage * * @see https://github.com/dsquier/oauth2-server-php-mysql + * + * @param string $dbName + * @return string */ public function getBuildSql($dbName = 'oauth2_server_php') { $sql = " CREATE TABLE {$this->config['client_table']} ( client_id VARCHAR(80) NOT NULL, - client_secret VARCHAR(80) NOT NULL, + client_secret VARCHAR(80), redirect_uri VARCHAR(2000), grant_types VARCHAR(80), scope VARCHAR(4000), @@ -463,65 +659,73 @@ public function getBuildSql($dbName = 'oauth2_server_php') PRIMARY KEY (client_id) ); - CREATE TABLE {$this->config['access_token_table']} ( - access_token VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - PRIMARY KEY (access_token) - ); + CREATE TABLE {$this->config['access_token_table']} ( + access_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (access_token) + ); - CREATE TABLE {$this->config['code_table']} ( - authorization_code VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - redirect_uri VARCHAR(2000), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - id_token VARCHAR(1000), - PRIMARY KEY (authorization_code) - ); + CREATE TABLE {$this->config['code_table']} ( + authorization_code VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + redirect_uri VARCHAR(2000), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + id_token VARCHAR(1000), + PRIMARY KEY (authorization_code) + ); - CREATE TABLE {$this->config['refresh_token_table']} ( - refresh_token VARCHAR(40) NOT NULL, - client_id VARCHAR(80) NOT NULL, - user_id VARCHAR(80), - expires TIMESTAMP NOT NULL, - scope VARCHAR(4000), - PRIMARY KEY (refresh_token) - ); + CREATE TABLE {$this->config['refresh_token_table']} ( + refresh_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (refresh_token) + ); - CREATE TABLE {$this->config['user_table']} ( - username VARCHAR(80), - password VARCHAR(80), - first_name VARCHAR(80), - last_name VARCHAR(80), - email VARCHAR(80), - email_verified BOOLEAN, - scope VARCHAR(4000) - ); + CREATE TABLE {$this->config['user_table']} ( + username VARCHAR(80), + password VARCHAR(80), + first_name VARCHAR(80), + last_name VARCHAR(80), + email VARCHAR(80), + email_verified BOOLEAN, + scope VARCHAR(4000) + ); - CREATE TABLE {$this->config['scope_table']} ( - scope VARCHAR(80) NOT NULL, - is_default BOOLEAN, - PRIMARY KEY (scope) - ); + CREATE TABLE {$this->config['scope_table']} ( + scope VARCHAR(80) NOT NULL, + is_default BOOLEAN, + PRIMARY KEY (scope) + ); - CREATE TABLE {$this->config['jwt_table']} ( - client_id VARCHAR(80) NOT NULL, - subject VARCHAR(80), - public_key VARCHAR(2000) NOT NULL - ); + CREATE TABLE {$this->config['jwt_table']} ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80), + public_key VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['jti_table']} ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80), + audiance VARCHAR(80), + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL + ); - CREATE TABLE {$this->config['public_key_table']} ( - client_id VARCHAR(80), - public_key VARCHAR(2000), - private_key VARCHAR(2000), - encryption_algorithm VARCHAR(100) DEFAULT 'RS256' - ) -"; + CREATE TABLE {$this->config['public_key_table']} ( + client_id VARCHAR(80), + public_key VARCHAR(2000), + private_key VARCHAR(2000), + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' + ) + "; return $sql; } -} +} \ No newline at end of file diff --git a/OAuth2/Storage/PublicKeyInterface.php b/OAuth2/Storage/PublicKeyInterface.php index 108418d..a6dc49f 100644 --- a/OAuth2/Storage/PublicKeyInterface.php +++ b/OAuth2/Storage/PublicKeyInterface.php @@ -10,7 +10,21 @@ */ interface PublicKeyInterface { + /** + * @param mixed $client_id + * @return mixed + */ public function getPublicKey($client_id = null); + + /** + * @param mixed $client_id + * @return mixed + */ public function getPrivateKey($client_id = null); + + /** + * @param mixed $client_id + * @return mixed + */ public function getEncryptionAlgorithm($client_id = null); -} +} \ No newline at end of file diff --git a/OAuth2/Storage/Redis.php b/OAuth2/Storage/Redis.php index fd94f47..e6294e2 100644 --- a/OAuth2/Storage/Redis.php +++ b/OAuth2/Storage/Redis.php @@ -162,7 +162,7 @@ public function isPublicClient($client_id) return false; } - return empty($result['client_secret']);; + return empty($client['client_secret']); } /* ClientInterface */ @@ -209,7 +209,9 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + + return $result > 0; } /* AccessTokenInterface */ @@ -227,6 +229,13 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s ); } + public function unsetAccessToken($access_token) + { + $result = $this->expireValue($this->config['access_token_key'] . $access_token); + + return $result > 0; + } + /* ScopeInterface */ public function scopeExists($scope) { diff --git a/OAuth2/Storage/RefreshTokenInterface.php b/OAuth2/Storage/RefreshTokenInterface.php index 0273f21..e6407e4 100644 --- a/OAuth2/Storage/RefreshTokenInterface.php +++ b/OAuth2/Storage/RefreshTokenInterface.php @@ -74,7 +74,7 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, * and provide a descriptive fail message. * * @param $refresh_token - * Refresh token to be expirse. + * Refresh token to be expired. * * @ingroup oauth2_section_6 */ diff --git a/OAuth2/Storage/UserCredentialsInterface.php b/OAuth2/Storage/UserCredentialsInterface.php index 6e0fd7b..f550579 100644 --- a/OAuth2/Storage/UserCredentialsInterface.php +++ b/OAuth2/Storage/UserCredentialsInterface.php @@ -37,15 +37,15 @@ interface UserCredentialsInterface public function checkUserCredentials($username, $password); /** - * @return - * ARRAY the associated "user_id" and optional "scope" values - * This function MUST return FALSE if the requested user does not exist or is - * invalid. "scope" is a space-separated list of restricted scopes. + * @param string $username - username to get details for + * @return array|false - the associated "user_id" and optional "scope" values + * This function MUST return FALSE if the requested user does not exist or is + * invalid. "scope" is a space-separated list of restricted scopes. * @code - * return array( - * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token - * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes - * ); + * return array( + * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token + * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes + * ); * @endcode */ public function getUserDetails($username); diff --git a/OAuth2/TokenType/Bearer.php b/OAuth2/TokenType/Bearer.php index 0ec9fec..8ac8596 100644 --- a/OAuth2/TokenType/Bearer.php +++ b/OAuth2/TokenType/Bearer.php @@ -91,7 +91,7 @@ public function getAccessTokenParameter(RequestInterface $request, ResponseInter // HEADER: Get the access token from the header if (!empty($headers)) { - if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/', $headers, $matches)) { + if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/i', $headers, $matches)) { $response->setError(400, 'invalid_request', 'Malformed auth header'); return null; diff --git a/db/install.xml b/db/install.xml old mode 100755 new mode 100644 diff --git a/index.php b/index.php index f4be545..7805103 100644 --- a/index.php +++ b/index.php @@ -67,12 +67,12 @@ $form->user_id = $client_edit->user_id; $form->action = 'edit'; } else { - $form->client_id = ""; - $form->redirect_uri = ""; - $form->grant_types = "authorization_code"; - $form->scope = "user_info"; - $form->user_id = "0"; - $form->action = 'add'; + $form->client_id = ""; + $form->redirect_uri = ""; + $form->grant_types = "authorization_code"; + $form->scope = "user_info"; + $form->user_id = "0"; + $form->action = 'add'; } $bform->set_data($form); $bform->display(); @@ -135,6 +135,7 @@ if ($view_table) { echo '

'; + if (function_exists('is_agora') && is_agora()) { if (is_service_enabled('nodes') && !$DB->record_exists('oauth_clients', array('client_id' => 'nodes'))) { echo ''.get_string('addnodesclient', 'local_oauth').''; @@ -145,7 +146,6 @@ echo ''.get_string('addclient', 'local_oauth').''; } - echo '

'; $clients = $DB->get_records('oauth_clients'); diff --git a/lang/ca/local_oauth.php b/lang/ca/local_oauth.php index 7a4e2a9..ebd29e9 100644 --- a/lang/ca/local_oauth.php +++ b/lang/ca/local_oauth.php @@ -39,4 +39,4 @@ $string['event_user_info_request_failed'] = 'Error en la petició de dades d\'usuari/ària'; $string['client_id_help'] = 'ID de l\'aplicació per fer servir al formulari del client (XTECBlocs o Nodes) per tal de referenciar el proveïdor. Ha de ser únic. Per exemple, un ID de l\'aplicació vàlid pot ser "blog1" o "nodes".'; -$string['redirect_uri_help'] = 'URI a on redirigir després d\'autenticar-se. Per exemple, per a XTECBlocs o Nodes, l\'URL de redirecció ha de ser semblant a:
  • XTECBlocs: http://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
  • NODES: http://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
'; +$string['redirect_uri_help'] = 'URI a on redirigir després d\'autenticar-se. Per exemple, per a XTECBlocs o Nodes, l\'URL de redirecció ha de ser semblant a:
  • XTECBlocs: https://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
  • NODES: https://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
'; diff --git a/lang/en/local_oauth.php b/lang/en/local_oauth.php index 2d50385..188147f 100644 --- a/lang/en/local_oauth.php +++ b/lang/en/local_oauth.php @@ -39,4 +39,4 @@ $string['event_user_info_request_failed'] = 'User info request failed'; $string['client_id_help'] = 'Identifier to be used from the client form in order to reference this provider. It has to be unique. For instance, a valid identifier could be "blog1" or "nodes".'; -$string['redirect_uri_help'] = 'URI where to redirect after login. For instance, for XTECBlocs or Nodes, the redirect URI are like:
  • XTECBlocs: http://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
  • NODES: http://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
'; +$string['redirect_uri_help'] = 'URI where to redirect after login. For instance, for XTECBlocs or Nodes, the redirect URI are like:
  • XTECBlocs: https://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
  • NODES: https://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
'; diff --git a/lang/es/local_oauth.php b/lang/es/local_oauth.php index 4a11d50..3f04bbd 100644 --- a/lang/es/local_oauth.php +++ b/lang/es/local_oauth.php @@ -24,7 +24,7 @@ $string['client_not_exists'] = 'El cliente no existe'; $string['saveok'] = 'El cliente se ha guardado correctamente'; -$string['confirmdeletestr'] = 'Está seguro que quiere eleminar el cliente {$a}?'; +$string['confirmdeletestr'] = 'Está seguro que quiere eliminar el cliente {$a}?'; $string['delok'] = 'El cliente se ha eliminado correctamente'; $string['client_id_existing_error'] = 'El identificador de cliente especificado ya existe, seleccione otro'; $string['insert_error'] = 'Se ha producido un error creando el cliente'; @@ -39,4 +39,4 @@ $string['event_user_info_request_failed'] = 'Error en la petición de datos de usuario'; $string['client_id_help'] = 'ID de la aplicación para utilizar en el formulario del cliente (XTECBlocs o Nodes) para referenciar el proveedor. Debe ser único. Por ejemplo, un ID de la aplicación válido puede ser "blog1" o "nodes".'; -$string['redirect_uri_help'] = 'URI donde redirigir después de la autenticación. Por ejemplo, para XTECBlocs o Nodes, la URL de redirección debe ser parecida a:
  • XTECBlocs: http://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
  • NODES: http://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle
'; +$string['redirect_uri_help'] = 'URI donde redirigir después de la autenticación. Por ejemplo, para XTECBlocs o Nodes, la URL de redirección debe ser parecida a:
  • XTECBlocs: https://blocs.xtec.cat/nomdelbloc/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
  • NODES: https://agora.xtec.cat/nomdelcentre/wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php
'; diff --git a/locallib.php b/locallib.php index 3041db7..aa25116 100644 --- a/locallib.php +++ b/locallib.php @@ -8,7 +8,7 @@ function oauth_add_wordpress_client($client_id, $url) { $url .= '/'; } $record = new stdClass(); - $record->redirect_uri = $url.'wp-content/plugins/wordpress-social-login/hybridauth/?hauth.done=Moodle'; + $record->redirect_uri = $url . 'wp-content/plugins/wordpress-social-login/hybridauth/callbacks/moodle.php'; $record->grant_types = 'authorization_code'; $record->scope = 'user_info '; $record->user_id = ''; diff --git a/settings.php b/settings.php old mode 100755 new mode 100644 diff --git a/version.php b/version.php index d12fdc5..07b7606 100644 --- a/version.php +++ b/version.php @@ -2,6 +2,6 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2016021601; // The (date) version of this plugin +$plugin->version = 2021071400; // The (date) version of this plugin $plugin->requires = 2011021900; // Requires this Moodle version -$plugin->component = 'local_oauth'; \ No newline at end of file +$plugin->component = 'local_oauth';