diff options
| author | Brent Shaffer <bshafs@gmail.com> | 2013-05-28 01:28:48 +0400 |
|---|---|---|
| committer | Brent Shaffer <bshafs@gmail.com> | 2013-05-28 01:28:48 +0400 |
| commit | f58f5adf9ff7f7dbb0e1938bce1e726d94837cc0 (patch) | |
| tree | 4175cff0eeb992de6cf93a70367a94b0f595b426 /src | |
| parent | d5bb9c8bdbe8ef86c65d6479f81db859f707b865 (diff) | |
| parent | e82dfa379bf021f2b3c00ce7f89a596ae4b0e5d4 (diff) | |
| download | oauth2-server-php-0.8.tar.xz | |
merges develop into trunk for v0.8 tagv0.8
Diffstat (limited to 'src')
44 files changed, 1052 insertions, 841 deletions
diff --git a/src/OAuth2/ClientAssertionType/HttpBasic.php b/src/OAuth2/ClientAssertionType/HttpBasic.php index aa1d130..db376b0 100644 --- a/src/OAuth2/ClientAssertionType/HttpBasic.php +++ b/src/OAuth2/ClientAssertionType/HttpBasic.php @@ -5,11 +5,11 @@ * * @author Brent Shaffer <bshafs at gmail dot com> */ -class OAuth2_ClientAssertionType_HttpBasic implements OAuth2_ClientAssertionTypeInterface, OAuth2_Response_ProviderInterface +class OAuth2_ClientAssertionType_HttpBasic implements OAuth2_ClientAssertionTypeInterface { - private $response; private $storage; private $config; + private $clientData; public function __construct(OAuth2_Storage_ClientCredentialsInterface $storage, array $config = array()) { @@ -19,34 +19,36 @@ class OAuth2_ClientAssertionType_HttpBasic implements OAuth2_ClientAssertionType $this->storage = $storage; } - public function getClientDataFromRequest(OAuth2_RequestInterface $request) + public function validateRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - if (!$clientData = $this->getClientCredentials($request)) { - return null; - } - - if (!isset($clientData['client_id']) || !isset($clientData['client_secret'])) { - throw new LogicException('the clientData array must have "client_id" and "client_secret" values set.'); - } + if (!$clientData = $this->getClientCredentials($request, $response)) { + return false; + } - return $clientData; - } + if (!isset($clientData['client_id']) || !isset($clientData['client_secret'])) { + throw new LogicException('the clientData array must have "client_id" and "client_secret" values set.'); + } - public function validateClientData(array $clientData, $grantTypeIdentifier) - { if ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { - $this->response = new OAuth2_Response_Error(400, 'invalid_client', 'The client credentials are invalid'); + $response->setError(400, 'invalid_client', 'The client credentials are invalid'); return false; } - if (!$this->storage->checkRestrictedGrantType($clientData['client_id'], $grantTypeIdentifier)) { - $this->response = new OAuth2_Response_Error(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id'); + if (!$this->storage->checkRestrictedGrantType($clientData['client_id'], $request->request('grant_type'))) { + $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id'); return false; } + $this->clientData = $clientData; + return true; } + public function getClientId() + { + return $this->clientData['client_id']; + } + /** * Internal function used to get the client credentials from HTTP basic * auth or POST data. @@ -67,7 +69,7 @@ class OAuth2_ClientAssertionType_HttpBasic implements OAuth2_ClientAssertionType * * @ingroup oauth2_section_2 */ - public function getClientCredentials(OAuth2_RequestInterface $request) + public function getClientCredentials(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response = null) { if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); @@ -80,12 +82,10 @@ class OAuth2_ClientAssertionType_HttpBasic implements OAuth2_ClientAssertionType } } - $this->response = new OAuth2_Response_Error(400, 'invalid_client', 'Client credentials were not found in the headers or body'); - return null; - } + if ($response) { + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers or body'); + } - public function getResponse() - { - return $this->response; + return null; } }
\ No newline at end of file diff --git a/src/OAuth2/ClientAssertionTypeInterface.php b/src/OAuth2/ClientAssertionTypeInterface.php index 9d034c4..93f6005 100644 --- a/src/OAuth2/ClientAssertionTypeInterface.php +++ b/src/OAuth2/ClientAssertionTypeInterface.php @@ -1,8 +1,11 @@ <?php -interface OAuth2_ClientAssertionTypeInterface +/** + * Interface for all OAuth2 Client Assertion Types + * + * @see OAuth2_CompatibilityInterface + */ +interface OAuth2_ClientAssertionTypeInterface extends OAuth2_CompatibilityInterface { - public function getClientDataFromRequest(OAuth2_RequestInterface $request); - public function validateClientData(array $clientData, $grantTypeIdentifier); } diff --git a/src/OAuth2/CompatibilityInterface.php b/src/OAuth2/CompatibilityInterface.php new file mode 100644 index 0000000..6707573 --- /dev/null +++ b/src/OAuth2/CompatibilityInterface.php @@ -0,0 +1,16 @@ +<?php + +/** + * OAuth2_CompatibilityInterface + * This interface exists because PHP 5.2 cannot combine matching + * functions from separate interfaces. + * + * Even though OAuth2_ClientAssertionTypeInterface and OAuth2_GrantTypeInterface + * have common methods, it doesn't actually make sense to extend a base interface. + * However, this is temporarily required for the library to run in PHP 5.2 + * + */ +interface OAuth2_CompatibilityInterface +{ + +}
\ No newline at end of file diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index 7075c3d..701e886 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -1,11 +1,10 @@ <?php /** - * @see OAuth2_Controller_AuthorizeControllerInterface + * @see OAuth2_Controller_AuthorizeControllerInterface */ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_AuthorizeControllerInterface { - private $response; private $clientStorage; private $responseTypes; private $config; @@ -26,7 +25,7 @@ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_Authori $this->scopeUtil = $scopeUtil; } - public function handleAuthorizeRequest(OAuth2_RequestInterface $request, $is_authorized, $user_id = null) + public function handleAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_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.'); @@ -34,45 +33,42 @@ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_Authori // We repeat this, because we need to re-validate. In theory, this could be POSTed // by a 3rd-party (because we are not internally enforcing NONCEs, etc) - if (!$params = $this->validateAuthorizeRequest($request)) { - return $this->response; + if (!$params = $this->validateAuthorizeRequest($request, $response)) { + return; } if ($is_authorized === false) { - $this->response = new OAuth2_Response_Redirect($params['redirect_uri'], 302, 'access_denied', "The user denied access to your application", $params['state']); - return $this->response; + $response->setRedirect(302, $params['redirect_uri'], $params['state'], 'access_denied', "The user denied access to your application"); + return; } - if (!$authResult = $this->responseTypes[$params['response_type']]->getAuthorizeResponse($params, $user_id)) { - // an error has occurred along the way, return error response - return $this->response; - } + $authResult = $this->responseTypes[$params['response_type']]->getAuthorizeResponse($params, $user_id); - list($redirect_uri, $result) = $authResult; - $uri = $this->buildUri($redirect_uri, $result); + list($redirect_uri, $uri_params) = $authResult; + $uri = $this->buildUri($redirect_uri, $uri_params); // return redirect response - return $this->response = new OAuth2_Response_Redirect($uri); + $response->setRedirect(302, $uri); } - public function validateAuthorizeRequest(OAuth2_RequestInterface $request) + public function validateAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_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")) { // We don't have a good URI to use - $this->response = new OAuth2_Response_Error(400, 'invalid_client', "No client id supplied"); + $response->setError(400, 'invalid_client', "No client id supplied"); return false; } // Get client details if (!$clientData = $this->clientStorage->getClientDetails($client_id)) { - $this->response = new OAuth2_Response_Error(400, 'invalid_client', 'The client id supplied is invalid'); + $response->setError(400, 'invalid_client', 'The client id supplied is invalid'); return false; } $clientData += array('redirect_uri' => null); // this should be set. We should create ClientData interface if ($clientData === false) { - $this->response = new OAuth2_Response_Error(400, 'invalid_client', "Client id does not exist"); + $response->setError(400, 'invalid_client', "Client id does not exist"); return false; } @@ -81,20 +77,20 @@ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_Authori // @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 (!($redirect_uri = $request->query('redirect_uri')) && !($redirect_uri = $clientData['redirect_uri'])) { - $this->response = new OAuth2_Response_Error(400, 'invalid_uri', 'No redirect URI was supplied or stored'); + $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored'); return false; } $parts = parse_url($redirect_uri); if (isset($parts['fragment']) && $parts['fragment']) { - $this->response = new OAuth2_Response_Error(400, 'invalid_uri', 'The redirect URI must not contain a fragment'); + $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment'); return false; } // Only need to validate if redirect_uri provided on input and clientData. if ($clientData["redirect_uri"] && $redirect_uri && !$this->validateRedirectUri($redirect_uri, $clientData["redirect_uri"])) { - $this->response = new OAuth2_Response_Error(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match'); + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match'); return false; } @@ -108,54 +104,54 @@ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_Authori // type and client_id are required if (!$response_type || !in_array($response_type, array(self::RESPONSE_TYPE_AUTHORIZATION_CODE, self::RESPONSE_TYPE_ACCESS_TOKEN))) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'invalid_request', 'Invalid or missing response type', $state); + $response->setRedirect(302, $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null); return false; } if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { if (!isset($this->responseTypes['code'])) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'unsupported_response_type', 'authorization code grant type not supported', $state); + $response->setRedirect(302, $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); return false; } if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'unauthorized_client', 'The grant type is unauthorized for this client_id', $state); + $response->setRedirect(302, $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); return false; } if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) { - $this->response = new OAuth2_Response_Error(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied.'); + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied.'); return false; } } if ($response_type == self::RESPONSE_TYPE_ACCESS_TOKEN) { if (!$this->config['allow_implicit']) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'unsupported_response_type', 'implicit grant type not supported', $state); + $response->setRedirect(302, $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null); return false; } if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'unauthorized_client', 'The grant type is unauthorized for this client_id', $state); + $response->setRedirect(302, $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); return false; } } // Validate that the requested scope is supported if (false === $scope) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'invalid_client', 'This application requires you specify a scope parameter', $state); + $response->setRedirect(302, $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null); return false; } if (!is_null($scope) && !$this->scopeUtil->scopeExists($scope, $client_id)) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'invalid_scope', 'An unsupported scope was requested', $state); + $response->setRedirect(302, $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); return false; } // Validate state parameter exists (if configured to enforce this) if ($this->config['enforce_state'] && !$state) { - $this->response = new OAuth2_Response_Redirect($redirect_uri, 302, 'invalid_request', 'The state parameter is required'); + $response->setRedirect(302, $redirect_uri, null, 'invalid_request', 'The state parameter is required'); return false; } // Return retrieved client details together with input - return array_merge(array('scope' => $scope, 'state' => null), $clientData, $request->getAllQueryParameters(), array('redirect_uri' => $redirect_uri)); + return array_merge(array('scope' => $scope, 'state' => $state), $clientData, $request->getAllQueryParameters(), array('redirect_uri' => $redirect_uri)); } /** @@ -199,19 +195,27 @@ class OAuth2_Controller_AuthorizeController implements OAuth2_Controller_Authori /** * Internal method for validating redirect URI supplied + * * @param string $inputUri + * The submitted URI to be validated + * * @param string $storedUri + * The allowed URIs to validate against. Can be a space-delimited string of URIs to + * allow for multiple URIs */ private function validateRedirectUri($inputUri, $storedUri) { if (!$inputUri || !$storedUri) { return false; // if either one is missing, assume INVALID } - return strcasecmp(substr($inputUri, 0, strlen($storedUri)), $storedUri) === 0; - } - public function getResponse() - { - return $this->response; + $stored_uris = explode(' ', $storedUri); + + foreach ($stored_uris as $stored) { + if (strcasecmp(substr($inputUri, 0, strlen($stored)), $stored) === 0) { + return true; + } + } + return false; } } diff --git a/src/OAuth2/Controller/AuthorizeControllerInterface.php b/src/OAuth2/Controller/AuthorizeControllerInterface.php index 88c8793..7d9ae68 100644 --- a/src/OAuth2/Controller/AuthorizeControllerInterface.php +++ b/src/OAuth2/Controller/AuthorizeControllerInterface.php @@ -9,14 +9,16 @@ * ex: * > $user_id = $this->somehowDetermineUserId(); * > $is_authorized = $this->somehowDetermineUserAuthorization(); - * > $response = $authorizeController->handleAuthorizeRequest( + * > $response = new OAuth2_Response(); + * > $authorizeController->handleAuthorizeRequest( * > OAuth2_Request::createFromGlobals(), + * > $response, * > $is_authorized, * > $user_id); * > $response->send(); * */ -interface OAuth2_Controller_AuthorizeControllerInterface extends OAuth2_Response_ProviderInterface +interface OAuth2_Controller_AuthorizeControllerInterface { /** * List of possible authentication response types. @@ -30,7 +32,7 @@ interface OAuth2_Controller_AuthorizeControllerInterface extends OAuth2_Response const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; - public function handleAuthorizeRequest(OAuth2_RequestInterface $request, $is_authorized, $user_id = null); + public function handleAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response, $is_authorized, $user_id = null); - public function validateAuthorizeRequest(OAuth2_RequestInterface $request); + public function validateAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response); } diff --git a/src/OAuth2/Controller/ResourceController.php b/src/OAuth2/Controller/ResourceController.php index 4dd827d..017b2cf 100644 --- a/src/OAuth2/Controller/ResourceController.php +++ b/src/OAuth2/Controller/ResourceController.php @@ -1,7 +1,7 @@ <?php /** - * @see OAuth2_Controller_ResourceControllerInterface + * @see OAuth2_Controller_ResourceControllerInterface */ class OAuth2_Controller_ResourceController implements OAuth2_Controller_ResourceControllerInterface { @@ -26,52 +26,43 @@ class OAuth2_Controller_ResourceController implements OAuth2_Controller_Resource $this->scopeUtil = $scopeUtil; } - public function verifyResourceRequest(OAuth2_RequestInterface $request, $scope = null) + public function verifyResourceRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response, $scope = null) { - $token_data = $this->getAccessTokenData($request, $scope); + $token = $this->getAccessTokenData($request, $response, $scope); - return (bool) $token_data; + // Check scope, if provided + // If token doesn't have a scope, it's null/empty, or it's insufficient, then throw an error + if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { + $response->setError(401, 'insufficient_scope', 'The request requires higher privileges than provided by the access token'); + $response->addHttpHeaders(array('WWW-Authenticate' => sprintf('%s, realm="%s", scope="%s"', $this->tokenType->getTokenType(), $this->config['www_realm'], $scope))); + return false; + } + + return (bool) $token; } - public function getAccessTokenData(OAuth2_RequestInterface $request, $scope = null) + public function getAccessTokenData(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { // Get the token parameter - $token_param = $this->tokenType->getAccessTokenParameter($request); + $token_param = $this->tokenType->getAccessTokenParameter($request, $response); if (is_null($token_param)) { - $this->response = $this->tokenType->getResponse(); return null; } // Get the stored token data (from the implementing subclass) - if (!$token = $this->tokenStorage->getAccessToken($token_param)) { - $this->response = new OAuth2_Response_AuthenticationError(401, 'invalid_grant', 'The access token provided is invalid', $this->tokenType->getTokenType(), $this->config['www_realm'], $scope); - return null; - } - // Check we have a well formed token - if (!isset($token["expires"]) || !isset($token["client_id"])) { - $this->response = new OAuth2_Response_AuthenticationError(401, 'invalid_grant', 'Malformed token (missing "expires" or "client_id")', $this->tokenType->getTokenType(), $this->config['www_realm'], $scope); - return null; - } - // Check token expiration (expires is a mandatory paramter) - if (isset($token["expires"]) && time() > $token["expires"]) { - $this->response = new OAuth2_Response_AuthenticationError(401, 'invalid_grant', 'The access token provided has expired', $this->tokenType->getTokenType(), $this->config['www_realm'], $scope); - return null; - } - - // Check scope, if provided - // If token doesn't have a scope, it's null/empty, or it's insufficient, then throw an error - if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { - $this->response = new OAuth2_Response_AuthenticationError(401, 'insufficient_scope', 'The request requires higher privileges than provided by the access token', $this->tokenType->getTokenType(), $this->config['www_realm'], $scope); - return null; + if (!$token = $this->tokenStorage->getAccessToken($token_param)) { + $response->setError(401, 'invalid_grant', 'The access token provided is invalid', $this->tokenType->getTokenType(), $this->config['www_realm']); + } else if (!isset($token["expires"]) || !isset($token["client_id"])) { + $response->setError(401, 'invalid_grant', 'Malformed token (missing "expires" or "client_id")', $this->tokenType->getTokenType(), $this->config['www_realm']); + } else if (isset($token["expires"]) && time() > $token["expires"]) { + $response->setError(401, 'invalid_grant', 'The access token provided has expired'); + } else { + return $token; } - return $token; - } - - public function getResponse() - { - return $this->response; + $response->addHttpHeaders(array('WWW-Authenticate' => sprintf('%s, realm="%"', $this->tokenType->getTokenType(), $this->config['www_realm']))); + return null; } } diff --git a/src/OAuth2/Controller/ResourceControllerInterface.php b/src/OAuth2/Controller/ResourceControllerInterface.php index f98f697..df87338 100644 --- a/src/OAuth2/Controller/ResourceControllerInterface.php +++ b/src/OAuth2/Controller/ResourceControllerInterface.php @@ -6,16 +6,16 @@ * contains a valid token. * * ex: - * > if (!$resourceController->verifyResourceRequest(OAuth2_Request::createFromGlobals())) { - * > $resourceController->getResponse()->send(); // authorization failed + * > if (!$resourceController->verifyResourceRequest(OAuth2_Request::createFromGlobals(), $response = new OAuth2_Response())) { + * > $response->send(); // authorization failed * > die(); * > } * > return json_encode($resource); // valid token! Send the stuff! * */ -interface OAuth2_Controller_ResourceControllerInterface extends OAuth2_Response_ProviderInterface +interface OAuth2_Controller_ResourceControllerInterface { - public function verifyResourceRequest(OAuth2_RequestInterface $request, $scope = null); + public function verifyResourceRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response, $scope = null); - public function getAccessTokenData(OAuth2_RequestInterface $request); + public function getAccessTokenData(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response); } diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index f1be5da..1652531 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -1,7 +1,7 @@ <?php /** - * @see OAuth2_Controller_TokenControllerInterface + * @see OAuth2_Controller_TokenControllerInterface */ class OAuth2_Controller_TokenController implements OAuth2_Controller_TokenControllerInterface { @@ -11,14 +11,14 @@ class OAuth2_Controller_TokenController implements OAuth2_Controller_TokenContro private $grantTypes; private $scopeUtil; - public function __construct($clientAssertionType = null, OAuth2_ResponseType_AccessTokenInterface $accessToken, array $grantTypes = array(), OAuth2_ScopeInterface $scopeUtil = null) + public function __construct(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $grantTypes = array(), OAuth2_ClientAssertionTypeInterface $clientAssertionType = null, OAuth2_ScopeInterface $scopeUtil = null) { - if ($clientAssertionType instanceof OAuth2_Storage_ClientCredentialsInterface) { - // this is for backwards compatibility - $clientAssertionType = new OAuth2_ClientAssertionType_HttpBasic($clientAssertionType); - } - if (!is_null($clientAssertionType) && !$clientAssertionType instanceof OAuth2_ClientAssertionTypeInterface) { - throw new LogicException('$clientAssertionType must be an instance of OAuth2_Storage_ClientCredentialsInterface, OAuth2_ClientAssertionTypeInterface, or null'); + if (is_null($clientAssertionType)) { + foreach ($grantTypes as $grantType) { + if (!$grantType instanceof OAuth2_ClientAssertionTypeInterface) { + throw new InvalidArgumentException('You must supply an instance of OAuth2_ClientAssertionTypeInterface or only use grant types which implement OAuth2_ClientAssertionTypeInterface'); + } + } } $this->clientAssertionType = $clientAssertionType; $this->accessToken = $accessToken; @@ -32,14 +32,15 @@ class OAuth2_Controller_TokenController implements OAuth2_Controller_TokenContro $this->scopeUtil = $scopeUtil; } - public function handleTokenRequest(OAuth2_RequestInterface $request) + public function handleTokenRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - if ($token = $this->grantAccessToken($request)) { + if ($token = $this->grantAccessToken($request, $response)) { // @see http://tools.ietf.org/html/rfc6749#section-5.1 // server MUST disable caching in headers when tokens are involved - $this->response = new OAuth2_Response($token, 200, array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); + $response->setStatusCode(200); + $response->addParameters($token); + $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); } - return $this->response; } /** @@ -61,67 +62,71 @@ class OAuth2_Controller_TokenController implements OAuth2_Controller_TokenContro * * @ingroup oauth2_section_4 */ - public function grantAccessToken(OAuth2_RequestInterface $request) + public function grantAccessToken(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { if (strtolower($request->server('REQUEST_METHOD')) != 'post') { - $this->response = new OAuth2_Response_Error(405, 'invalid_request', 'The request method must be POST when requesting an access token', 'http://tools.ietf.org/html/rfc6749#section-3.2'); - $this->response->setHttpHeader( 'Allow', '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')); return null; } - // Determine grant type from request - if (!$grantType = $request->request('grant_type')) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'The grant type was not specified in the request'); + /* Determine grant type from request + * and validate the request for that grant type + */ + if (!$grantTypeIdentifier = $request->request('grant_type')) { + $response->setError(400, 'invalid_request', 'The grant type was not specified in the request'); return null; } - if (!isset($this->grantTypes[$grantType])) { + if (!isset($this->grantTypes[$grantTypeIdentifier])) { /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ - $this->response = new OAuth2_Response_Error(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantType)); - return null; - } - $grantType = $this->grantTypes[$grantType]; - - // Hack to see if clientAssertionType is part of the grant type - // this should change, but right now changing it will break BC - $clientAssertionType = $grantType instanceof OAuth2_ClientAssertionTypeInterface ? $grantType : $this->clientAssertionType; - - $clientData = $clientAssertionType->getClientDataFromRequest($request); - - if (!$clientData || !$clientAssertionType->validateClientData($clientData, $grantType->getQuerystringIdentifier())) { - $this->response = $this->getObjectResponse($clientAssertionType, new OAuth2_Response_Error(400, 'invalid_request', 'Unable to verify client')); - return null; - } - - // validate the request for the token - if (!$grantType->validateRequest($request)) { - $this->response = $this->getObjectResponse($grantType, new OAuth2_Response_Error(400, 'invalid_request', sprintf('Invalid request for "%s" grant type', $grantType->getQuerystringIdentifier()))); - return null; - } - - if (!$tokenData = $grantType->getTokenDataFromRequest($request)) { - $this->response = $this->getObjectResponse($grantType, new OAuth2_Response_Error(400, 'invalid_grant', sprintf('Unable to retrieve token for "%s" grant type', $grantType->getQuerystringIdentifier()))); + $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); return null; } - if (!$grantType->validateTokenData($tokenData, $clientData)) { - $this->response = $this->getObjectResponse($grantType, new OAuth2_Response_Error(400, 'invalid_grant', 'Token is no longer valid')); + $grantType = $this->grantTypes[$grantTypeIdentifier]; + if (!$grantType->validateRequest($request, $response)) { return null; } - if (!isset($tokenData["scope"])) { - $tokenData["scope"] = $this->scopeUtil->getDefaultScope(); - } - - $scope = $this->scopeUtil->getScopeFromRequest($request); - // Check scope, if provided - if (!is_null($scope) && !$this->scopeUtil->checkScope($scope, $tokenData["scope"])) { - $this->response = new OAuth2_Response_Error(400, 'invalid_scope', 'An unsupported scope was requested.'); + /* Retrieve the client information from the request + * 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 + */ + if ($grantType instanceof OAuth2_ClientAssertionTypeInterface) { + $clientId = $grantType->getClientId(); + } else { + if (!$this->clientAssertionType->validateRequest($request, $response)) { + return null; + } + $clientId = $this->clientAssertionType->getClientId(); + + // validate the Client ID (if applicable) + if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) { + $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier)); + return null; + } + } + + /* + * Validate the scope of the token + * If the grant type returns a value for the scope, + * this value must be verified with the scope being requested + */ + $availableScope = $grantType->getScope(); + if (!$requestedScope = $this->scopeUtil->getScopeFromRequest($request)) { + $requestedScope = $availableScope ? $availableScope : $this->scopeUtil->getDefaultScope(); + } + + if (($requestedScope && !$this->scopeUtil->scopeExists($requestedScope, $clientId)) + || ($availableScope && !$this->scopeUtil->checkScope($requestedScope, $availableScope))) { + $response->setError(400, 'invalid_scope', 'An unsupported scope was requested.'); return null; } - $tokenData['user_id'] = isset($tokenData['user_id']) ? $tokenData['user_id'] : null; - - return $grantType->createAccessToken($this->accessToken, $clientData, $tokenData); + return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope); } /** @@ -140,17 +145,4 @@ class OAuth2_Controller_TokenController implements OAuth2_Controller_TokenContro $this->grantTypes[$identifier] = $grantType; } - - public function getResponse() - { - return $this->response; - } - - public function getObjectResponse($object, OAuth2_Response $defaultResponse = null) - { - if ($object instanceof OAuth2_Response_ProviderInterface && $response = $object->getResponse()) { - return $response; - } - return $defaultResponse; - } } diff --git a/src/OAuth2/Controller/TokenControllerInterface.php b/src/OAuth2/Controller/TokenControllerInterface.php index 3af5ac5..4fe9624 100644 --- a/src/OAuth2/Controller/TokenControllerInterface.php +++ b/src/OAuth2/Controller/TokenControllerInterface.php @@ -6,11 +6,11 @@ * It also validates the client's credentials * * ex: - * > $response = $tokenController->handleTokenRequest(OAuth2_Request::createFromGlobals()); + * > $tokenController->handleTokenRequest(OAuth2_Request::createFromGlobals(), $response = new OAuth2_Response()); * > $response->send(); * */ -interface OAuth2_Controller_TokenControllerInterface extends OAuth2_Response_ProviderInterface +interface OAuth2_Controller_TokenControllerInterface { /** * handleTokenRequest @@ -18,8 +18,11 @@ interface OAuth2_Controller_TokenControllerInterface extends OAuth2_Response_Pro * @param $request * OAuth2_RequestInterface - The current http request * - **/ - public function handleTokenRequest(OAuth2_RequestInterface $request); + * @param $response + * OAuth2_ResponseInterface - An instance of OAuth2_ResponseInterface to contain the response data + * + */ + public function handleTokenRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response); - public function grantAccessToken(OAuth2_RequestInterface $request); + public function grantAccessToken(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response); } diff --git a/src/OAuth2/Encryption/JWT.php b/src/OAuth2/Encryption/JWT.php index 2398808..16d3102 100644 --- a/src/OAuth2/Encryption/JWT.php +++ b/src/OAuth2/Encryption/JWT.php @@ -1,9 +1,9 @@ <?php + /** * @link https://github.com/F21/jwt * @author F21 */ - class OAuth2_Encryption_JWT { public function encode($payload, $key, $algo = 'HS256') @@ -33,23 +33,22 @@ class OAuth2_Encryption_JWT list($headb64, $payloadb64, $cryptob64) = $tks; - if (null === ($header = json_decode($this->urlsafeB64Decode($headb64)))){ + if (null === ($header = json_decode($this->urlsafeB64Decode($headb64), true))) { return false; } - if (null === $payload = json_decode($this->urlsafeB64Decode($payloadb64))){ + if (null === $payload = json_decode($this->urlsafeB64Decode($payloadb64), true)) { return false; } $sig = $this->urlsafeB64Decode($cryptob64); if ($verify) { - - if (empty($header->alg)) { + if (!isset($header['alg'])) { return false; } - if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header->alg)) { + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { return false; } } @@ -66,7 +65,7 @@ class OAuth2_Encryption_JWT return $this->sign($input, $key, $algo) === $signature; case 'RS256': - return @openssl_verify($input, $signature, $key, 'sha256') === 1; + return openssl_verify($input, $signature, $key, 'sha256') === 1; case 'RS384': return @openssl_verify($input, $signature, $key, 'sha384') === 1; @@ -82,7 +81,6 @@ class OAuth2_Encryption_JWT private function sign($input, $key, $algo = 'HS256') { switch($algo){ - case 'HS256': return hash_hmac('sha256', $input, $key, true); diff --git a/src/OAuth2/GrantType/AuthorizationCode.php b/src/OAuth2/GrantType/AuthorizationCode.php index 6067c5a..55b8fba 100644 --- a/src/OAuth2/GrantType/AuthorizationCode.php +++ b/src/OAuth2/GrantType/AuthorizationCode.php @@ -1,13 +1,13 @@ <?php /** -* -*/ -class OAuth2_GrantType_AuthorizationCode implements OAuth2_GrantTypeInterface, OAuth2_Response_ProviderInterface + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class OAuth2_GrantType_AuthorizationCode implements OAuth2_GrantTypeInterface { private $storage; - private $response; - private $code; + private $authCode; public function __construct(OAuth2_Storage_AuthorizationCodeInterface $storage) { @@ -19,65 +19,68 @@ class OAuth2_GrantType_AuthorizationCode implements OAuth2_GrantTypeInterface, O return 'authorization_code'; } - public function validateRequest($request) + public function validateRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { if (!$request->request('code')) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Missing parameter: "code" is required'); + $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); return false; } - return true; - } - - public function getTokenDataFromRequest($request) - { - $this->code = $request->request('code'); - if (!$tokenData = $this->storage->getAuthorizationCode($this->code)) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Authorization code doesn't exist or is invalid for the client"); - return null; + $code = $request->request('code'); + if (!$authCode = $this->storage->getAuthorizationCode($code)) { + $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); + return false; } /* * 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 */ - if (isset($tokenData['redirect_uri']) && $tokenData['redirect_uri']) { - if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $tokenData['redirect_uri']) { - $this->response = new OAuth2_Response_Error(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) { + $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); return false; } } - return $tokenData; - } + if (!isset($authCode['expires'])) { + throw new Exception('Storage must return authcode with a value for "expires"'); + } - public function validateTokenData($tokenData, array $clientData) - { - // Check the code exists - if ($tokenData === null || $clientData['client_id'] != $tokenData['client_id']) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Authorization code doesn't exist or is invalid for the client"); + if ($authCode["expires"] < time()) { + $response->setError(400, 'invalid_grant', "The authorization code has expired"); return false; } - if ($tokenData["expires"] < time()) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "The authorization code has expired"); - return false; + if (!isset($authCode['code'])) { + $authCode['code'] = $code; // used to expire the code after the access token is granted } - // Scope is validated in the client class + $this->authCode = $authCode; + return true; } - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData) + public function getClientId() { - $this->storage->expireAuthorizationCode($this->code); - $token = $accessToken->createAccessToken($clientData['client_id'], $tokenData['user_id'], $tokenData['scope']); + return $this->authCode['client_id']; + } - return $token; + public function getScope() + { + return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; } - public function getResponse() + public function getUserId() { - return $this->response; + return $this->authCode['user_id']; + } + + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $token = $accessToken->createAccessToken($client_id, $user_id, $scope); + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; } } diff --git a/src/OAuth2/GrantType/ClientCredentials.php b/src/OAuth2/GrantType/ClientCredentials.php index d518414..ab37409 100644 --- a/src/OAuth2/GrantType/ClientCredentials.php +++ b/src/OAuth2/GrantType/ClientCredentials.php @@ -1,40 +1,33 @@ <?php -class OAuth2_GrantType_ClientCredentials implements OAuth2_GrantTypeInterface +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class OAuth2_GrantType_ClientCredentials extends OAuth2_ClientAssertionType_HttpBasic implements OAuth2_GrantTypeInterface { public function getQuerystringIdentifier() { return 'client_credentials'; } - public function validateRequest($request) + public function getScope() { - // This has been done in the server class - return true; + return null; } - public function getTokenDataFromRequest($request) + public function getUserId() { - // the only piece to pull is the "scope" parameter - $scope = $request->request('scope'); - return array( - "scope" => $scope - ); + return null; } - public function validateTokenData($tokenData, array $clientData) - { - // Scope is validated in the client class - return true; - } - - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData) + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { /* * Client Credentials Grant does NOT include a refresh token * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 */ $includeRefreshToken = false; - return $accessToken->createAccessToken($clientData['client_id'], $tokenData['user_id'], $tokenData['scope'], $includeRefreshToken); + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); } } diff --git a/src/OAuth2/GrantType/JWTBearer.php b/src/OAuth2/GrantType/JWTBearer.php index 705856a..6ba2521 100644 --- a/src/OAuth2/GrantType/JWTBearer.php +++ b/src/OAuth2/GrantType/JWTBearer.php @@ -1,17 +1,19 @@ <?php /** -* The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft. -* @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4 -*/ -class OAuth2_GrantType_JWTBearer implements OAuth2_GrantTypeInterface, OAuth2_Response_ProviderInterface, OAuth2_ClientAssertionTypeInterface + * The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4 + * + * @author F21 + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class OAuth2_GrantType_JWTBearer implements OAuth2_GrantTypeInterface, OAuth2_ClientAssertionTypeInterface { private $storage; - private $response; - private $audience = null; - private $jwt = null; - private $undecodedJWT = null; + private $audience; private $jwtUtil; + private $jwt; /** * Creates an instance of the JWT bearer grant type. @@ -23,7 +25,7 @@ class OAuth2_GrantType_JWTBearer implements OAuth2_GrantTypeInterface, OAuth2_Re * @param OAuth2_Encryption_JWT OPTIONAL $jwtUtil * The class used to decode, encode and verify JWTs. */ - public function __construct(OAuth2_Storage_JWTBearerInterface $storage, $audience, OAuth2_Encryption_JWT $jwtUtil = null) + public function __construct(OAuth2_Storage_JWTBearerInterface $storage, $audience, OAuth2_Encryption_JWT $jwtUtil = null) { $this->storage = $storage; $this->audience = $audience; @@ -37,7 +39,10 @@ class OAuth2_GrantType_JWTBearer implements OAuth2_GrantTypeInterface, OAuth2_Re /** * 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 + * The string identifier for grant_type. + * * @see OAuth2_GrantTypeInterface::getQuerystringIdentifier() */ public function getQuerystringIdentifier() @@ -46,240 +51,137 @@ class OAuth2_GrantType_JWTBearer implements OAuth2_GrantTypeInterface, OAuth2_Re } /** - * Validates the request by making share all GET parameters exists. - * @return TRUE if the request is valid, otherwise FALSE. - * @see OAuth2_GrantTypeInterface::validateRequest() + * 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_GrantTypeInterface::getTokenData() */ - public function validateRequest($request) + public function validateRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { if (!$request->request("assertion")) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Missing parameters: "assertion" required'); - - return false; + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); + return null; } - return true; - } - - /** - * Gets the data from the decoded JWT. - * @return Array containing the token data if the JWT can be decoded. Otherwise, NULL is returned. - * @see OAuth2_GrantTypeInterface::getTokenDataFromRequest() - */ - public function getTokenDataFromRequest($request) - { - if (!$request->request("assertion")) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Missing parameters: "assertion" required'); - + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); return null; } - //Store the undecoded JWT for later use - $this->undecodedJWT = $request->request('assertion'); + // Store the undecoded JWT for later use + $undecodedJWT = $request->request('assertion'); - //Decode the JWT + // Decode the JWT $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false); if (!$jwt) { - - $this->response = new OAuth2_Response_Error(400, 'invalid_request', "JWT is malformed"); + $response->setError(400, 'invalid_request', "JWT is malformed"); return null; } - $this->jwt = $jwt; - - $tokenData = array(); - - $tokenData['scope'] = $this->getJWTParameter('scope'); - $tokenData['iss'] = $this->getJWTParameter('iss'); - $tokenData['sub'] = $this->getJWTParameter('sub'); - $tokenData['aud'] = $this->getJWTParameter('aud'); - $tokenData['exp'] = $this->getJWTParameter('exp'); - $tokenData['nbf'] = $this->getJWTParameter('nbf'); - $tokenData['iat'] = $this->getJWTParameter('iat'); - $tokenData['jti'] = $this->getJWTParameter('jti'); - $tokenData['typ'] = $this->getJWTParameter('typ'); - - //Other token data in the claim - foreach ($this->jwt as $key => $value) { - if (!array_key_exists($key, $tokenData)) { - $tokenData[$key] = $value; - } - } - - return $tokenData; - } - - - /** - * Helper function to make it easier to return a JWT parameter. - * @param string $parameter - * The JWT parameter to get. - * @param mixed $default - * The value to return if the JWT parameter does not exist. - * @return mixed - * The JWT parameter. - */ - private function getJWTParameter($parameter, $default = null) - { - return isset($this->jwt->$parameter) ? $this->jwt->$parameter : null; - } - - /** - * Return the data used to verify the request. For JWT bearer authorization grants, the 'iss' is synonymous to the 'client_id'. - * The subject is 'sub' and the 'client_secret' is the key to decode the JWT. - * @return array An array of the client data containing the client_id. - * @see OAuth2_ClientAssertionTypeInterface::getClientDataFromRequest() - */ - public function getClientDataFromRequest(OAuth2_RequestInterface $request) - { - $tokenData = $this->getTokenDataFromRequest($request); - - if (!$tokenData) { + // ensure these properties contain a value + // @todo: throw malformed error for missing properties + $jwt = array_merge(array( + 'scope' => null, + 'iss' => null, + 'sub' => null, + 'aud' => null, + 'exp' => null, + 'nbf' => null, + 'iat' => null, + 'jti' => null, + 'typ' => null, + ), $jwt); + + if (!isset($jwt['iss'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided"); return null; } - if (!isset($tokenData['iss'])) { - - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Invalid issuer (iss) provided"); - + if (!isset($jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided"); return null; } - //Get the iss's public key (http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1) - $key = $this->storage->getClientKey($tokenData['iss'], $tokenData['sub']); - - if (!$key) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); - + if (!isset($jwt['exp'])) { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present"); return null; } - return array('client_id' => $tokenData['iss'], 'subject' => $tokenData['sub'], 'client_secret' => $key); - } - - /** - * Validates the client data by checking if the client_id exists in storage. It also checks to see if the client_id is associated with a key to decode the JWT. - * @see OAuth2_ClientAssertionTypeInterface::validateClientData() - */ - public function validateClientData(array $clientData, $grantTypeIdentifier) - { - - //Check that all array keys exist - $diff = array_diff(array_keys($clientData), array('client_id', 'subject', 'client_secret')); - - if (!empty($diff)) { - - throw new DomainException('The clientData array is missing one or more of the following: client_id, subject, client_secret'); - - return false; - } - - foreach ($clientData as $key => $value) { - - if (in_array($key, array('client_id', 'client_secret'))) { - - if (!isset($value)) { - throw new LogicException('client_id and client_secret in the clientData array may not be null'); - - return false; - } - } - } - - return true; - } - - /** - * Validates the token data using the rules in the IETF draft. - * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-3 - * @see OAuth2_GrantTypeInterface::validateTokenData() - */ - public function validateTokenData($tokenData, array $clientData) - { - // Note: Scope is validated in the client class - - //Check the expiry time - $expiration = $tokenData['exp']; - - if (ctype_digit($expiration)) { - - if ($expiration <= time()) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "JWT has expired"); - - return false; + // Check expiration + if (ctype_digit($jwt['exp'])) { + if ($jwt['exp'] <= time()) { + $response->setError(400, 'invalid_grant', "JWT has expired"); + return null; } - - } elseif (!$expiration) { - - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Expiration (exp) time must be present"); - - return false; - } else { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); - - return false; + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); + return null; } - - //Check the not before time - if ($notBefore = $tokenData['nbf']) { - + // Check the not before time + if ($notBefore = $jwt['nbf']) { if (ctype_digit($notBefore)) { - if ($notBefore > time()) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); - - return false; + $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); + return null; } - } else { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); - - return false; + $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); + return null; } } - //Check the audience if required to match - $aud = $tokenData['aud']; - if (!isset($aud) || ($aud != $this->audience)) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "Invalid audience (aud)"); - - return false; + // Check the audience if required to match + if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) { + $response->setError(400, 'invalid_grant', "Invalid audience (aud)"); + return null; } - //Verify the JWT - $jwt = $this->jwtUtil->decode($this->undecodedJWT, $clientData['client_secret'], true); - - if (!$jwt) { + // Get the iss's public key + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1 + if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); + return null; + } - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', "JWT failed signature verification"); + // Verify the JWT + if (!$this->jwtUtil->decode($undecodedJWT, $key, true)) { + $response->setError(400, 'invalid_grant', "JWT failed signature verification"); return null; } + $this->jwt = $jwt; + return true; } - /** - * 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_GrantTypeInterface::createAccessToken() - */ - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData) + public function getClientId() { - $includeRefreshToken = false; + return $this->jwt['iss']; + } + + public function getUserId() + { + return $this->jwt['sub']; + } - return $accessToken->createAccessToken($clientData['client_id'], $tokenData['sub'], $tokenData['scope'], $includeRefreshToken); + public function getScope() + { + return $this->jwt['scope']; } /** - * Returns the response of this grant type. - * @see OAuth2_Response_ProviderInterface::getResponse() + * 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_GrantTypeInterface::createAccessToken() */ - public function getResponse() + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { - return $this->response; + $includeRefreshToken = false; + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); } } diff --git a/src/OAuth2/GrantType/RefreshToken.php b/src/OAuth2/GrantType/RefreshToken.php index 2f5336a..f98ed84 100644 --- a/src/OAuth2/GrantType/RefreshToken.php +++ b/src/OAuth2/GrantType/RefreshToken.php @@ -1,14 +1,14 @@ <?php /** -* -*/ -class OAuth2_GrantType_RefreshToken implements OAuth2_GrantTypeInterface, OAuth2_Response_ProviderInterface + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class OAuth2_GrantType_RefreshToken implements OAuth2_GrantTypeInterface { private $storage; - private $response; private $config; - private $oldRefreshToken; + private $refreshToken; public function __construct(OAuth2_Storage_RefreshTokenInterface $storage, $config = array()) { @@ -23,45 +23,45 @@ class OAuth2_GrantType_RefreshToken implements OAuth2_GrantTypeInterface, OAuth2 return 'refresh_token'; } - public function validateRequest($request) + public function validateRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { if (!$request->request("refresh_token")) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); - return false; + $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); + return null; } - return true; - } + if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) { + $response->setError(400, 'invalid_grant', 'Invalid refresh token'); + return null; + } - public function getTokenDataFromRequest($request) - { - if (!$stored = $this->storage->getRefreshToken($request->request("refresh_token"))) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', 'Invalid refresh token'); - return false; + if ($refreshToken["expires"] < time()) { + $response->setError(400, 'invalid_grant', 'Refresh token has expired'); + return null; } - return $stored; + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->refreshToken = $refreshToken; + + return true; } - public function validateTokenData($tokenData, array $clientData) + public function getClientId() { - if ($tokenData === null || $clientData['client_id'] != $tokenData["client_id"]) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', 'Invalid refresh token'); - return false; - } - - if ($tokenData["expires"] < time()) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', 'Refresh token has expired'); - return false; - } + return $this->refreshToken['client_id']; + } - // store the refresh token locally so we can delete it when a new refresh token is generated - $this->oldRefreshToken = $tokenData["refresh_token"]; + public function getUserId() + { + return $this->refreshToken['user_id']; + } - return true; + public function getScope() + { + return $this->refreshToken['scope']; } - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData) + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { /* * It is optional to force a new refresh token when a refresh token is used. @@ -69,17 +69,12 @@ class OAuth2_GrantType_RefreshToken implements OAuth2_GrantTypeInterface, OAuth2 * @see http://tools.ietf.org/html/rfc6749#section-6 */ $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; - $token = $accessToken->createAccessToken($clientData['client_id'], $tokenData['user_id'], $tokenData['scope'], $issueNewRefreshToken); + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); if ($issueNewRefreshToken) { - $this->storage->unsetRefreshToken($this->oldRefreshToken); + $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); } return $token; } - - public function getResponse() - { - return $this->response; - } } diff --git a/src/OAuth2/GrantType/UserCredentials.php b/src/OAuth2/GrantType/UserCredentials.php index 0d4638e..c41e8db 100644 --- a/src/OAuth2/GrantType/UserCredentials.php +++ b/src/OAuth2/GrantType/UserCredentials.php @@ -1,12 +1,13 @@ <?php /** -* -*/ -class OAuth2_GrantType_UserCredentials implements OAuth2_GrantTypeInterface, OAuth2_Response_ProviderInterface + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class OAuth2_GrantType_UserCredentials implements OAuth2_GrantTypeInterface { private $storage; - private $response; + private $userInfo; public function __construct(OAuth2_Storage_UserCredentialsInterface $storage) { @@ -18,47 +19,48 @@ class OAuth2_GrantType_UserCredentials implements OAuth2_GrantTypeInterface, OAu return 'password'; } - public function validateRequest($request) + public function validateRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { if (!$request->request("password") || !$request->request("username")) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); - return false; + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + return null; } - return true; - } - - public function getTokenDataFromRequest($request) - { if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', 'Invalid username and password combination'); - return false; + $response->setError(400, 'invalid_grant', 'Invalid username and password combination'); + return null; } - $tokenData = $this->storage->getUserDetails($request->request("username")); + $userInfo = $this->storage->getUserDetails($request->request("username")); - // tokenData can be an empty array - if (false === $tokenData || is_null($tokenData)) { - $this->response = new OAuth2_Response_Error(400, 'invalid_grant', 'Unable to retrieve user information'); - return false; + // userInfo can be an empty array + if (false === $userInfo || is_null($userInfo)) { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + return null; } - return $tokenData; + $this->userInfo = $userInfo; + + return true; } - public function validateTokenData($tokenData, array $clientData) + public function getClientId() { - // Scope is validated in the client class - return true; + return null; + } + + public function getUserId() + { + return $this->userInfo['user_id']; } - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData) + public function getScope() { - return $accessToken->createAccessToken($clientData['client_id'], $tokenData['user_id'], $tokenData['scope']); + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; } - public function getResponse() + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { - return $this->response; + return $accessToken->createAccessToken($client_id, $user_id, $scope); } } diff --git a/src/OAuth2/GrantTypeInterface.php b/src/OAuth2/GrantTypeInterface.php index c029a1d..9bc2327 100644 --- a/src/OAuth2/GrantTypeInterface.php +++ b/src/OAuth2/GrantTypeInterface.php @@ -1,10 +1,13 @@ <?php -interface OAuth2_GrantTypeInterface +/** + * Interface for all OAuth2 Grant Types + * + * @see OAuth2_CompatibilityInterface + */ +interface OAuth2_GrantTypeInterface extends OAuth2_CompatibilityInterface { - public function getQuerystringIdentifier(); - public function validateRequest($request); - public function getTokenDataFromRequest($request); - public function validateTokenData($tokenData, array $clientData); - public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, array $clientData, array $tokenData); + public function getUserId(); + public function getScope(); + public function createAccessToken(OAuth2_ResponseType_AccessTokenInterface $accessToken, $client_id, $user_id, $scope); } diff --git a/src/OAuth2/Request.php b/src/OAuth2/Request.php index 8f2ca3e..063966d 100644 --- a/src/OAuth2/Request.php +++ b/src/OAuth2/Request.php @@ -1,10 +1,10 @@ <?php /** -* OAuth2_Request -* This class is taken from the Symfony2 Framework and is part of the Symfony package. -* See Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony) -*/ + * OAuth2_Request + * This class is taken from the Symfony2 Framework and is part of the Symfony package. + * See Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony) + */ class OAuth2_Request implements OAuth2_RequestInterface { public $attributes; @@ -29,9 +29,9 @@ class OAuth2_Request implements OAuth2_RequestInterface * * @api */ - public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) { - $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); + $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers); } /** @@ -49,7 +49,7 @@ class OAuth2_Request implements OAuth2_RequestInterface * * @api */ - public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) { $this->request = $request; $this->query = $query; @@ -57,7 +57,8 @@ class OAuth2_Request implements OAuth2_RequestInterface $this->cookies = $cookies; $this->files = $files; $this->server = $server; - $this->headers = $this->getHeadersFromServer($this->server); + $this->content = $content; + $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; } public function query($name, $default = null) diff --git a/src/OAuth2/Response.php b/src/OAuth2/Response.php index bc88f2e..4ef801a 100644 --- a/src/OAuth2/Response.php +++ b/src/OAuth2/Response.php @@ -1,9 +1,15 @@ <?php /** -* -*/ -class OAuth2_Response + * Class to handle OAuth2 Responses in a graceful way. Use this interface + * to output the proper OAuth2 responses. + * + * @see OAuth2_ResponseInterface + * + * This class borrows heavily from the Symfony2 Framework and is part of the symfony package + * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony) + */ +class OAuth2_Response implements OAuth2_ResponseInterface { public $version; protected $statusCode = 200; @@ -119,10 +125,16 @@ class OAuth2_Response return $this->parameters; } - public function setParameters($parameters) + public function setParameters(array $parameters) { $this->parameters = $parameters; } + + public function addParameters(array $parameters) + { + $this->parameters = array_merge($this->parameters, $parameters); + } + public function getParameter($name, $default = null) { return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; @@ -133,7 +145,7 @@ class OAuth2_Response $this->parameters[$name] = $value; } - public function setHttpHeaders($httpHeaders) + public function setHttpHeaders(array $httpHeaders) { $this->httpHeaders = $httpHeaders; } @@ -143,6 +155,11 @@ class OAuth2_Response $this->httpHeaders[$name] = $value; } + public function addHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); + } + public function getHttpHeaders() { return $this->httpHeaders; @@ -193,7 +210,67 @@ class OAuth2_Response echo $this->getResponseBody($format); } -// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) + { + $parameters = array( + 'error' => $error, + 'error_description' => $errorDescription, + ); + + if (!is_null($errorUri)) { + if (strlen($errorUri) > 0 && $errorUri[0] == '#') { + // we are referencing an oauth bookmark (for brevity) + $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri; + } + $parameters['error_uri'] = $errorUri; + } + + $httpHeaders = array( + 'Cache-Control' => 'no-store' + ); + + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + $this->addHttpHeaders($httpHeaders); + + if (!$this->isClientError() && !$this->isServerError()) { + throw new InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + } + } + + public function setRedirect($statusCode = 302, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) + { + if (empty($url)) { + throw new InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $parameters = array(); + + if (!is_null($state)) { + $parameters['state'] = $state; + } + + if (!is_null($error)) { + $this->setError(400, $error, $errorDescription, $errorUri); + } + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + + if (count($this->parameters) > 0) { + // add parameters to URL redirection + $parts = parse_url($url); + $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; + $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)); + } + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html /** * @return Boolean * @@ -262,6 +339,7 @@ class OAuth2_Response if (count($headers) == 0) { return ''; } + $max = max(array_map('strlen', array_keys($headers))) + 1; $content = ''; ksort($headers); diff --git a/src/OAuth2/Response/AuthenticationError.php b/src/OAuth2/Response/AuthenticationError.php deleted file mode 100644 index dadd4bf..0000000 --- a/src/OAuth2/Response/AuthenticationError.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -/** -* -*/ -class OAuth2_Response_AuthenticationError extends OAuth2_Response_Error -{ - public function __construct($statusCode, $error, $errorDescription, $tokenType, $realm, $scope = null) - { - parent::__construct($statusCode, $error, $errorDescription); - $authHeader = sprintf('%s realm="%s"', $tokenType, $realm); - if ($scope) { - $authHeader = sprintf('%s, scope="%s"', $authHeader, $scope); - } - $this->setHttpHeader('WWW-Authenticate', $authHeader); - } -} diff --git a/src/OAuth2/Response/Error.php b/src/OAuth2/Response/Error.php deleted file mode 100644 index 305abd4..0000000 --- a/src/OAuth2/Response/Error.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -/** -* -*/ -class OAuth2_Response_Error extends OAuth2_Response -{ - public function __construct($statusCode, $error, $errorDescription, $errorUri = null) - { - $parameters = array( - 'error' => $error, - 'error_description' => $errorDescription, - ); - - if (!is_null($errorUri)) { - if (strlen($errorUri) > 0 && $errorUri[0] == '#') { - // we are referencing an oauth bookmark (for brevity) - $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri; - } - $parameters['error_uri'] = $errorUri; - } - - $httpHeaders = array( - 'Cache-Control' => 'no-store' - ); - - parent::__construct($parameters, $statusCode, $httpHeaders); - - if (!$this->isClientError() && !$this->isServerError()) { - throw new InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); - } - } - - public function getError() - { - return $this->parameters['error']; - } - - public function getErrorDescription() - { - return $this->parameters['error_description']; - } - - public function getErrorUri() - { - return $this->parameters['error_uri']; - } -} diff --git a/src/OAuth2/Response/ProviderInterface.php b/src/OAuth2/Response/ProviderInterface.php deleted file mode 100644 index 9b147c0..0000000 --- a/src/OAuth2/Response/ProviderInterface.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php - -interface OAuth2_Response_ProviderInterface -{ - public function getResponse(); -} diff --git a/src/OAuth2/Response/Redirect.php b/src/OAuth2/Response/Redirect.php deleted file mode 100644 index 86ec291..0000000 --- a/src/OAuth2/Response/Redirect.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -/** -* -*/ -class OAuth2_Response_Redirect extends OAuth2_Response -{ - public function __construct($url, $statusCode = 302, $error = null, $errorDescription = null, $state = null) - { - if (empty($url)) { - throw new InvalidArgumentException('Cannot redirect to an empty URL.'); - } - - $query = array(); - - if (!is_null($error)) { - $query['error'] = $error; - } - - if (!is_null($errorDescription)) { - $query['error_description'] = $errorDescription; - } - - if (!is_null($state)) { - $query['state'] = $state; - } - - if (count($query) > 0) { - $parts = parse_url($url); - $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; - $url = $url . $sep . http_build_query($query); - } - - $httpHeaders = array( - 'Location' => $url, - ); - - parent::__construct(array(), $statusCode, $httpHeaders); - - if (!$this->isRedirection()) { - throw new InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); - } - } -} diff --git a/src/OAuth2/ResponseInterface.php b/src/OAuth2/ResponseInterface.php new file mode 100644 index 0000000..4aa32cd --- /dev/null +++ b/src/OAuth2/ResponseInterface.php @@ -0,0 +1,20 @@ +<?php + +/** + * Interface which represents an object response. Meant to handle and display the proper OAuth2 Responses + * for errors and successes + * + * @see OAuth2_Response + */ +interface OAuth2_ResponseInterface +{ + public function addParameters(array $parameters); + + public function addHttpHeaders(array $httpHeaders); + + public function setStatusCode($statusCode); + + public function setError($statusCode, $name, $description = null, $uri = null); + + public function setRedirect($statusCode = 302, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null); +}
\ No newline at end of file diff --git a/src/OAuth2/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php index 30beaf6..b92ead9 100644 --- a/src/OAuth2/ResponseType/AccessToken.php +++ b/src/OAuth2/ResponseType/AccessToken.php @@ -1,8 +1,9 @@ <?php /** -* -*/ + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ class OAuth2_ResponseType_AccessToken implements OAuth2_ResponseType_AccessTokenInterface { private $tokenStorage; @@ -20,7 +21,6 @@ class OAuth2_ResponseType_AccessToken implements OAuth2_ResponseType_AccessToken ), $config); } - // same params as above public function getAuthorizeResponse($params, $user_id = null) { // build the URL to redirect to @@ -44,17 +44,16 @@ class OAuth2_ResponseType_AccessToken implements OAuth2_ResponseType_AccessToken } /** - * Handle the creation of access token, also issue refresh token if support. - * - * This belongs in a separate factory, but to keep it simple, I'm just - * keeping it here. + * 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 $excludeRefreshToken - * If true, the refresh_token will be omitted from the response + * @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 diff --git a/src/OAuth2/ResponseType/AccessTokenInterface.php b/src/OAuth2/ResponseType/AccessTokenInterface.php index d362893..19180b9 100644 --- a/src/OAuth2/ResponseType/AccessTokenInterface.php +++ b/src/OAuth2/ResponseType/AccessTokenInterface.php @@ -1,6 +1,25 @@ <?php +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ interface OAuth2_ResponseType_AccessTokenInterface extends OAuth2_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 + * (optional) 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); } diff --git a/src/OAuth2/ResponseType/AuthorizationCode.php b/src/OAuth2/ResponseType/AuthorizationCode.php index d8a2626..84f2eeb 100644 --- a/src/OAuth2/ResponseType/AuthorizationCode.php +++ b/src/OAuth2/ResponseType/AuthorizationCode.php @@ -1,8 +1,9 @@ <?php /** -* -*/ + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ class OAuth2_ResponseType_AuthorizationCode implements OAuth2_ResponseType_AuthorizationCodeInterface { private $storage; @@ -17,7 +18,7 @@ class OAuth2_ResponseType_AuthorizationCode implements OAuth2_ResponseType_Autho ), $config); } - // same params as above + public function getAuthorizeResponse($params, $user_id = null) { // build the URL to redirect to @@ -35,19 +36,19 @@ class OAuth2_ResponseType_AuthorizationCode implements OAuth2_ResponseType_Autho } /** - * Handle the creation of auth code. - * - * This belongs in a separate factory, but to keep it simple, I'm just - * keeping it here. + * Handle the creation of the authorization code. * * @param $client_id - * Client identifier related to the access token. + * 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. * + * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 */ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) @@ -57,6 +58,10 @@ class OAuth2_ResponseType_AuthorizationCode implements OAuth2_ResponseType_Autho return $code; } + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ public function enforceRedirect() { return $this->config['enforce_redirect']; diff --git a/src/OAuth2/ResponseType/AuthorizationCodeInterface.php b/src/OAuth2/ResponseType/AuthorizationCodeInterface.php index f139ae3..6a791b3 100644 --- a/src/OAuth2/ResponseType/AuthorizationCodeInterface.php +++ b/src/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -1,5 +1,9 @@ <?php +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ interface OAuth2_ResponseType_AuthorizationCodeInterface extends OAuth2_ResponseTypeInterface { /** @@ -8,5 +12,21 @@ interface OAuth2_ResponseType_AuthorizationCodeInterface extends OAuth2_Response */ 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. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null); } diff --git a/src/OAuth2/ScopeInterface.php b/src/OAuth2/ScopeInterface.php index 63fb906..bab9ac6 100644 --- a/src/OAuth2/ScopeInterface.php +++ b/src/OAuth2/ScopeInterface.php @@ -1,8 +1,10 @@ <?php /** -* Class to handle scope implementation logic -*/ + * Class to handle scope implementation logic + * + * @see OAuth2_Storage_ScopeInterface + */ interface OAuth2_ScopeInterface extends OAuth2_Storage_ScopeInterface { /** diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index be115b7..c730607 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -1,8 +1,9 @@ <?php /** -* Service class for OAuth -* This class serves only to wrap the other Controller classes +* 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 @@ -16,15 +17,15 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, protected $storages; // servers - protected $resourceController; protected $authorizeController; protected $tokenController; + protected $resourceController; // config classes protected $responseTypes; protected $grantTypes; - protected $accessTokenResponseType; protected $scopeUtil; + protected $clientAssertionType; protected $storageMap = array( 'access_token' => 'OAuth2_Storage_AccessTokenInterface', @@ -34,6 +35,7 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, 'refresh_token' => 'OAuth2_Storage_RefreshTokenInterface', 'user_credentials' => 'OAuth2_Storage_UserCredentialsInterface', 'jwt_bearer' => 'OAuth2_Storage_JWTBearerInterface', + 'scope' => 'OAuth2_Storage_ScopeInterface', ); protected $responseTypeMap = array( 'token' => 'OAuth2_ResponseType_AccessTokenInterface', @@ -55,8 +57,14 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, * Response types to use. array keys should be "code" and and "token" for * Access Token and Authorization Code response types * - * @param OAuth2_ResponseType_AccessTokenInterface $accessTokenResponseType - * Response type to use for access token + * @param OAuth2_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_ClientAssertionTypeInterface $clientAssertionType + * The method in which to verify the client identity. Default is HttpBasic * * @return * TRUE if everything in required scope is contained in available scope, @@ -66,7 +74,7 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, * * @ingroup oauth2_section_7 */ - public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), OAuth2_ResponseType_AccessTokenInterface $accessTokenResponseType = null, OAuth2_ScopeInterface $scopeUtil = null) + public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), OAuth2_TokenTypeInterface $tokenType = null, OAuth2_ScopeInterface $scopeUtil = null, OAuth2_ClientAssertionTypeInterface $clientAssertionType = null) { $storage = is_array($storage) ? $storage : array($storage); $this->storages = array(); @@ -76,7 +84,6 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, // merge all config values. These get passed to our controller objects $this->config = array_merge(array( - 'token_type' => 'bearer', 'access_lifetime' => 3600, 'www_realm' => 'Service', 'token_param_name' => 'access_token', @@ -85,51 +92,21 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, 'allow_implicit' => false, ), $config); - foreach ($responseTypes as $key => $responseType) { - $this->addResponseType($responseType, $key); - } foreach ($grantTypes as $key => $grantType) { $this->addGrantType($grantType, $key); } - $this->accessTokenResponseType = $accessTokenResponseType; - $this->scopeUtil = $scopeUtil; - } - - public function getResourceController() - { - if (is_null($this->resourceController)) { - if (is_null($this->config['token_type'])) { - $this->config['token_type'] = 'bearer'; - } - $tokenType = null; - if ($this->config['token_type'] == 'bearer') { - $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); - $tokenType = new OAuth2_TokenType_Bearer($config); - } elseif ($this->config['token_type'] == 'mac') { - $tokenType = new OAuth2_TokenType_MAC(); - } else { - throw new LogicException('unrecognized token type: '.$this->config['token_type']); - } - if (!isset($this->storages['access_token'])) { - throw new LogicException("You must supply a storage object implementing OAuth2_Storage_AccessTokenInterface to use the access server"); - } - $config = array_intersect_key($this->config, array('www_realm' => '')); - $this->resourceController = new OAuth2_Controller_ResourceController($tokenType, $this->storages['access_token'], $config, $this->scopeUtil); + foreach ($responseTypes as $key => $responseType) { + $this->addResponseType($responseType, $key); } - return $this->resourceController; + $this->tokenType = $tokenType; + $this->scopeUtil = $scopeUtil; + $this->clientAssertionType = $clientAssertionType; } public function getAuthorizeController() { if (is_null($this->authorizeController)) { - if (!isset($this->storages['client'])) { - 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(); - } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state'))); - $this->authorizeController = new OAuth2_Controller_AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->scopeUtil); + $this->authorizeController = $this->createDefaultAuthorizeController(); } return $this->authorizeController; } @@ -137,81 +114,41 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, public function getTokenController() { if (is_null($this->tokenController)) { - if (!isset($this->storages['client_credentials'])) { - throw new LogicException("You must supply a storage object implementing OAuth2_Storage_ClientCredentialsInterface to use the grant server"); - } - if (is_null($this->accessTokenResponseType)) { - if (isset($this->responseTypes['access_token'])) { - $this->accessTokenResponseType = $this->responseTypes['access_token']; - } else { - if (!isset($this->storages['access_token'])) { - throw new LogicException("You must supply a storage object implementing OAuth2_Storage_AccessTokenInterface to use the grant server"); - } - $refreshStorage = null; - if (isset($this->storages['refresh_token'])) { - $refreshStorage = $this->storages['refresh_token']; - } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_type access_lifetime refresh_token_lifetime'))); - $this->accessTokenResponseType = new OAuth2_ResponseType_AccessToken($this->storages['access_token'], $refreshStorage, $config); - } - } - if (0 == count($this->grantTypes)) { - $this->grantTypes = $this->getDefaultGrantTypes(); - } - $this->tokenController = new OAuth2_Controller_TokenController($this->storages['client_credentials'], $this->accessTokenResponseType, $this->grantTypes, $this->scopeUtil); + $this->tokenController = $this->createDefaultTokenController(); } return $this->tokenController; } - protected function getDefaultResponseTypes() + public function getResourceController() { - $responseTypes = array(); - if (isset($this->storages['access_token'])) { - $refreshStorage = null; - if (isset($this->storages['refresh_token'])) { - $refreshStorage = $this->storages['refresh_token']; - } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_type access_lifetime refresh_token_lifetime'))); - $responseTypes['token'] = new OAuth2_ResponseType_AccessToken($this->storages['access_token'], $refreshStorage, $config); - } - - if (isset($this->storages['authorization_code'])) { - $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); - $responseTypes['code'] = new OAuth2_ResponseType_AuthorizationCode($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_AccessTokenInterface or OAuth2_Storage_AuthorizationCodeInterface storage object"); + if (is_null($this->resourceController)) { + $this->resourceController = $this->createDefaultResourceController(); } - - return $responseTypes; + return $this->resourceController; } - protected function getDefaultGrantTypes() + /** + * every getter deserves a setter + */ + public function setAuthorizeController(OAuth2_Controller_AuthorizeControllerInterface $authorizeController) { - $grantTypes = array(); - - if (isset($this->storages['user_credentials'])) { - $grantTypes['password'] = new OAuth2_GrantType_UserCredentials($this->storages['user_credentials']); - } - - if (isset($this->storages['client_credentials'])) { - $grantTypes['client_credentials'] = new OAuth2_GrantType_ClientCredentials($this->storages['client_credentials']); - } - - if (isset($this->storages['refresh_token'])) { - $grantTypes['refresh_token'] = new OAuth2_GrantType_RefreshToken($this->storages['refresh_token']); - } - - if (isset($this->storages['authorization_code'])) { - $grantTypes['authorization_code'] = new OAuth2_GrantType_AuthorizationCode($this->storages['authorization_code']); - } + $this->authorizeController = $authorizeController; + } - if (count($grantTypes) == 0) { - throw new LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); - } + /** + * every getter deserves a setter + */ + public function setTokenController(OAuth2_Controller_TokenControllerInterface $tokenController) + { + $this->tokenController = $tokenController; + } - return $grantTypes; + /** + * every getter deserves a setter + */ + public function setResourceController(OAuth2_Controller_ResourceControllerInterface $resourceController) + { + $this->resourceController = $resourceController; } /** @@ -234,24 +171,17 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, * * @ingroup oauth2_section_4 */ - public function handleTokenRequest(OAuth2_RequestInterface $request) - { - $value = $this->getTokenController()->handleTokenRequest($request); - $this->response = $this->tokenController->getResponse(); - return $value; - } - - public function grantAccessToken(OAuth2_RequestInterface $request) + public function handleTokenRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - $value = $this->getTokenController()->grantAccessToken($request); - $this->response = $this->tokenController->getResponse(); - return $value; + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $this->getTokenController()->handleTokenRequest($request, $this->response); + return $this->response; } - public function getClientCredentials(OAuth2_RequestInterface $request) + public function grantAccessToken(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - $value = $this->getTokenController()->getClientCredentials($request); - $this->response = $this->tokenController->getResponse(); + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $value = $this->getTokenController()->grantAccessToken($request, $this->response); return $value; } @@ -285,11 +215,11 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, * * @ingroup oauth2_section_4 */ - public function handleAuthorizeRequest(OAuth2_RequestInterface $request, $is_authorized, $user_id = null) + public function handleAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response, $is_authorized, $user_id = null) { - $value = $this->getAuthorizeController()->handleAuthorizeRequest($request, $is_authorized, $user_id); - $this->response = $this->authorizeController->getResponse(); - return $value; + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id); + return $this->response; } /** @@ -311,24 +241,24 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, * * @ingroup oauth2_section_3 */ - public function validateAuthorizeRequest(OAuth2_RequestInterface $request) + public function validateAuthorizeRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - $value = $this->getAuthorizeController()->validateAuthorizeRequest($request); - $this->response = $this->authorizeController->getResponse(); + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response); return $value; } - public function verifyResourceRequest(OAuth2_RequestInterface $request, $scope = null) + public function verifyResourceRequest(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response, $scope = null) { - $value = $this->getResourceController()->verifyResourceRequest($request, $scope); - $this->response = $this->resourceController->getResponse(); + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope); return $value; } - public function getAccessTokenData(OAuth2_RequestInterface $request, $scope = null) + public function getAccessTokenData(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { - $value = $this->getResourceController()->getAccessTokenData($request, $scope); - $this->response = $this->resourceController->getResponse(); + $this->response = is_null($response) ? new OAuth2_Response() : $response; + $value = $this->getResourceController()->getAccessTokenData($request, $this->response); return $value; } @@ -346,6 +276,16 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, } } + /** + * 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 + * @see storageMap + */ public function addStorage($storage, $key = null) { // if explicitly set to a valid key, do not "magically" set below @@ -358,9 +298,6 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, throw new InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap)))); } else { $set = false; - // set a storage object to each key for the interface it represents - // this means if an object represents more than one storage type, it will be referenced by multiple storage keys - // ex: OAuth2_Storage_Pdo will be set for all the $storageMap keys foreach ($this->storageMap as $type => $interface) { if ($storage instanceof $interface) { $this->storages[$type] = $storage; @@ -398,11 +335,140 @@ class OAuth2_Server implements OAuth2_Controller_ResourceControllerInterface, } } + public function getScopeUtil() + { + if (!$this->scopeUtil) { + $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null; + $this->scopeUtil = new OAuth2_Scope($storage); + } + return $this->scopeUtil; + } + + /** + * every getter deserves a setter + */ public function setScopeUtil($scopeUtil) { $this->scopeUtil = $scopeUtil; } + 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"); + } + if (0 == count($this->responseTypes)) { + $this->responseTypes = $this->getDefaultResponseTypes(); + } + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state'))); + return new OAuth2_Controller_AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + protected function createDefaultTokenController() + { + if (0 == count($this->grantTypes)) { + $this->grantTypes = $this->getDefaultGrantTypes(); + } + + if (is_null($this->clientAssertionType)) { + // see if HttpBasic assertion type is requred. If so, then create it from storage classes. + foreach ($this->grantTypes as $grantType) { + if (!$grantType instanceof OAuth2_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"); + } + $this->clientAssertionType = new OAuth2_ClientAssertionType_HttpBasic($this->storages['client_credentials']); + break; + } + } + } + + return new OAuth2_Controller_TokenController($this->getAccessTokenResponseType(), $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); + } + + protected function createDefaultResourceController() + { + if (!isset($this->storages['access_token'])) { + throw new LogicException("You must supply a storage object implementing OAuth2_Storage_AccessTokenInterface to use the resource server"); + } + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + $config = array_intersect_key($this->config, array('www_realm' => '')); + return new OAuth2_Controller_ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); + } + + protected function getDefaultTokenType() + { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); + return new OAuth2_TokenType_Bearer($config); + } + + protected function getDefaultResponseTypes() + { + $responseTypes = array(); + + if (isset($this->storages['access_token'])) { + $responseTypes['token'] = $this->getAccessTokenResponseType(); + } + + if (isset($this->storages['authorization_code'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); + $responseTypes['code'] = new OAuth2_ResponseType_AuthorizationCode($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_AccessTokenInterface or OAuth2_Storage_AuthorizationCodeInterface storage object"); + } + + return $responseTypes; + } + + protected function getDefaultGrantTypes() + { + $grantTypes = array(); + + if (isset($this->storages['user_credentials'])) { + $grantTypes['password'] = new OAuth2_GrantType_UserCredentials($this->storages['user_credentials']); + } + + if (isset($this->storages['client_credentials'])) { + $grantTypes['client_credentials'] = new OAuth2_GrantType_ClientCredentials($this->storages['client_credentials']); + } + + if (isset($this->storages['refresh_token'])) { + $grantTypes['refresh_token'] = new OAuth2_GrantType_RefreshToken($this->storages['refresh_token']); + } + + if (isset($this->storages['authorization_code'])) { + $grantTypes['authorization_code'] = new OAuth2_GrantType_AuthorizationCode($this->storages['authorization_code']); + } + + if (count($grantTypes) == 0) { + throw new LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); + } + + return $grantTypes; + } + + protected function getAccessTokenResponseType() + { + if (isset($this->responseTypes['access_token'])) { + return $this->responseTypes['access_token']; + } + 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"); + } + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime'))); + $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() : $this->getDefaultTokenType()->getTokenType(); + + return new OAuth2_ResponseType_AccessToken($this->storages['access_token'], $refreshStorage, $config); + } + public function getResponse() { return $this->response; diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php index 4bbad84..6454ab1 100644 --- a/src/OAuth2/Storage/AccessTokenInterface.php +++ b/src/OAuth2/Storage/AccessTokenInterface.php @@ -4,7 +4,7 @@ * Implement this interface to specify where the OAuth2 Server * should get/save access tokens * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_AccessTokenInterface { diff --git a/src/OAuth2/Storage/AuthorizationCodeInterface.php b/src/OAuth2/Storage/AuthorizationCodeInterface.php index 71fa5a8..16a055d 100644 --- a/src/OAuth2/Storage/AuthorizationCodeInterface.php +++ b/src/OAuth2/Storage/AuthorizationCodeInterface.php @@ -5,7 +5,7 @@ * should get/save authorization codes for the "Authorization Code" * grant type * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_AuthorizationCodeInterface { diff --git a/src/OAuth2/Storage/ClientCredentialsInterface.php b/src/OAuth2/Storage/ClientCredentialsInterface.php index 45af123..d66da74 100644 --- a/src/OAuth2/Storage/ClientCredentialsInterface.php +++ b/src/OAuth2/Storage/ClientCredentialsInterface.php @@ -4,7 +4,7 @@ * Implement this interface to specify how the OAuth2 Server * should verify client credentials * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_ClientCredentialsInterface extends OAuth2_Storage_ClientInterface { diff --git a/src/OAuth2/Storage/ClientInterface.php b/src/OAuth2/Storage/ClientInterface.php index 9ca80fe..0f3def5 100644 --- a/src/OAuth2/Storage/ClientInterface.php +++ b/src/OAuth2/Storage/ClientInterface.php @@ -4,7 +4,7 @@ * Implement this interface to specify where the OAuth2 Server * should retrieve client information * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_ClientInterface { @@ -18,8 +18,9 @@ interface OAuth2_Storage_ClientInterface * Client identifier to be check with. * * @return array - * Client details. Only mandatory item is the "registered redirect URI", and MUST - * return FALSE if the given client does not exist or is invalid. + * Client details. The only mandatory key in the array is "redirect_uri". + * This function MUST return FALSE if the given client does not exist or is + * invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris. * * @ingroup oauth2_section_4 */ diff --git a/src/OAuth2/Storage/JWTBearerInterface.php b/src/OAuth2/Storage/JWTBearerInterface.php index 65001d1..d1cc86f 100644 --- a/src/OAuth2/Storage/JWTBearerInterface.php +++ b/src/OAuth2/Storage/JWTBearerInterface.php @@ -8,11 +8,10 @@ * be the same storage as retrieving clientData * * @author F21 - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_JWTBearerInterface { - /** * Get the public key associated with a client_id * diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 32bf485..97acd12 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -6,7 +6,7 @@ * NOTE: This class should never be used in production, and is * a stub class for example use only * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface, OAuth2_Storage_UserCredentialsInterface, OAuth2_Storage_AccessTokenInterface, @@ -20,6 +20,7 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface private $accessTokens; private $jwt; private $supportedScopes; + private $clientSupportedScopes; private $defaultScope; public function __construct($params = array()) @@ -32,6 +33,7 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface 'access_tokens' => array(), 'jwt' => array(), 'default_scope' => null, + 'client_supported_scopes' => array(), 'supported_scopes' => array(), ), $params); @@ -42,6 +44,7 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface $this->accessTokens = $params['access_tokens']; $this->jwt = $params['jwt']; $this->supportedScopes = $params['supported_scopes']; + $this->clientSupportedScopes = $params['client_supported_scopes']; $this->defaultScope = $params['default_scope']; } @@ -76,14 +79,16 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface return isset($this->userCredentials[$username]) && $this->userCredentials[$username] === $password; } - public function setUserCredentials($user_credentials) - { - return isset($this->userCredentials[$username]) ? $this->userCredentials[$username] : null; - } - public function getUserDetails($username) { - return $this->getUser($username); + if (!isset($this->userCredentials[$username])) { + return null; + } + + return array( + 'user_id' => $username, + 'password' => $this->userCredentials[$username], + ); } /* ClientCredentialsInterface */ @@ -151,7 +156,14 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface public function scopeExists($scope, $client_id = null) { $scope = explode(' ', trim($scope)); - return (count(array_diff($scope, $this->supportedScopes)) == 0); + + if (!is_null($client_id) && array_key_exists($client_id, $this->clientSupportedScopes)) { + $allowedScopes = array_merge($this->supportedScopes, $this->clientSupportedScopes[$client_id]); + } else { + $allowedScopes = $this->supportedScopes; + } + + return (count(array_diff($scope, $allowedScopes)) == 0); } public function getDefaultScope() @@ -163,11 +175,8 @@ class OAuth2_Storage_Memory implements OAuth2_Storage_AuthorizationCodeInterface public function getClientKey($client_id, $subject) { if (isset($this->jwt[$client_id])) { - $jwt = $this->jwt[$client_id]; - if ($jwt) { - if ($jwt["subject"] == $subject) { return $jwt["key"]; } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 6fd1182..2a2cdfc 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -169,9 +169,10 @@ class OAuth2_Storage_Mongo implements OAuth2_Storage_AuthorizationCodeInterface, public function getUserDetails($username) { - $user = $this->getUser($username); - // User details must have a 'user_id' field, to store in token data for example: - $user['user_id'] = $user['username']; + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; } diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index ca8bcb5..d45d8e4 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -3,11 +3,14 @@ /** * Simple PDO storage for all storage types * - * NOTE: This class is meant to get users started - * quickly. If your application requires further + * NOTE: This class is meant to get users started + * quickly. If your application requires further * customization, extend this class or create your own. * - * @author Brent Shaffer <bshafs@gmail.com> + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Brent Shaffer <bshafs at gmail dot com> */ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, OAuth2_Storage_AccessTokenInterface, OAuth2_Storage_ClientCredentialsInterface, @@ -47,7 +50,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, ), $config); } - /* ClientCredentialsInterface */ + /* OAuth2_Storage_ClientCredentialsInterface */ 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'])); @@ -77,7 +80,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, return true; } - /* AccessTokenInterface */ + /* OAuth2_Storage_AccessTokenInterface */ public function getAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); @@ -105,7 +108,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } - /* AuthorizationCodeInterface */ + /* OAuth2_Storage_AuthorizationCodeInterface */ public function getAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); @@ -140,7 +143,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, return $stmt->execute(compact('code')); } - /* UserCredentialsInterface */ + /* OAuth2_Storage_UserCredentialsInterface */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { @@ -154,7 +157,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, return $this->getUser($username); } - /* RefreshTokenInterface */ + /* OAuth2_Storage_RefreshTokenInterface */ public function getRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); @@ -209,6 +212,7 @@ class OAuth2_Storage_Pdo implements OAuth2_Storage_AuthorizationCodeInterface, return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); } + /* OAuth2_Storage_JWTBearerInterface */ 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'])); diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php new file mode 100644 index 0000000..bce438e --- /dev/null +++ b/src/OAuth2/Storage/Redis.php @@ -0,0 +1,195 @@ +<?php +/** + * redis storage for all storage types + * + * Register client: + * <code> + * $storage = new OAuth2_Storage_Redis($redis); + * $storage->registerClient($client_id, $client_secret, $redirect_uri); + * </code> + */ +class OAuth2_Storage_Redis implements OAuth2_Storage_AuthorizationCodeInterface, + OAuth2_Storage_AccessTokenInterface, + OAuth2_Storage_ClientCredentialsInterface, + OAuth2_Storage_UserCredentialsInterface, + OAuth2_Storage_RefreshTokenInterface, + OAuth2_Storage_JWTBearerInterface +{ + private $redis; + private $config; + private $cache; + + /** + * Redis Storage! + * + * @param \Predis\Client $redis + * @param array $config + */ + public function __construct($redis, $config=array()) + { + $this->redis = $redis; + $this->config = array_merge(array( + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + ), $config); + } + + protected function getValue($key) + { + if ( isset($this->cache[$key]) ) { + return $this->cache[$key]; + } + $value = $this->redis->get($key); + if ( isset($value) ) { + return json_decode($value, true); + } else { + return false; + } + } + + protected function setValue($key, $value, $expire=0) + { + $this->cache[$key] = $value; + $str = json_encode($value); + if ( $expire > 0 ) { + $seconds = $expire - time(); + return $this->redis->setex($key, $seconds, $str); + } else { + return $this->redis->set($key, $str); + } + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + return $this->redis->expire($key); + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + return $this->getValue($this->config['user_key'] . $username); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $client = $this->getClientDetails($client_id); + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + return in_array($grant_type, (array) $details['grant_types']); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + public function registerClient($client_id, $client_secret, $redirect_uri) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri') + ); + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $jwt = $this->getValue($this->config['jwt_key'] . $client_id); + if ( isset($jwt['subject']) && $jwt['subject'] == $subject ) { + return $jwt['key']; + } + return null; + } +} diff --git a/src/OAuth2/Storage/RefreshTokenInterface.php b/src/OAuth2/Storage/RefreshTokenInterface.php index eaff39e..b148ecb 100644 --- a/src/OAuth2/Storage/RefreshTokenInterface.php +++ b/src/OAuth2/Storage/RefreshTokenInterface.php @@ -5,11 +5,10 @@ * should get/save refresh tokens for the "Refresh Token" * grant type * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_RefreshTokenInterface { - /** * Grant refresh access tokens. * diff --git a/src/OAuth2/Storage/ScopeInterface.php b/src/OAuth2/Storage/ScopeInterface.php index 720b74d..5b2caaa 100644 --- a/src/OAuth2/Storage/ScopeInterface.php +++ b/src/OAuth2/Storage/ScopeInterface.php @@ -5,7 +5,7 @@ * should retrieve data involving the relevent scopes associated * with this implementation. * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_ScopeInterface { diff --git a/src/OAuth2/Storage/UserCredentialsInterface.php b/src/OAuth2/Storage/UserCredentialsInterface.php index 5ae184a..ea414d3 100644 --- a/src/OAuth2/Storage/UserCredentialsInterface.php +++ b/src/OAuth2/Storage/UserCredentialsInterface.php @@ -5,11 +5,10 @@ * should retrieve user credentials for the * "Resource Owner Password Credentials" grant type * - * @author Brent Shaffer <bshafs@gmail.com> + * @author Brent Shaffer <bshafs at gmail dot com> */ interface OAuth2_Storage_UserCredentialsInterface { - /** * Grant access tokens for basic user credentials. * @@ -47,7 +46,6 @@ interface OAuth2_Storage_UserCredentialsInterface * @return * ARRAY the associated "scope" or "user_id" values if applicable, or an empty array * if this does not apply - * */ public function getUserDetails($username); } diff --git a/src/OAuth2/TokenType/Bearer.php b/src/OAuth2/TokenType/Bearer.php index ad5e344..838286b 100644 --- a/src/OAuth2/TokenType/Bearer.php +++ b/src/OAuth2/TokenType/Bearer.php @@ -3,9 +3,8 @@ /** * */ -class OAuth2_TokenType_Bearer implements OAuth2_TokenTypeInterface, OAuth2_Response_ProviderInterface +class OAuth2_TokenType_Bearer implements OAuth2_TokenTypeInterface { - private $response; private $config; public function __construct(array $config = array()) @@ -43,25 +42,25 @@ class OAuth2_TokenType_Bearer implements OAuth2_TokenTypeInterface, OAuth2_Respo * @see http://code.google.com/p/android/issues/detail?id=6684 * */ - public function getAccessTokenParameter(OAuth2_RequestInterface $request) + public function getAccessTokenParameter(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { $headers = $request->headers('AUTHORIZATION'); // Check that exactly one method was used $methodsUsed = !empty($headers) + !is_null($request->query($this->config['token_param_name'])) + !is_null($request->request($this->config['token_param_name'])); if ($methodsUsed > 1) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); return null; } if ($methodsUsed == 0) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'The access token was not found'); + $response->setError(400, 'invalid_request', 'The access token was not found'); return null; } // HEADER: Get the access token from the header if (!empty($headers)) { if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/', $headers, $matches)) { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'Malformed auth header'); + $response->setError(400, 'invalid_request', 'Malformed auth header'); return null; } return $matches[1]; @@ -70,14 +69,14 @@ class OAuth2_TokenType_Bearer implements OAuth2_TokenTypeInterface, OAuth2_Respo if ($request->request($this->config['token_param_name'])) { // POST: Get the token from POST data if (strtolower($request->server('REQUEST_METHOD')) != 'post') { - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'When putting the token in the body, the method must be POST'); + $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST'); return null; } if ($request->server('CONTENT_TYPE') !== null && $request->server('CONTENT_TYPE') != 'application/x-www-form-urlencoded') { // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable // @see http://tools.ietf.org/html/rfc6750#section-2.2 - $this->response = new OAuth2_Response_Error(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); return null; } @@ -87,9 +86,4 @@ class OAuth2_TokenType_Bearer implements OAuth2_TokenTypeInterface, OAuth2_Respo // GET method return $request->query($this->config['token_param_name']); } - - public function getResponse() - { - return $this->response; - } } diff --git a/src/OAuth2/TokenType/MAC.php b/src/OAuth2/TokenType/MAC.php index c6c8fe3..60427e0 100644 --- a/src/OAuth2/TokenType/MAC.php +++ b/src/OAuth2/TokenType/MAC.php @@ -10,7 +10,7 @@ class OAuth2_TokenType_MAC implements OAuth2_TokenTypeInterface return 'mac'; } - public function getAccessTokenParameter(OAuth2_RequestInterface $request) + public function getAccessTokenParameter(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response) { throw new LogicException("Not supported"); diff --git a/src/OAuth2/TokenTypeInterface.php b/src/OAuth2/TokenTypeInterface.php index 8960e9e..fe7cfdb 100644 --- a/src/OAuth2/TokenTypeInterface.php +++ b/src/OAuth2/TokenTypeInterface.php @@ -2,6 +2,15 @@ interface OAuth2_TokenTypeInterface { + /** + * Token type identification string + * + * ex: "bearer" or "mac" + */ public function getTokenType(); - public function getAccessTokenParameter(OAuth2_RequestInterface $request); + + /** + * Retrieves the token string from the request object + */ + public function getAccessTokenParameter(OAuth2_RequestInterface $request, OAuth2_ResponseInterface $response); } |
