From 49bc005f5e6056754e0b039353d54d75b79f3bab Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 9 May 2015 13:26:42 -0600 Subject: [PATCH 01/12] fixes #547 - ensures Grant Type identifier is passed to tokencontroller --- src/OAuth2/Server.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index 199e74722..c98901853 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -348,17 +348,17 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface return $value; } - public function addGrantType(GrantTypeInterface $grantType, $key = null) + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { - if (is_string($key)) { - $this->grantTypes[$key] = $grantType; - } else { - $this->grantTypes[$grantType->getQuerystringIdentifier()] = $grantType; + if (!is_string($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); } + $this->grantTypes[$identifier] = $grantType; + // persist added grant type down to TokenController if (!is_null($this->tokenController)) { - $this->getTokenController()->addGrantType($grantType); + $this->getTokenController()->addGrantType($grantType, $identifier); } } From 48301f14d78040f7a5e12d00802e17ad8c9c597e Mon Sep 17 00:00:00 2001 From: F21 Date: Sun, 10 May 2015 11:58:35 +1000 Subject: [PATCH 02/12] Added FirebaseJwt bridge to allow firebase/php-jwt to be used. --- .travis.yml | 1 + composer.json | 3 +- src/OAuth2/Encryption/FirebaseJwt.php | 47 ++++++++++ test/OAuth2/Encryption/FirebaseJwtTest.php | 102 +++++++++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/OAuth2/Encryption/FirebaseJwt.php create mode 100644 test/OAuth2/Encryption/FirebaseJwtTest.php diff --git a/.travis.yml b/.travis.yml index 8f2ab4946..5c655037c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,5 +19,6 @@ before_script: - composer require predis/predis:dev-master - composer require thobbs/phpcassa:dev-master - composer require aws/aws-sdk-php:dev-master +- composer require firebase/php-jwt after_script: - php test/cleanup.php \ No newline at end of file diff --git a/composer.json b/composer.json index 6e2abe2b9..f52612290 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "suggest": { "predis/predis": "Required to use the Redis storage engine", "thobbs/phpcassa": "Required to use the Cassandra storage engine", - "aws/aws-sdk-php": "Required to use the DynamoDB storage engine" + "aws/aws-sdk-php": "Required to use the DynamoDB storage engine", + "firebase/php-jwt": "Required to use JWT features" } } diff --git a/src/OAuth2/Encryption/FirebaseJwt.php b/src/OAuth2/Encryption/FirebaseJwt.php new file mode 100644 index 000000000..7b7289753 --- /dev/null +++ b/src/OAuth2/Encryption/FirebaseJwt.php @@ -0,0 +1,47 @@ + + */ +class FirebaseJwt implements EncryptionInterface +{ + public function __construct() + { + if (!class_exists('\JWT')) { + throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); + } + } + + public function encode($payload, $key, $alg = 'HS256', $keyId = null) + { + return \JWT::encode($payload, $key, $alg, $keyId); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = null) + { + try { + + //Maintain BC: Do not verify if no algorithms are passed in. + if (!$allowedAlgorithms) { + $key = null; + } + + return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); + } catch (\Exception $e) { + return false; + } + } + + public function urlSafeB64Encode($data) + { + return \JWT::urlsafeB64Encode($data); + } + + public function urlSafeB64Decode($b64) + { + return \JWT::urlsafeB64Decode($b64); + } +} diff --git a/test/OAuth2/Encryption/FirebaseJwtTest.php b/test/OAuth2/Encryption/FirebaseJwtTest.php new file mode 100644 index 000000000..89e2c8ac4 --- /dev/null +++ b/test/OAuth2/Encryption/FirebaseJwtTest.php @@ -0,0 +1,102 @@ +privateKey = << $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); + + // test BC behaviour of trusting the algorithm in the header + $payload = $jwtUtil->decode($encoded, $client_key); + $this->assertEquals($params, $payload); + + // test BC behaviour of not verifying by passing false + $payload = $jwtUtil->decode($encoded, $client_key, false); + $this->assertEquals($params, $payload); + + // test the new restricted algorithms header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + } + + public function testInvalidJwt() + { + $jwtUtil = new FirebaseJwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + + /** @dataProvider provideClientCredentials */ + public function testInvalidJwtHeader($client_id, $client_key) + { + $jwtUtil = new FirebaseJwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + // testing for algorithm tampering when only RSA256 signing is allowed + // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); + + $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); + + $this->assertFalse($payload); + } + + public function provideClientCredentials() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $client_id = 'Test Client ID'; + $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); + + return array( + array($client_id, $client_key), + ); + } +} From de0d9fb26f289de2a8193a1814275a9033d1436d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 21 May 2015 10:44:56 -0600 Subject: [PATCH 03/12] runs php-cs-fixer - using the command "php-cs-fixer fix . --fixers=-phpdoc_params" --- src/OAuth2/Encryption/Jwt.php | 3 ++- src/OAuth2/ResponseType/AccessToken.php | 3 ++- src/OAuth2/Storage/Cassandra.php | 5 +++-- src/OAuth2/Storage/CouchbaseDB.php | 5 +---- src/OAuth2/Storage/JwtAccessToken.php | 2 +- src/OAuth2/Storage/Mongo.php | 4 ---- src/OAuth2/Storage/Pdo.php | 2 +- .../OpenID/Controller/AuthorizeControllerTest.php | 4 ---- .../OpenID/Controller/UserInfoControllerTest.php | 11 +++++------ test/OAuth2/Storage/DynamoDBTest.php | 1 - test/OAuth2/Storage/JwtAccessTokenTest.php | 2 +- test/cleanup.php | 2 +- test/lib/OAuth2/Storage/Bootstrap.php | 9 ++++----- 13 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/OAuth2/Encryption/Jwt.php b/src/OAuth2/Encryption/Jwt.php index 030f621d8..ee576e643 100644 --- a/src/OAuth2/Encryption/Jwt.php +++ b/src/OAuth2/Encryption/Jwt.php @@ -157,7 +157,7 @@ protected function generateJwtHeader($payload, $algorithm) 'alg' => $algorithm, ); } - + protected function hash_equals($a, $b) { if (function_exists('hash_equals')) { @@ -167,6 +167,7 @@ protected function hash_equals($a, $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/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php index 98cf41cc1..c1ce43575 100644 --- a/src/OAuth2/ResponseType/AccessToken.php +++ b/src/OAuth2/ResponseType/AccessToken.php @@ -125,7 +125,7 @@ protected function generateAccessToken() 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) { @@ -134,6 +134,7 @@ protected function generateAccessToken() } // 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/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index 5a21b8780..cf8acda81 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -205,6 +205,7 @@ public function getUser($username) public function setUser($username, $password, $first_name = null, $last_name = null) { $password = sha1($password); + return $this->setValue( $this->config['user_key'] . $username, compact('username', 'password', 'first_name', 'last_name') @@ -258,7 +259,6 @@ public function checkRestrictedGrantType($client_id, $grant_type) return true; } - /* RefreshTokenInterface */ public function getRefreshToken($refresh_token) { @@ -378,7 +378,7 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti) throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); } - /* PublicKeyInterface */ + /* PublicKeyInterface */ public function getPublicKey($client_id = '') { $public_key = $this->getValue($this->config['public_key_key'] . $client_id); @@ -413,6 +413,7 @@ public function getEncryptionAlgorithm($client_id = null) if (is_array($public_key)) { return $public_key['encryption_algorithm']; } + return 'RS256'; } diff --git a/src/OAuth2/Storage/CouchbaseDB.php b/src/OAuth2/Storage/CouchbaseDB.php index 29226e086..1eb55f027 100755 --- a/src/OAuth2/Storage/CouchbaseDB.php +++ b/src/OAuth2/Storage/CouchbaseDB.php @@ -57,6 +57,7 @@ protected function getObjectByType($name,$id) protected function setObjectByType($name,$id,$array) { $array['type'] = $name; + return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); } @@ -164,7 +165,6 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return true; } - /* AuthorizationCodeInterface */ public function getAuthorizationCode($code) { @@ -208,7 +208,6 @@ public function expireAuthorizationCode($code) return true; } - /* UserCredentialsInterface */ public function checkUserCredentials($username, $password) { @@ -256,7 +255,6 @@ public function unsetRefreshToken($refresh_token) return true; } - // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { @@ -331,4 +329,3 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti) throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); } } - diff --git a/src/OAuth2/Storage/JwtAccessToken.php b/src/OAuth2/Storage/JwtAccessToken.php index a2673dd13..3a35aca03 100644 --- a/src/OAuth2/Storage/JwtAccessToken.php +++ b/src/OAuth2/Storage/JwtAccessToken.php @@ -71,7 +71,7 @@ protected function convertJwtToOAuth2($tokenData) foreach ($keyMapping as $jwtKey => $oauth2Key) { if (isset($tokenData[$jwtKey])) { $tokenData[$oauth2Key] = $tokenData[$jwtKey]; - unset($tokenData[$jwtKey]); + unset($tokenData[$jwtKey]); } } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 8a7ef1097..b82678f39 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -40,7 +40,6 @@ public function __construct($connection, $config = array()) $this->db = $m->{$connection['database']}; } - $this->config = array_merge(array( 'client_table' => 'oauth_clients', 'access_token_table' => 'oauth_access_tokens', @@ -162,7 +161,6 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return true; } - /* AuthorizationCodeInterface */ public function getAuthorizationCode($code) { @@ -210,7 +208,6 @@ public function expireAuthorizationCode($code) return true; } - /* UserCredentialsInterface */ public function checkUserCredentials($username, $password) { @@ -260,7 +257,6 @@ public function unsetRefreshToken($refresh_token) return true; } - // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index 7ae3bf71a..8369dbac6 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -514,7 +514,7 @@ public function getBuildSql($dbName = 'oauth2_server_php') subject VARCHAR(80), public_key VARCHAR(2000) NOT NULL ); - + CREATE TABLE {$this->config['jti_table']} ( issuer VARCHAR(80) NOT NULL, subject VARCHAR(80), diff --git a/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php b/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php index a0c9b5766..46de936d8 100644 --- a/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php +++ b/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php @@ -2,10 +2,6 @@ namespace OAuth2\OpenID\Controller; -use OAuth2\OpenID\Controller\AuthorizeController; -use OAuth2\OpenID\ResponseType\IdToken; -use OAuth2\OpenID\ResponseType\IdTokenToken; -use OAuth2\ResponseType\AccessToken; use OAuth2\Storage\Bootstrap; use OAuth2\Server; use OAuth2\Request; diff --git a/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php b/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php index 90fdd0b61..b1b687077 100644 --- a/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php +++ b/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php @@ -2,7 +2,6 @@ namespace OAuth2\OpenID\Controller; -use OAuth2\OpenID\Controller\UserInfoController; use OAuth2\Storage\Bootstrap; use OAuth2\Server; use OAuth2\Request; @@ -12,13 +11,13 @@ class UserInfoControllerTest extends \PHPUnit_Framework_TestCase { public function testCreateController() { - $tokenType = new \OAuth2\TokenType\Bearer(); - $storage = new \OAuth2\Storage\Memory(); + $tokenType = new \OAuth2\TokenType\Bearer(); + $storage = new \OAuth2\Storage\Memory(); $controller = new UserInfoController($tokenType, $storage, $storage); - $response = new Response(); - $controller->handleUserInfoRequest(new Request(), $response); - $this->assertEquals(401, $response->getStatusCode()); + $response = new Response(); + $controller->handleUserInfoRequest(new Request(), $response); + $this->assertEquals(401, $response->getStatusCode()); } public function testValidToken() diff --git a/test/OAuth2/Storage/DynamoDBTest.php b/test/OAuth2/Storage/DynamoDBTest.php index 69968490e..2147f0914 100644 --- a/test/OAuth2/Storage/DynamoDBTest.php +++ b/test/OAuth2/Storage/DynamoDBTest.php @@ -29,7 +29,6 @@ public function testGetDefaultScope() ->method('toArray') ->will($this->returnValue($data)); - // should return null default scope if none is set in database $client->expects($this->once()) ->method('query') diff --git a/test/OAuth2/Storage/JwtAccessTokenTest.php b/test/OAuth2/Storage/JwtAccessTokenTest.php index 38f8be7b6..a6acbea1f 100644 --- a/test/OAuth2/Storage/JwtAccessTokenTest.php +++ b/test/OAuth2/Storage/JwtAccessTokenTest.php @@ -4,7 +4,7 @@ use OAuth2\Encryption\Jwt; -class jwtAccessTokenTest extends BaseTest +class JwtAccessTokenTest extends BaseTest { /** @dataProvider provideStorage */ public function testSetAccessToken($storage) diff --git a/test/cleanup.php b/test/cleanup.php index 4042eb27e..8663a901b 100644 --- a/test/cleanup.php +++ b/test/cleanup.php @@ -12,4 +12,4 @@ } // remove the dynamoDB database that was created for this build -OAuth2\Storage\Bootstrap::getInstance()->cleanupTravisDynamoDb(); \ No newline at end of file +OAuth2\Storage\Bootstrap::getInstance()->cleanupTravisDynamoDb(); diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 38e1af181..7497bedd0 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -213,7 +213,6 @@ private function testCouchbaseConnection(\Couchbase $couchbase) return true; } - public function getCassandraStorage() { if (!$this->cassandra) { @@ -402,9 +401,8 @@ public function getConfigDir() return $this->configDir; } - - private function createCouchbaseDB(\Couchbase $db) { - + private function createCouchbaseDB(\Couchbase $db) + { $db->set('oauth_clients-oauth_test_client',json_encode(array( 'client_id' => "oauth_test_client", 'client_secret' => "testpass", @@ -434,7 +432,8 @@ private function createCouchbaseDB(\Couchbase $db) { ))); } - private function clearCouchbase(\Couchbase $cb) { + 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'); From 4aceb7cb587056a7d126613df79d9cda16da0f97 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 21 May 2015 11:15:20 -0600 Subject: [PATCH 04/12] fixes #591 - adds jti --- src/OAuth2/ResponseType/JwtAccessToken.php | 4 +++- test/OAuth2/ResponseType/JwtAccessTokenTest.php | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/OAuth2/ResponseType/JwtAccessToken.php b/src/OAuth2/ResponseType/JwtAccessToken.php index b9182385e..3942fe41e 100644 --- a/src/OAuth2/ResponseType/JwtAccessToken.php +++ b/src/OAuth2/ResponseType/JwtAccessToken.php @@ -61,8 +61,10 @@ public function createAccessToken($client_id, $user_id, $scope = null, $includeR { // token to encrypt $expires = time() + $this->config['access_lifetime']; + $id = $this->generateAccessToken(); $jwtAccessToken = array( - 'id' => $this->generateAccessToken(), + 'id' => $id, // for BC (see #591) + 'jti' => $id, 'iss' => $this->config['issuer'], 'aud' => $client_id, 'sub' => $user_id, diff --git a/test/OAuth2/ResponseType/JwtAccessTokenTest.php b/test/OAuth2/ResponseType/JwtAccessTokenTest.php index b326ec451..51b01a927 100644 --- a/test/OAuth2/ResponseType/JwtAccessTokenTest.php +++ b/test/OAuth2/ResponseType/JwtAccessTokenTest.php @@ -24,6 +24,7 @@ public function testCreateAccessToken() $decodedAccessToken = $jwt->decode($accessToken['access_token'], null, false); $this->assertArrayHasKey('id', $decodedAccessToken); + $this->assertArrayHasKey('jti', $decodedAccessToken); $this->assertArrayHasKey('iss', $decodedAccessToken); $this->assertArrayHasKey('aud', $decodedAccessToken); $this->assertArrayHasKey('exp', $decodedAccessToken); @@ -36,6 +37,7 @@ public function testCreateAccessToken() $this->assertEquals(123, $decodedAccessToken['sub']); $delta = $decodedAccessToken['exp'] - $decodedAccessToken['iat']; $this->assertEquals(3600, $delta); + $this->assertEquals($decodedAccessToken['id'], $decodedAccessToken['jti']); } public function testGrantJwtAccessToken() From d00a4002f11157b9198a5e5dca077a7d0fb58302 Mon Sep 17 00:00:00 2001 From: Q Date: Fri, 15 May 2015 15:25:01 +0200 Subject: [PATCH 05/12] Fixes issue where full product page is ajaxed into a product detail page. Product page inception, bro --- src/OAuth2/Server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index c98901853..bdd9c5c0f 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -693,7 +693,7 @@ protected function createDefaultJwtAccessTokenResponseType() $refreshStorage = $this->storages['refresh_token']; } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer'))); + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime'))); return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); } From 72258c7ca3975ff24499c7c8a6ee2f5dc612c0b6 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Tue, 14 Jul 2015 11:42:03 +0900 Subject: [PATCH 06/12] Remove extra semicolon --- src/OAuth2/Storage/Redis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index fd94f475d..eb024c57a 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -162,7 +162,7 @@ public function isPublicClient($client_id) return false; } - return empty($result['client_secret']);; + return empty($result['client_secret']); } /* ClientInterface */ From f712d61e4efc546e8fdd9b5dcc9a79c385133dc7 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 24 Jul 2015 15:27:52 -0700 Subject: [PATCH 07/12] fix aws sdk composer dependency uses only 2.8 for tests --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f2ab4946..0f1b54e37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,6 @@ before_script: - psql -c 'create database oauth2_server_php;' -U postgres - composer require predis/predis:dev-master - composer require thobbs/phpcassa:dev-master -- composer require aws/aws-sdk-php:dev-master +- composer require 'aws/aws-sdk-php:~2.8' after_script: -- php test/cleanup.php \ No newline at end of file +- php test/cleanup.php From 7e07d7cfdab528f0222b8d64dd96b37af8b5b4a4 Mon Sep 17 00:00:00 2001 From: Patrick Hindmarsh Date: Mon, 11 May 2015 13:13:59 +1200 Subject: [PATCH 08/12] Added support for revoking tokens Implementation follows the Draft RFC7009 OAuth 2.0 Token Revocation. Essentially adds a unsetAccessToken method to all the storage objects, and exposes a revoke endpoint on controllers. The only interesting thing is the ResponseType\AccessToken::revokeToken() method which has to check all token types if the specified type doesn't exist. Maintains BC with v1.x by commenting out the AccessToken interface methods, and annotated an @todo to implement these in 2.x. Throws a \RuntimeException if storage/response objects don't support the interface --- src/OAuth2/Controller/TokenController.php | 54 +++++++++ src/OAuth2/ResponseType/AccessToken.php | 37 ++++++ .../ResponseType/AccessTokenInterface.php | 11 ++ src/OAuth2/Server.php | 18 +++ src/OAuth2/Storage/AccessTokenInterface.php | 15 +++ src/OAuth2/Storage/Cassandra.php | 5 + src/OAuth2/Storage/DynamoDB.php | 10 ++ src/OAuth2/Storage/JwtAccessToken.php | 8 ++ src/OAuth2/Storage/Memory.php | 5 + src/OAuth2/Storage/Mongo.php | 6 + src/OAuth2/Storage/Pdo.php | 7 ++ src/OAuth2/Storage/Redis.php | 5 + .../OAuth2/Controller/TokenControllerTest.php | 48 ++++++++ test/OAuth2/ResponseType/AccessTokenTest.php | 107 ++++++++++++++++++ test/OAuth2/Storage/AccessTokenTest.php | 25 ++++ 15 files changed, 361 insertions(+) create mode 100644 test/OAuth2/ResponseType/AccessTokenTest.php diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index 46cfb1755..2e0750f6a 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -217,4 +217,58 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) $this->grantTypes[$identifier] = $grantType; } + + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response) + { + if ($this->revokeToken($request, $response)) { + $response->setStatusCode(200); + $response->addParameters(array('revoked' => true)); + } + } + + /** + * Revoke a refresh or access token. Returns true on success and when tokens are invalid + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|null + */ + public function revokeToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + $token_type_hint = $request->request('token_type_hint'); + if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) { + $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\''); + + return null; + } + + $token = $request->request('token'); + if ($token === null) { + $response->setError(400, 'invalid_request', 'Missing token parameter to revoke'); + + return null; + } + + // @todo remove this check for v2.0 + if (!method_exists($this->accessToken, 'revokeToken')) { + $class = get_class($this->accessToken); + throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method"); + } + + $this->accessToken->revokeToken($token, $token_type_hint); + + return true; + } } diff --git a/src/OAuth2/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php index c1ce43575..b235ad0c5 100644 --- a/src/OAuth2/ResponseType/AccessToken.php +++ b/src/OAuth2/ResponseType/AccessToken.php @@ -154,4 +154,41 @@ protected function generateRefreshToken() { return $this->generateAccessToken(); // let's reuse the same scheme for token generation } + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + * + * @param $token + * @param null $tokenTypeHint + * @return boolean + */ + public function revokeToken($token, $tokenTypeHint = null) + { + if ($tokenTypeHint == 'refresh_token') { + if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) { + return true; + } + } + + /** @TODO remove in v2 */ + if (!method_exists($this->tokenStorage, 'unsetAccessToken')) { + throw new \RuntimeException( + sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage) + )); + } + + $revoked = $this->tokenStorage->unsetAccessToken($token); + + // if a typehint is supplied and fails, try other storages + // @see https://tools.ietf.org/html/rfc7009#section-2.1 + if (!$revoked && $tokenTypeHint != 'refresh_token') { + if ($this->refreshStorage) { + $revoked = $this->refreshStorage->unsetRefreshToken($token); + } + } + + return $revoked; + } } diff --git a/src/OAuth2/ResponseType/AccessTokenInterface.php b/src/OAuth2/ResponseType/AccessTokenInterface.php index 2ba4a4fb5..4bd3928d8 100644 --- a/src/OAuth2/ResponseType/AccessTokenInterface.php +++ b/src/OAuth2/ResponseType/AccessTokenInterface.php @@ -20,4 +20,15 @@ interface AccessTokenInterface extends ResponseTypeInterface * @ingroup oauth2_section_5 */ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * + * @param $token + * @param $tokenTypeHint + * @return mixed + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function revokeToken($token, $tokenTypeHint); } diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index bdd9c5c0f..171a4f069 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -269,6 +269,24 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r return $value; } + /** + * Handle a revoke token request + * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec + * + * @see https://tools.ietf.org/html/rfc7009#section-2 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return Response|ResponseInterface + */ + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleRevokeRequest($request, $this->response); + + return $this->response; + } + /** * Redirect the user appropriately after approval. * diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php index d382f89f8..ac081bb6c 100644 --- a/src/OAuth2/Storage/AccessTokenInterface.php +++ b/src/OAuth2/Storage/AccessTokenInterface.php @@ -45,4 +45,19 @@ public function getAccessToken($oauth_token); * @ingroup oauth2_section_4 */ public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire an access token. + * + * This is not explicitly required in the spec, but if defined in a draft RFC for token + * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009 + * + * @param $access_token + * Access token to be expired. + * + * @ingroup oauth2_section_6 + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function unsetAccessToken($access_token); } diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index cf8acda81..4c661ed32 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -294,6 +294,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s ); } + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + /* ScopeInterface */ public function scopeExists($scope) { diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index 6ad016c37..596a31af5 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -182,6 +182,16 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s } + public function unsetAccessToken($access_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['access_token_table'], + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)) + )); + + return true; + } + /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { diff --git a/src/OAuth2/Storage/JwtAccessToken.php b/src/OAuth2/Storage/JwtAccessToken.php index 3a35aca03..75b49d301 100644 --- a/src/OAuth2/Storage/JwtAccessToken.php +++ b/src/OAuth2/Storage/JwtAccessToken.php @@ -59,6 +59,14 @@ public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $sc } } + public function unsetAccessToken($access_token) + { + if ($this->tokenStorage) { + return $this->tokenStorage->unsetAccessToken($access_token); + } + } + + // converts a JWT access token into an OAuth2-friendly format protected function convertJwtToOAuth2($tokenData) { diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 35e6cbacf..4f0859deb 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -257,6 +257,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return true; } + public function unsetAccessToken($access_token) + { + unset($this->accessTokens[$access_token]); + } + public function scopeExists($scope) { $scope = explode(' ', trim($scope)); diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index b82678f39..fd5e7be36 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -161,6 +161,12 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return true; } + public function unsetAccessToken($access_token) + { + $this->collection('access_token_table')->remove(array('access_token' => $access_token)); + } + + /* AuthorizationCodeInterface */ public function getAuthorizationCode($code) { diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index 8369dbac6..b8fac34ab 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -156,6 +156,13 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } + public function unsetAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table'])); + + return $stmt->execute(compact('access_token')); + } + /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index eb024c57a..c274f9129 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -227,6 +227,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s ); } + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + /* ScopeInterface */ public function scopeExists($scope) { diff --git a/test/OAuth2/Controller/TokenControllerTest.php b/test/OAuth2/Controller/TokenControllerTest.php index d53181d34..4a217bd55 100644 --- a/test/OAuth2/Controller/TokenControllerTest.php +++ b/test/OAuth2/Controller/TokenControllerTest.php @@ -223,6 +223,54 @@ public function testCanReceiveAccessTokenUsingPasswordGrantTypeWithoutClientSecr $this->assertNotNull($response->getParameter('token_type')); } + public function testInvalidTokenTypeHintForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'foo', + 'token' => 'sometoken' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Token type hint must be either \'access_token\' or \'refresh_token\''); + } + + public function testMissingTokenForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing token parameter to revoke'); + } + + public function testInvalidRequestMethodForRevoke() + { + $server = $this->getTestServer(); + + $request = new TestRequest(); + $request->setQuery(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(405, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The request method must be POST when revoking an access token'); + } + public function testCreateController() { $storage = Bootstrap::getInstance()->getMemoryStorage(); diff --git a/test/OAuth2/ResponseType/AccessTokenTest.php b/test/OAuth2/ResponseType/AccessTokenTest.php new file mode 100644 index 000000000..0ed1c82fc --- /dev/null +++ b/test/OAuth2/ResponseType/AccessTokenTest.php @@ -0,0 +1,107 @@ + array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'access_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeRefreshTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeAccessTokenWithRefreshTokenTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } +} diff --git a/test/OAuth2/Storage/AccessTokenTest.php b/test/OAuth2/Storage/AccessTokenTest.php index e0b9d72ed..345daaee3 100644 --- a/test/OAuth2/Storage/AccessTokenTest.php +++ b/test/OAuth2/Storage/AccessTokenTest.php @@ -54,4 +54,29 @@ public function testSetAccessToken(AccessTokenInterface $storage) $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEOTHERID', $expires, ''); $this->assertTrue($success); } + + /** @dataProvider provideStorage */ + public function testUnsetAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $storage->unsetAccessToken('revokabletoken'); + + // assert token we are unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + } } From 1b2dcde189593c6baa92fffd667d314afa666b24 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 26 Aug 2015 17:19:25 -0700 Subject: [PATCH 09/12] skipping all dynamodb tests in travis --- test/lib/OAuth2/Storage/Bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 7497bedd0..efb6644c2 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -4,7 +4,7 @@ class Bootstrap { - const DYNAMODB_PHP_VERSION = '5.5'; + const DYNAMODB_PHP_VERSION = 'none'; protected static $instance; private $mysql; From 000affccb0064e93186dd06b19a0371a779d3151 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 6 Aug 2015 09:40:45 -0700 Subject: [PATCH 10/12] migrate travis for containers --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0f1b54e37..1baa3f63f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: php +sudo: false php: - 5.3 - 5.4 From 6d8fc0dd73523ab315b8ccee1d6627e587a4687c Mon Sep 17 00:00:00 2001 From: Jochen Niebuhr Date: Fri, 4 Sep 2015 14:06:34 +0200 Subject: [PATCH 11/12] Passing variables on MongoDB insert When running in hhvm passing a value as reference will result in a fatal error. --- src/OAuth2/Storage/Mongo.php | 79 +++++++++++++++++------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index fd5e7be36..95104477a 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -97,16 +97,15 @@ public function setClientDetails($client_id, $client_secret = null, $redirect_ur )) ); } else { - $this->collection('client_table')->insert( - array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - ) + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, ); + $this->collection('client_table')->insert($client); } return true; @@ -147,15 +146,14 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s )) ); } else { - $this->collection('access_token_table')->insert( - array( - 'access_token' => $access_token, - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - ) + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope ); + $this->collection('access_token_table')->insert($token); } return true; @@ -191,17 +189,16 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, )) ); } else { - $this->collection('code_table')->insert( - array( - 'authorization_code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - ) + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, ); + $this->collection('code_table')->insert($token); } return true; @@ -243,15 +240,14 @@ public function getRefreshToken($refresh_token) public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { - $this->collection('refresh_token_table')->insert( - array( - 'refresh_token' => $refresh_token, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'scope' => $scope - ) + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope ); + $this->collection('refresh_token_table')->insert($token); return true; } @@ -288,14 +284,13 @@ public function setUser($username, $password, $firstName = null, $lastName = nul )) ); } else { - $this->collection('user_table')->insert( - array( - 'username' => $username, - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - ) + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName ); + $this->collection('user_table')->insert($user); } return true; From 7fa4185b16447155590125523f9e57dba94d3f87 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 18 Sep 2015 11:02:16 -0700 Subject: [PATCH 12/12] Tag 1.8.0 Changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 197e95c40..03d925e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ 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.8.0 (2015-09-18) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/643 + + * bug #594 - adds jti + * bug #598 - fixes lifetime configurations for JWTs + * bug #634 - fixes travis builds, upgrade to containers + * bug #586 - support for revoking tokens + * bug #636 - Adds FirebaseJWT bridge + * bug #639 - Mongo HHVM compatibility + * 1.7.0 (2015-04-23) PR: https://github.com/bshaffer/oauth2-server-php/pull/572