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 | |
| 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
25 files changed, 742 insertions, 146 deletions
diff --git a/.travis.yml b/.travis.yml index 4046941..8f2ab49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,8 @@ php: - 5.6 - hhvm env: - matrix: - - SKIP_MONGO_TESTS=1 global: + - SKIP_MONGO_TESTS=1 - secure: Bc5ZqvZ1YYpoPZNNuU2eCB8DS6vBYrAdfBtTenBs5NSxzb+Vjven4kWakbzaMvZjb/Ib7Uph7DGuOtJXpmxnvBXPLd707LZ89oFWN/yqQlZKCcm8iErvJCB5XL+/ONHj2iPdR242HJweMcat6bMCwbVWoNDidjtWMH0U2mYFy3M= - secure: R3bXlymyFiY2k2jf7+fv/J8i34wtXTkmD4mCr5Ps/U+vn9axm2VtvR2Nj+r7LbRjn61gzFE/xIVjYft/wOyBOYwysrfriydrnRVS0owh6y+7EyOyQWbRX11vVQMf8o31QCQE5BY58V5AJZW3MjoOL0FVlTgySJiJvdw6Pv18v+E= services: diff --git a/CHANGELOG.md b/CHANGELOG.md index 00813f1..23ea53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,34 @@ To see the files changed for a given bug, go to https://github.com/bshaffer/oaut To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1 To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash +* 1.6 (2014-01-16) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/496 + + * bug 437 - renames CryptoToken to JwtAccessToken / use_crypto_tokens to use_jwt_access_tokens + * bug 447 - Adds a Couchbase storage implementation + * bug 460 - Rename JWT claims to match spec + * bug 470 - order does not matter for multi-valued response types + * bug 471 - Make validateAuthorizeRequest available for POST in addition to GET + * bug 475 - Adds JTI table definitiion + * bug 481 - better randomness for generating access tokens + * bug 480 - Use hash_equals() for signature verification (prevents remote timing attacks) + * bugs 489, 491, 498 - misc other fixes + +* 1.5 (2014-08-27) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/446 + + * bug #399 - Add DynamoDB Support + * bug #404 - renamed error name for malformed/expired tokens + * bug #412 - Openid connect: fixes for claims with more than one scope / Add support for the prompt parameter ('consent' and 'none') + * bug #411 - fixes xml output + * bug #413 - fixes invalid format error + * bug #401 - fixes code standards / whitespace + * bug #354 - bundles PDO SQL with the library + * [BC] bug #397 - refresh tokens should not be encrypted + * bug #423 - makes "scope" optional for refresh token storage + * 1.4 (2014-06-12) PR: https://github.com/bshaffer/oauth2-server-php/pull/392 @@ -1,7 +1,7 @@ oauth2-server-php ================= -[](http://travis-ci.org/bshaffer/oauth2-server-php) +[](https://travis-ci.org/bshaffer/oauth2-server-php) [](https://packagist.org/packages/bshaffer/oauth2-server-php) 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), diff --git a/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php b/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php index 4b217c5..a0c9b57 100644 --- a/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php +++ b/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php @@ -4,7 +4,7 @@ namespace OAuth2\OpenID\Controller; use OAuth2\OpenID\Controller\AuthorizeController; use OAuth2\OpenID\ResponseType\IdToken; -use OAuth2\OpenID\ResponseType\TokenIdToken; +use OAuth2\OpenID\ResponseType\IdTokenToken; use OAuth2\ResponseType\AccessToken; use OAuth2\Storage\Bootstrap; use OAuth2\Server; @@ -42,6 +42,22 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase $this->assertArrayNotHasKey('token_type', $query); // Test valid token id_token request + $request->query['response_type'] = 'id_token token'; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types, order does not matter $request->query['response_type'] = 'token id_token'; $server->handleAuthorizeRequest($request, $response, true); @@ -56,6 +72,22 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase $this->assertArrayHasKey('token_type', $query); $this->assertArrayHasKey('state', $query); $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types with extra spaces do not matter + $request->query['response_type'] = ' token id_token '; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); } public function testMissingNonce() @@ -73,16 +105,14 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase // Test missing nonce for 'id_token' response type $server->handleAuthorizeRequest($request, $response, true); - $params = $response->getParameters(); $this->assertEquals($params['error'], 'invalid_nonce'); $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter'); - // Test missing nonce for 'id_token' response type - $request->query['response_type'] = 'token id_token'; + // Test missing nonce for 'id_token token' response type + $request->query['response_type'] = 'id_token token'; $server->handleAuthorizeRequest($request, $response, true); - $params = $response->getParameters(); $this->assertEquals($params['error'], 'invalid_nonce'); @@ -143,10 +173,10 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase private function getTestServer($config = array()) { $config += array( - 'use_openid_connect' => true, - 'issuer' => 'anzev', - 'allow_implicit' => true - ); + 'use_openid_connect' => true, + 'issuer' => 'phpunit', + 'allow_implicit' => true + ); $storage = Bootstrap::getInstance()->getMemoryStorage(); $server = new Server($storage, $config); diff --git a/test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php b/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php index f687cff..bc564d3 100644 --- a/test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php +++ b/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php @@ -9,15 +9,16 @@ use OAuth2\Storage\Bootstrap; use OAuth2\GrantType\ClientCredentials; use OAuth2\ResponseType\AccessToken; -class TokenIdTokenTest extends \PHPUnit_Framework_TestCase +class IdTokenTokenTest extends \PHPUnit_Framework_TestCase { public function testHandleAuthorizeRequest() { // add the test parameters in memory $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( - 'response_type' => 'token id_token', + 'response_type' => 'id_token token', 'redirect_uri' => 'http://adobe.com', 'client_id' => 'Test Client ID', 'scope' => 'openid', @@ -40,12 +41,9 @@ class TokenIdTokenTest extends \PHPUnit_Framework_TestCase $this->assertNotNull($params); $this->assertArrayHasKey('id_token', $params); $this->assertArrayHasKey('access_token', $params); - $this->validateIdToken($params['id_token']); - } - private function validateIdToken($id_token) - { - $parts = explode('.', $id_token); + // validate ID Token + $parts = explode('.', $params['id_token']); foreach ($parts as &$part) { // Each part is a base64url encoded json string. $part = str_replace(array('-', '_'), array('+', '/'), $part); @@ -80,10 +78,10 @@ class TokenIdTokenTest extends \PHPUnit_Framework_TestCase $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); $responseTypes = array( - 'token' => new AccessToken($memoryStorage, $memoryStorage), - 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config), + 'token' => $token = new AccessToken($memoryStorage, $memoryStorage), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token token' => new IdTokenToken($token, $idToken), ); - $responseTypes['token id_token'] = new TokenIdToken($responseTypes['token'], $responseTypes['id_token']); $server = new Server($memoryStorage, $config, array(), $responseTypes); $server->addGrantType(new ClientCredentials($memoryStorage)); diff --git a/test/OAuth2/RequestTest.php b/test/OAuth2/RequestTest.php index a7bcc4e..10db321 100644 --- a/test/OAuth2/RequestTest.php +++ b/test/OAuth2/RequestTest.php @@ -75,6 +75,16 @@ class RequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals('Basic secret', $request->headers('Authorization')); } + public function testRequestReturnsPostParamIfNoQueryParamAvailable() + { + $request = new Request( + array(), + array('client_id' => 'correct') + ); + + $this->assertEquals('correct', $request->query('client_id', $request->request('client_id'))); + } + private function getTestServer($config = array()) { $storage = Bootstrap::getInstance()->getMemoryStorage(); diff --git a/test/OAuth2/ResponseType/CryptoTokenTest.php b/test/OAuth2/ResponseType/JwtAccessTokenTest.php index d1ab89e..b326ec4 100644 --- a/test/OAuth2/ResponseType/CryptoTokenTest.php +++ b/test/OAuth2/ResponseType/JwtAccessTokenTest.php @@ -6,14 +6,39 @@ use OAuth2\Server; use OAuth2\Response; use OAuth2\Request\TestRequest; use OAuth2\Storage\Bootstrap; -use OAuth2\Storage\CryptoToken as CryptoTokenStorage; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; use OAuth2\GrantType\ClientCredentials; use OAuth2\GrantType\UserCredentials; use OAuth2\GrantType\RefreshToken; +use OAuth2\Encryption\Jwt; -class CryptoTokenTest extends \PHPUnit_Framework_TestCase +class JwtAccessTokenTest extends \PHPUnit_Framework_TestCase { - public function testGrantCryptoToken() + public function testCreateAccessToken() + { + $server = $this->getTestServer(); + $jwtResponseType = $server->getResponseType('token'); + + $accessToken = $jwtResponseType->createAccessToken('Test Client ID', 123, 'test', false); + $jwt = new Jwt; + $decodedAccessToken = $jwt->decode($accessToken['access_token'], null, false); + + $this->assertArrayHasKey('id', $decodedAccessToken); + $this->assertArrayHasKey('iss', $decodedAccessToken); + $this->assertArrayHasKey('aud', $decodedAccessToken); + $this->assertArrayHasKey('exp', $decodedAccessToken); + $this->assertArrayHasKey('iat', $decodedAccessToken); + $this->assertArrayHasKey('token_type', $decodedAccessToken); + $this->assertArrayHasKey('scope', $decodedAccessToken); + + $this->assertEquals('https://api.example.com', $decodedAccessToken['iss']); + $this->assertEquals('Test Client ID', $decodedAccessToken['aud']); + $this->assertEquals(123, $decodedAccessToken['sub']); + $delta = $decodedAccessToken['exp'] - $decodedAccessToken['iat']; + $this->assertEquals(3600, $delta); + } + + public function testGrantJwtAccessToken() { // add the test parameters in memory $server = $this->getTestServer(); @@ -28,7 +53,7 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase $this->assertEquals(2, substr_count($response->getParameter('access_token'), '.')); } - public function testAccessResourceWithCryptoToken() + public function testAccessResourceWithJwtAccessToken() { // add the test parameters in memory $server = $this->getTestServer(); @@ -38,17 +63,17 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase 'client_secret' => 'TestSecret', // valid client secret )); $server->handleTokenRequest($request, $response = new Response()); - $this->assertNotNull($cryptoToken = $response->getParameter('access_token')); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); // make a call to the resource server using the crypto token $request = TestRequest::createPost(array( - 'access_token' => $cryptoToken, + 'access_token' => $JwtAccessToken, )); $this->assertTrue($server->verifyResourceRequest($request)); } - public function testAccessResourceWithCryptoTokenUsingSecondaryStorage() + public function testAccessResourceWithJwtAccessTokenUsingSecondaryStorage() { // add the test parameters in memory $server = $this->getTestServer(); @@ -58,11 +83,11 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase 'client_secret' => 'TestSecret', // valid client secret )); $server->handleTokenRequest($request, $response = new Response()); - $this->assertNotNull($cryptoToken = $response->getParameter('access_token')); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); // make a call to the resource server using the crypto token $request = TestRequest::createPost(array( - 'access_token' => $cryptoToken, + 'access_token' => $JwtAccessToken, )); // create a resource server with the "memory" storage from the grant server @@ -71,16 +96,16 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase $this->assertTrue($resourceServer->verifyResourceRequest($request)); } - public function testCryptoTokenWithRefreshToken() + public function testJwtAccessTokenWithRefreshToken() { $server = $this->getTestServer(); - // add "UserCredentials" grant type and "CryptoToken" response type - // and ensure "CryptoToken" response type has "RefreshToken" storage + // add "UserCredentials" grant type and "JwtAccessToken" response type + // and ensure "JwtAccessToken" response type has "RefreshToken" storage $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); $server->addGrantType(new UserCredentials($memoryStorage)); $server->addGrantType(new RefreshToken($memoryStorage)); - $server->addResponseType(new CryptoToken($memoryStorage, $memoryStorage, $memoryStorage), 'token'); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, $memoryStorage), 'token'); $request = TestRequest::createPost(array( 'grant_type' => 'password', // valid grant type @@ -92,11 +117,11 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase // make the call to grant a crypto token $server->handleTokenRequest($request, $response = new Response()); - $this->assertNotNull($cryptoToken = $response->getParameter('access_token')); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); $this->assertNotNull($refreshToken = $response->getParameter('refresh_token')); // decode token and make sure refresh_token isn't set - list($header, $payload, $signature) = explode('.', $cryptoToken); + list($header, $payload, $signature) = explode('.', $JwtAccessToken); $decodedToken = json_decode(base64_decode($payload), true); $this->assertFalse(array_key_exists('refresh_token', $decodedToken)); @@ -117,15 +142,16 @@ class CryptoTokenTest extends \PHPUnit_Framework_TestCase $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); $storage = array( - 'access_token' => new CryptoTokenStorage($memoryStorage), + 'access_token' => new JwtAccessTokenStorage($memoryStorage), 'client' => $memoryStorage, 'client_credentials' => $memoryStorage, ); $server = new Server($storage); $server->addGrantType(new ClientCredentials($memoryStorage)); - // make the "token" response type a CryptoToken - $server->addResponseType(new CryptoToken($memoryStorage, $memoryStorage)); + // make the "token" response type a JwtAccessToken + $config = array('issuer' => 'https://api.example.com'); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, null, $config)); return $server; } diff --git a/test/OAuth2/ServerTest.php b/test/OAuth2/ServerTest.php index fd824fc..2d1460c 100644 --- a/test/OAuth2/ServerTest.php +++ b/test/OAuth2/ServerTest.php @@ -428,9 +428,9 @@ class ServerTest extends \PHPUnit_Framework_TestCase /** * @expectedException LogicException OAuth2\Storage\PublicKeyInterface **/ - public function testUsingCryptoTokensWithoutPublicKeyStorageThrowsException() + public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException() { - $server = new Server(array(), array('use_crypto_tokens' => true)); + $server = new Server(array(), array('use_jwt_access_tokens' => true)); $server->addGrantType($this->getMock('OAuth2\GrantType\GrantTypeInterface')); $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); @@ -438,10 +438,10 @@ class ServerTest extends \PHPUnit_Framework_TestCase $server->getTokenController(); } - public function testUsingJustCryptoTokenStorageWithResourceControllerIsOkay() + public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay() { $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($pubkey), array('use_crypto_tokens' => true)); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $this->assertNotNull($server->getResourceController()); $this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key')); @@ -450,31 +450,31 @@ class ServerTest extends \PHPUnit_Framework_TestCase /** * @expectedException LogicException OAuth2\Storage\ClientInterface **/ - public function testUsingJustCryptoTokenStorageWithAuthorizeControllerThrowsException() + public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException() { $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($pubkey), array('use_crypto_tokens' => true)); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $this->assertNotNull($server->getAuthorizeController()); } /** * @expectedException LogicException grant_types **/ - public function testUsingJustCryptoTokenStorageWithTokenControllerThrowsException() + public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException() { $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($pubkey), array('use_crypto_tokens' => true)); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $server->getTokenController(); } - public function testUsingCryptoTokenAndClientStorageWithAuthorizeControllerIsOkay() + public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay() { $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $client = $this->getMock('OAuth2\Storage\ClientInterface'); - $server = new Server(array($pubkey, $client), array('use_crypto_tokens' => true, 'allow_implicit' => true)); + $server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true)); $this->assertNotNull($server->getAuthorizeController()); - $this->assertInstanceOf('OAuth2\ResponseType\CryptoToken', $server->getResponseType('token')); + $this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token')); } /** @@ -526,7 +526,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $server->getAuthorizeController(); $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); - $this->assertNull($server->getResponseType('token id_token')); + $this->assertNull($server->getResponseType('id_token token')); } /** @@ -546,7 +546,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $server->getAuthorizeController(); } - public function testUsingOpenIDConnectWithAllowImplicitAndUseCryptoTokensIsOkay() + public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay() { $client = $this->getMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); @@ -555,13 +555,13 @@ class ServerTest extends \PHPUnit_Framework_TestCase 'use_openid_connect' => true, 'issuer' => 'someguy', 'allow_implicit' => true, - 'use_crypto_tokens' => true, + 'use_jwt_access_tokens' => true, )); $server->getAuthorizeController(); $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); - $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); } public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay() @@ -579,7 +579,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $server->getAuthorizeController(); $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); - $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); } public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay() @@ -600,7 +600,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase $server->getAuthorizeController(); $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); - $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); } /** @@ -642,6 +642,16 @@ class ServerTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code')); } + public function testMultipleValuedResponseTypeOrderDoesntMatter() + { + $responseType = $this->getMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface'); + $server = new Server(array(), array(), array(), array( + 'token id_token' => $responseType, + )); + + $this->assertEquals($responseType, $server->getResponseType('id_token token')); + } + public function testAddGrantTypeWithoutKey() { $server = new Server(); diff --git a/test/OAuth2/Storage/CryptoTokenTest.php b/test/OAuth2/Storage/JwtAccessTokenTest.php index e7f686d..38f8be7 100644 --- a/test/OAuth2/Storage/CryptoTokenTest.php +++ b/test/OAuth2/Storage/JwtAccessTokenTest.php @@ -4,7 +4,7 @@ namespace OAuth2\Storage; use OAuth2\Encryption\Jwt; -class CryptoTokenTest extends BaseTest +class jwtAccessTokenTest extends BaseTest { /** @dataProvider provideStorage */ public function testSetAccessToken($storage) @@ -14,18 +14,18 @@ class CryptoTokenTest extends BaseTest return; } - $crypto = new CryptoToken($storage); + $crypto = new jwtAccessToken($storage); $publicKeyStorage = Bootstrap::getInstance()->getMemoryStorage(); $encryptionUtil = new Jwt(); - $cryptoToken = array( + $jwtAccessToken = array( 'access_token' => rand(), 'expires' => time() + 100, 'scope' => 'foo', ); - $token = $encryptionUtil->encode($cryptoToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm()); + $token = $encryptionUtil->encode($jwtAccessToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm()); $this->assertNotNull($token); @@ -34,8 +34,8 @@ class CryptoTokenTest extends BaseTest $this->assertTrue(is_array($tokenData)); /* assert the decoded token is the same */ - $this->assertEquals($tokenData['access_token'], $cryptoToken['access_token']); - $this->assertEquals($tokenData['expires'], $cryptoToken['expires']); - $this->assertEquals($tokenData['scope'], $cryptoToken['scope']); + $this->assertEquals($tokenData['access_token'], $jwtAccessToken['access_token']); + $this->assertEquals($tokenData['expires'], $jwtAccessToken['expires']); + $this->assertEquals($tokenData['scope'], $jwtAccessToken['scope']); } } diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index 579997c..921d525 100644..100755 --- a/test/lib/OAuth2/Storage/BaseTest.php +++ b/test/lib/OAuth2/Storage/BaseTest.php @@ -14,6 +14,7 @@ abstract class BaseTest extends \PHPUnit_Framework_TestCase $redis = Bootstrap::getInstance()->getRedisStorage(); $cassandra = Bootstrap::getInstance()->getCassandraStorage(); $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); + $couchbase = Bootstrap::getInstance()->getCouchbase(); /* hack until we can fix "default_scope" dependencies in other tests */ $memory->defaultScope = 'defaultscope1 defaultscope2'; @@ -27,6 +28,7 @@ abstract class BaseTest extends \PHPUnit_Framework_TestCase array($redis), array($cassandra), array($dynamodb), + array($couchbase), ); } } diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 129ca2f..f1af0ba 100644..100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -15,6 +15,7 @@ class Bootstrap private $cassandra; private $configDir; private $dynamodb; + private $couchbase; public function __construct() { @@ -167,6 +168,43 @@ class Bootstrap return true; } + public function getCouchbase() + { + if (!$this->couchbase) { + $skipCouchbase = $this->getEnvVar('SKIP_COUCHBASE_TESTS'); + if (!$skipCouchbase && class_exists('Couchbase')) { + + $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false); + if ($this->testCouchbaseConnection($couchbase)) { + $this->clearCouchbase($couchbase); + $this->createCouchbaseDB($couchbase); + + $this->couchbase = new CouchbaseDB($couchbase); + } else { + $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"'); + } + } else { + $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so'); + } + } + + return $this->couchbase; + } + + private function testCouchbaseConnection(\Couchbase $couchbase) + { + try { + if (count($couchbase->getServers()) > 0) { + return true; + } + } catch (\CouchbaseException $e) { + return false; + } + + return true; + } + + public function getCassandraStorage() { if (!$this->cassandra) { @@ -341,6 +379,45 @@ class Bootstrap return $this->configDir; } + + private function createCouchbaseDB(\Couchbase $db) { + + $db->set('oauth_clients-oauth_test_client',json_encode(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + ))); + + $db->set('oauth_access_tokens-testtoken',json_encode(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_authorization_codes-testcode',json_encode(array( + 'access_token' => "testcode", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_users-testuser',json_encode(array( + 'username' => "testuser", + 'password' => "password" + ))); + + $db->set('oauth_jwt-oauth_test_client',json_encode(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + ))); + } + + private function clearCouchbase(\Couchbase $cb) { + $cb->delete('oauth_authorization_codes-new-openid-code'); + $cb->delete('oauth_access_tokens-newtoken'); + $cb->delete('oauth_authorization_codes-newcode'); + $cb->delete('oauth_refresh_tokens-refreshtoken'); + } + private function createMongoDb(\MongoDB $db) { $db->oauth_clients->insert(array( |
