diff options
| author | Brent Shaffer <bshafs@gmail.com> | 2015-01-17 01:43:04 +0300 |
|---|---|---|
| committer | Brent Shaffer <bshafs@gmail.com> | 2015-01-17 01:43:04 +0300 |
| commit | 7b083b76673d25e3522a579ec7566603da4b9132 (patch) | |
| tree | 22f043a1c8190300dbc45a74f4fbaaed4081068b /src | |
| parent | 070b0fcce4fef063ba890b888a74b1d681735e84 (diff) | |
| parent | 7670d99bdfffc69b09834f1d48c3217e2975a218 (diff) | |
| download | oauth2-server-php-1.6.tar.xz | |
Merge pull request #496 from bshaffer/developv1.6
Tag v1.6
Diffstat (limited to 'src')
14 files changed, 498 insertions, 82 deletions
diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index 66d1cd6..7a3327b 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -129,7 +129,7 @@ class AuthorizeController implements AuthorizeControllerInterface public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) { // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) - if (!$client_id = $request->query("client_id")) { + if (!$client_id = $request->query('client_id', $request->request('client_id'))) { // We don't have a good URI to use $response->setError(400, 'invalid_client', "No client id supplied"); @@ -149,7 +149,7 @@ class AuthorizeController implements AuthorizeControllerInterface // @see http://tools.ietf.org/html/rfc6749#section-3.1.2 // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1 // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1 - if ($supplied_redirect_uri = $request->query('redirect_uri')) { + if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) { // validate there is no fragment supplied $parts = parse_url($supplied_redirect_uri); if (isset($parts['fragment']) && $parts['fragment']) { @@ -182,8 +182,16 @@ class AuthorizeController implements AuthorizeControllerInterface } // Select the redirect URI - $response_type = $request->query('response_type'); - $state = $request->query('state'); + $response_type = $request->query('response_type', $request->request('response_type')); + + // for multiple-valued response types - make them alphabetical + if (false !== strpos($response_type, ' ')) { + $types = explode(' ', $response_type); + sort($types); + $response_type = ltrim(implode(' ', $types)); + } + + $state = $request->query('state', $request->request('state')); // type and client_id are required if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) { @@ -191,6 +199,7 @@ class AuthorizeController implements AuthorizeControllerInterface return false; } + if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { if (!isset($this->responseTypes['code'])) { $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); @@ -282,9 +291,9 @@ class AuthorizeController implements AuthorizeControllerInterface // Add our params to the parsed uri foreach ($params as $k => $v) { if (isset($parse_url[$k])) { - $parse_url[$k] .= "&" . http_build_query($v); + $parse_url[$k] .= "&" . http_build_query($v, '', '&'); } else { - $parse_url[$k] = http_build_query($v); + $parse_url[$k] = http_build_query($v, '', '&'); } } diff --git a/src/OAuth2/Encryption/Jwt.php b/src/OAuth2/Encryption/Jwt.php index 824ac13..181cead 100644 --- a/src/OAuth2/Encryption/Jwt.php +++ b/src/OAuth2/Encryption/Jwt.php @@ -69,7 +69,10 @@ class Jwt implements EncryptionInterface case'HS256': case'HS384': case'HS512': - return $this->sign($input, $key, $algo) === $signature; + return $this->hash_equals( + $this->sign($input, $key, $algo), + $signature + ); case 'RS256': return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; @@ -149,4 +152,16 @@ class Jwt implements EncryptionInterface 'alg' => $algorithm, ); } + + protected function hash_equals($a, $b) + { + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + $diff = strlen($a) ^ strlen($b); + for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + return $diff === 0; + } } diff --git a/src/OAuth2/OpenID/Controller/AuthorizeController.php b/src/OAuth2/OpenID/Controller/AuthorizeController.php index c6fe4f9..d130878 100644 --- a/src/OAuth2/OpenID/Controller/AuthorizeController.php +++ b/src/OAuth2/OpenID/Controller/AuthorizeController.php @@ -57,8 +57,8 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo $nonce = $request->query('nonce'); - // Validate required nonce for "id_token" and "token id_token" - if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_TOKEN_ID_TOKEN))) { + // Validate required nonce for "id_token" and "id_token token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); return false; @@ -75,7 +75,7 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo self::RESPONSE_TYPE_ACCESS_TOKEN, self::RESPONSE_TYPE_AUTHORIZATION_CODE, self::RESPONSE_TYPE_ID_TOKEN, - self::RESPONSE_TYPE_TOKEN_ID_TOKEN, + self::RESPONSE_TYPE_ID_TOKEN_TOKEN, ); } diff --git a/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php index 62704e7..b8a26db 100644 --- a/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php +++ b/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -5,5 +5,5 @@ namespace OAuth2\OpenID\Controller; interface AuthorizeControllerInterface { const RESPONSE_TYPE_ID_TOKEN = 'id_token'; - const RESPONSE_TYPE_TOKEN_ID_TOKEN = 'token id_token'; + const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token'; } diff --git a/src/OAuth2/OpenID/ResponseType/TokenIdToken.php b/src/OAuth2/OpenID/ResponseType/IdTokenToken.php index 5567569..498ab32 100644 --- a/src/OAuth2/OpenID/ResponseType/TokenIdToken.php +++ b/src/OAuth2/OpenID/ResponseType/IdTokenToken.php @@ -4,7 +4,7 @@ namespace OAuth2\OpenID\ResponseType; use OAuth2\ResponseType\AccessTokenInterface; -class TokenIdToken implements TokenIdTokenInterface +class IdTokenToken implements IdTokenTokenInterface { protected $accessToken; protected $idToken; diff --git a/src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php b/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php index 1b4c513..ac13e20 100644 --- a/src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php +++ b/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php @@ -4,6 +4,6 @@ namespace OAuth2\OpenID\ResponseType; use OAuth2\ResponseType\ResponseTypeInterface; -interface TokenIdTokenInterface extends ResponseTypeInterface +interface IdTokenTokenInterface extends ResponseTypeInterface { } diff --git a/src/OAuth2/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php index b05bf87..98cf41c 100644 --- a/src/OAuth2/ResponseType/AccessToken.php +++ b/src/OAuth2/ResponseType/AccessToken.php @@ -114,18 +114,27 @@ class AccessToken implements AccessTokenInterface */ protected function generateAccessToken() { - $tokenLen = 40; if (function_exists('mcrypt_create_iv')) { - $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - $randomData = openssl_random_pseudo_bytes(100); - } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data - $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); - } else { - $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } } - - return substr(hash('sha512', $randomData), 0, $tokenLen); + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + // Last resort which you probably should just get rid of: + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + return substr(hash('sha512', $randomData), 0, 40); } /** diff --git a/src/OAuth2/ResponseType/CryptoToken.php b/src/OAuth2/ResponseType/JwtAccessToken.php index 08ff853..b918238 100644 --- a/src/OAuth2/ResponseType/CryptoToken.php +++ b/src/OAuth2/ResponseType/JwtAccessToken.php @@ -13,7 +13,7 @@ use OAuth2\Storage\Memory; * * @author Brent Shaffer <bshafs at gmail dot com> */ -class CryptoToken extends AccessToken +class JwtAccessToken extends AccessToken { protected $publicKeyStorage; protected $encryptionUtil; @@ -29,6 +29,7 @@ class CryptoToken extends AccessToken $this->publicKeyStorage = $publicKeyStorage; $config = array_merge(array( 'store_encrypted_token_string' => true, + 'issuer' => '' ), $config); if (is_null($tokenStorage)) { // a pass-thru, so we can call the parent constructor @@ -60,11 +61,13 @@ class CryptoToken extends AccessToken { // token to encrypt $expires = time() + $this->config['access_lifetime']; - $cryptoToken = array( + $jwtAccessToken = array( 'id' => $this->generateAccessToken(), - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, + 'iss' => $this->config['issuer'], + 'aud' => $client_id, + 'sub' => $user_id, + 'exp' => $expires, + 'iat' => time(), 'token_type' => $this->config['token_type'], 'scope' => $scope ); @@ -72,14 +75,14 @@ class CryptoToken extends AccessToken /* * Encode the token data into a single access_token string */ - $access_token = $this->encodeToken($cryptoToken, $client_id); + $access_token = $this->encodeToken($jwtAccessToken, $client_id); /* * Save the token to a secondary storage. This is implemented on the - * OAuth2\Storage\CryptoToken side, and will not actually store anything, + * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, * if no secondary storage has been supplied */ - $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $cryptoToken['id']; + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $jwtAccessToken['id']; $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); // token to return to the client diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index 3434a7b..0dd982d 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -19,9 +19,9 @@ use OAuth2\ClientAssertionType\HttpBasic; use OAuth2\ResponseType\ResponseTypeInterface; use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType; use OAuth2\ResponseType\AccessToken; -use OAuth2\ResponseType\CryptoToken; +use OAuth2\ResponseType\JwtAccessToken; use OAuth2\OpenID\ResponseType\IdToken; -use OAuth2\OpenID\ResponseType\TokenIdToken; +use OAuth2\OpenID\ResponseType\IdTokenToken; use OAuth2\TokenType\TokenTypeInterface; use OAuth2\TokenType\Bearer; use OAuth2\GrantType\GrantTypeInterface; @@ -29,8 +29,8 @@ use OAuth2\GrantType\UserCredentials; use OAuth2\GrantType\ClientCredentials; use OAuth2\GrantType\RefreshToken; use OAuth2\GrantType\AuthorizationCode; -use OAuth2\Storage\CryptoToken as CryptoTokenStorage; -use OAuth2\Storage\CryptoTokenInterface; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; +use OAuth2\Storage\JwtAccessTokenInterface; /** * Server class for OAuth2 @@ -75,11 +75,12 @@ class Server implements ResourceControllerInterface, 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', 'scope' => 'OAuth2\Storage\ScopeInterface', ); + protected $responseTypeMap = array( 'token' => 'OAuth2\ResponseType\AccessTokenInterface', 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', - 'token id_token' => 'OAuth2\OpenID\ResponseType\TokenIdTokenInterface', + 'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface', ); /** @@ -105,7 +106,7 @@ class Server implements ResourceControllerInterface, // merge all config values. These get passed to our controller objects $this->config = array_merge(array( - 'use_crypto_tokens' => false, + 'use_jwt_access_tokens' => false, 'store_encrypted_token_string' => true, 'use_openid_connect' => false, 'id_lifetime' => 3600, @@ -124,9 +125,11 @@ class Server implements ResourceControllerInterface, foreach ($grantTypes as $key => $grantType) { $this->addGrantType($grantType, $key); } + foreach ($responseTypes as $key => $responseType) { $this->addResponseType($responseType, $key); } + $this->tokenType = $tokenType; $this->scopeUtil = $scopeUtil; $this->clientAssertionType = $clientAssertionType; @@ -400,6 +403,8 @@ class Server implements ResourceControllerInterface, public function addResponseType(ResponseTypeInterface $responseType, $key = null) { + $key = $this->normalizeResponseType($key); + if (isset($this->responseTypeMap[$key])) { if (!$responseType instanceof $this->responseTypeMap[$key]) { throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key])); @@ -451,7 +456,7 @@ class Server implements ResourceControllerInterface, if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); if ($this->config['allow_implicit']) { - $this->responseTypes['token id_token'] = $this->createDefaultTokenIdTokenResponseType(); + $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType(); } } @@ -495,13 +500,13 @@ class Server implements ResourceControllerInterface, protected function createDefaultResourceController() { - if ($this->config['use_crypto_tokens']) { - // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set - if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { - $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); } } elseif (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the resource server"); + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server"); } if (!$this->tokenType) { @@ -515,13 +520,13 @@ class Server implements ResourceControllerInterface, protected function createDefaultUserInfoController() { - if ($this->config['use_crypto_tokens']) { - // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set - if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) { - $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage(); + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); } } elseif (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the UserInfo server"); + throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server"); } if (!isset($this->storages['user_claims'])) { @@ -555,7 +560,7 @@ class Server implements ResourceControllerInterface, if ($this->config['use_openid_connect']) { $responseTypes['id_token'] = $this->getIdTokenResponseType(); if ($this->config['allow_implicit']) { - $responseTypes['token id_token'] = $this->getTokenIdTokenResponseType(); + $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType(); } } @@ -620,8 +625,8 @@ class Server implements ResourceControllerInterface, return $this->responseTypes['token']; } - if ($this->config['use_crypto_tokens']) { - return $this->createDefaultCryptoTokenResponseType(); + if ($this->config['use_jwt_access_tokens']) { + return $this->createDefaultJwtAccessTokenResponseType(); } return $this->createDefaultAccessTokenResponseType(); @@ -636,19 +641,19 @@ class Server implements ResourceControllerInterface, return $this->createDefaultIdTokenResponseType(); } - protected function getTokenIdTokenResponseType() + protected function getIdTokenTokenResponseType() { - if (isset($this->responseTypes['token id_token'])) { - return $this->responseTypes['token id_token']; + if (isset($this->responseTypes['id_token token'])) { + return $this->responseTypes['id_token token']; } - return $this->createDefaultTokenIdTokenResponseType(); + return $this->createDefaultIdTokenTokenResponseType(); } /** * For Resource Controller */ - protected function createDefaultCryptoTokenStorage() + protected function createDefaultJwtAccessTokenStorage() { if (!isset($this->storages['public_key'])) { throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); @@ -658,13 +663,13 @@ class Server implements ResourceControllerInterface, $tokenStorage = $this->storages['access_token']; } // wrap the access token storage as required. - return new CryptoTokenStorage($this->storages['public_key'], $tokenStorage); + return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage); } /** * For Authorize and Token Controllers */ - protected function createDefaultCryptoTokenResponseType() + protected function createDefaultJwtAccessTokenResponseType() { if (!isset($this->storages['public_key'])) { throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); @@ -682,7 +687,7 @@ class Server implements ResourceControllerInterface, $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string'))); - return new CryptoToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); } protected function createDefaultAccessTokenResponseType() @@ -716,9 +721,21 @@ class Server implements ResourceControllerInterface, return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); } - protected function createDefaultTokenIdTokenResponseType() + protected function createDefaultIdTokenTokenResponseType() + { + return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + } + + protected function normalizeResponseType($name) { - return new TokenIdToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + // for multiple-valued response types - make them alphabetical + if (!empty($name) && false !== strpos($name, ' ')) { + $types = explode(' ', $name); + sort($types); + $name = implode(' ', $types); + } + + return $name; } public function getResponse() @@ -753,6 +770,9 @@ class Server implements ResourceControllerInterface, public function getResponseType($name) { + // for multiple-valued response types - make them alphabetical + $name = $this->normalizeResponseType($name); + return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; } diff --git a/src/OAuth2/Storage/CouchbaseDB.php b/src/OAuth2/Storage/CouchbaseDB.php new file mode 100755 index 0000000..29226e0 --- /dev/null +++ b/src/OAuth2/Storage/CouchbaseDB.php @@ -0,0 +1,334 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple Couchbase storage for all storage types + * + * This class should be extended or overridden as required + * + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Tom Park <tom@raucter.com> + */ +class CouchbaseDB implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \Couchbase) { + $this->db = $connection; + } else { + if (!is_array($connection) || !is_array($connection['servers'])) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); + } + + $this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access couchbase item by type: + protected function getObjectByType($name,$id) + { + return json_decode($this->db->get($this->config[$name].'-'.$id),true); + } + + // Helper function to set couchbase item by type: + protected function setObjectByType($name,$id,$array) + { + $array['type'] = $name; + return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); + } + + // Helper function to delete couchbase item by type, wait for persist to at least 1 node + protected function deleteObjectByType($name,$id) + { + $this->db->delete($this->config[$name].'-'.$id,"",1); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->getObjectByType('client_table',$client_id)) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->getObjectByType('client_table',$client_id)) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->getObjectByType('client_table',$client_id); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } else { + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->getObjectByType('access_token_table',$access_token); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } else { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } + + return true; + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->getObjectByType('code_table',$code); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->setObjectByType('code_table',$code, array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } else { + $this->setObjectByType('code_table',$code,array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->deleteObjectByType('code_table',$code); + + return true; + } + + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->getObjectByType('refresh_token_table',$refresh_token); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->setObjectByType('refresh_token_table',$refresh_token, array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->deleteObjectByType('refresh_token_table',$refresh_token); + + return true; + } + + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->getObjectByType('user_table',$username); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } else { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('getJti() for the Couchbase driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); + } +} + diff --git a/src/OAuth2/Storage/CryptoToken.php b/src/OAuth2/Storage/JwtAccessToken.php index 31ebb65..74b1151 100644 --- a/src/OAuth2/Storage/CryptoToken.php +++ b/src/OAuth2/Storage/JwtAccessToken.php @@ -9,7 +9,7 @@ use OAuth2\Encryption\Jwt; * * @author Brent Shaffer <bshafs at gmail dot com> */ -class CryptoToken implements CryptoTokenInterface +class JwtAccessToken implements JwtAccessTokenInterface { protected $publicKeyStorage; protected $tokenStorage; @@ -39,7 +39,7 @@ class CryptoToken implements CryptoTokenInterface return false; } - $client_id = isset($tokenData['client_id']) ? $tokenData['client_id'] : null; + $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; $public_key = $this->publicKeyStorage->getPublicKey($client_id); $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); @@ -48,7 +48,8 @@ class CryptoToken implements CryptoTokenInterface return false; } - return $tokenData; + // normalize the JWT claims to the format expected by other components in this library + return $this->convertJwtToOAuth2($tokenData); } public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) @@ -57,4 +58,23 @@ class CryptoToken implements CryptoTokenInterface return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); } } + + // converts a JWT access token into an OAuth2-friendly format + protected function convertJwtToOAuth2($tokenData) + { + $keyMapping = array( + 'aud' => 'client_id', + 'exp' => 'expires', + 'sub' => 'user_id' + ); + + foreach ($keyMapping as $jwtKey => $oauth2Key) { + if (isset($tokenData[$jwtKey])) { + $tokenData[$oauth2Key] = $tokenData[$jwtKey]; + unset($tokenData[$jwtKey]); + } + } + + return $tokenData; + } } diff --git a/src/OAuth2/Storage/CryptoTokenInterface.php b/src/OAuth2/Storage/JwtAccessTokenInterface.php index 867710c..3abb2aa 100644 --- a/src/OAuth2/Storage/CryptoTokenInterface.php +++ b/src/OAuth2/Storage/JwtAccessTokenInterface.php @@ -8,7 +8,7 @@ namespace OAuth2\Storage; * * @author Brent Shaffer <bshafs at gmail dot com> */ -interface CryptoTokenInterface extends AccessTokenInterface +interface JwtAccessTokenInterface extends AccessTokenInterface { } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 87935fe..8a7ef10 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -40,9 +40,6 @@ class Mongo implements AuthorizationCodeInterface, $this->db = $m->{$connection['database']}; } - // Unix timestamps might get larger than 32 bits, - // so let's add native support for 64 bit ints. - ini_set('mongo.native_long', 1); $this->config = array_merge(array( 'client_table' => 'oauth_clients', diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index 17016e7..cb6b3cd 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -64,6 +64,7 @@ class Pdo implements 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', 'scope_table' => 'oauth_scopes', 'public_key_table' => 'oauth_public_keys', ), $config); @@ -74,7 +75,7 @@ class Pdo implements { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - $result = $stmt->fetch(); + $result = $stmt->fetch(\PDO::FETCH_BOTH); // make this extensible return $result && $result['client_secret'] == $client_secret; @@ -85,11 +86,11 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - if (!$result = $stmt->fetch()) { + if (!$result = $stmt->fetch(\PDO::FETCH_BOTH)) { return false; } - return empty($result['client_secret']);; + return empty($result['client_secret']); } /* OAuth2\Storage\ClientInterface */ @@ -98,7 +99,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); $stmt->execute(compact('client_id')); - return $stmt->fetch(); + return $stmt->fetch(\PDO::FETCH_BOTH); } public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) @@ -132,7 +133,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); $token = $stmt->execute(compact('access_token')); - if ($token = $stmt->fetch()) { + if ($token = $stmt->fetch(\PDO::FETCH_BOTH)) { // convert date string back to timestamp $token['expires'] = strtotime($token['expires']); } @@ -161,7 +162,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); $stmt->execute(compact('code')); - if ($code = $stmt->fetch()) { + if ($code = $stmt->fetch(\PDO::FETCH_BOTH)) { // convert date string back to timestamp $code['expires'] = strtotime($code['expires']); } @@ -271,7 +272,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); $token = $stmt->execute(compact('refresh_token')); - if ($token = $stmt->fetch()) { + if ($token = $stmt->fetch(\PDO::FETCH_BOTH)) { // convert expires to epoch time $token['expires'] = strtotime($token['expires']); } @@ -307,7 +308,7 @@ class Pdo implements $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); $stmt->execute(array('username' => $username)); - if (!$userInfo = $stmt->fetch()) { + if (!$userInfo = $stmt->fetch(\PDO::FETCH_BOTH)) { return false; } @@ -340,7 +341,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn)); $stmt->execute($scope); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_BOTH)) { return $result['count'] == count($scope); } @@ -352,7 +353,7 @@ class Pdo implements $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); $stmt->execute(array('is_default' => true)); - if ($result = $stmt->fetchAll()) { + if ($result = $stmt->fetchAll(\PDO::FETCH_BOTH)) { $defaultScope = array_map(function ($row) { return $row['scope']; }, $result); @@ -392,7 +393,7 @@ class Pdo implements $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_BOTH)) { return array( 'issuer' => $result['issuer'], 'subject' => $result['subject'], @@ -418,7 +419,7 @@ class Pdo implements $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_BOTH)) { return $result['public_key']; } } @@ -428,7 +429,7 @@ class Pdo implements $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_BOTH)) { return $result['private_key']; } } @@ -438,7 +439,7 @@ class Pdo implements $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); $stmt->execute(compact('client_id')); - if ($result = $stmt->fetch()) { + if ($result = $stmt->fetch(\PDO::FETCH_BOTH)) { return $result['encryption_algorithm']; } @@ -513,6 +514,14 @@ class Pdo implements subject VARCHAR(80), public_key VARCHAR(2000) NOT NULL ); + + CREATE TABLE {$this->config['jti_table']} ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80), + audiance VARCHAR(80), + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL + ); CREATE TABLE {$this->config['public_key_table']} ( client_id VARCHAR(80), |
