From b88da1e2d6aa8a4fe4211e3ebb6c1ca17d821d07 Mon Sep 17 00:00:00 2001 From: Wilt Date: Thu, 24 Sep 2015 09:03:19 +0200 Subject: [PATCH 01/26] Allow null for client_secret Changed `client_secret` declaration in schema to allow NULL values --- src/OAuth2/Storage/Pdo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index b8fac34ab..e7f35f648 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -463,7 +463,7 @@ public function getBuildSql($dbName = 'oauth2_server_php') $sql = " CREATE TABLE {$this->config['client_table']} ( client_id VARCHAR(80) NOT NULL, - client_secret VARCHAR(80) NOT NULL, + client_secret VARCHAR(80), redirect_uri VARCHAR(2000), grant_types VARCHAR(80), scope VARCHAR(4000), From 05d4da8fdd544c3157d2fdef1b36356d9a09da71 Mon Sep 17 00:00:00 2001 From: Tobias Gassmann Date: Tue, 6 Oct 2015 16:11:51 +0200 Subject: [PATCH 02/26] fix bug in isPublicClient of Cassandra-Storage --- src/OAuth2/Storage/Cassandra.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index 4c661ed32..f8596ceb0 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -229,7 +229,7 @@ public function isPublicClient($client_id) return false; } - return empty($result['client_secret']);; + return empty($client['client_secret']);; } /* ClientInterface */ From 9a919d69d3a74f18e1e53bfd205f90065caefa61 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 6 Oct 2015 14:07:04 -0700 Subject: [PATCH 03/26] add isPublicClient tests and fixes resulting bugs --- src/OAuth2/Storage/DynamoDB.php | 15 ++++++++++----- src/OAuth2/Storage/Redis.php | 2 +- test/OAuth2/Storage/ClientTest.php | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index 596a31af5..d7f729276 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -124,7 +124,7 @@ public function getClientDetails($client_id) public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['client_table'], @@ -171,7 +171,7 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['access_token_table'], @@ -218,7 +218,7 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['code_table'], @@ -319,7 +319,7 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, function ($value) { return !empty($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['refresh_token_table'], @@ -366,7 +366,7 @@ public function setUser($username, $password, $first_name = null, $last_name = n $password = sha1($password); $clientData = compact('username', 'password', 'first_name', 'last_name'); - $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); + $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( 'TableName' => $this->config['user_table'], @@ -525,4 +525,9 @@ private function dynamo2array($dynamodbResult) return $result; } + + private static function isNotEmpty($value) + { + return null !== $value && '' !== $value; + } } diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index c274f9129..b9a6939b2 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($client['client_secret']); } /* ClientInterface */ diff --git a/test/OAuth2/Storage/ClientTest.php b/test/OAuth2/Storage/ClientTest.php index d4991c14f..6a5cc0b49 100644 --- a/test/OAuth2/Storage/ClientTest.php +++ b/test/OAuth2/Storage/ClientTest.php @@ -61,6 +61,29 @@ public function testGetAccessToken(ClientInterface $storage) $this->assertNotNull($details); } + /** @dataProvider provideStorage */ + public function testIsPublicClient(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + $publicClientId = 'public-client-'.rand(); + $confidentialClientId = 'confidential-client-'.rand(); + + // create a new client + $success1 = $storage->setClientDetails($publicClientId, ''); + $success2 = $storage->setClientDetails($confidentialClientId, 'some-secret'); + $this->assertTrue($success1); + $this->assertTrue($success2); + + // assert isPublicClient for both + $this->assertTrue($storage->isPublicClient($publicClientId)); + $this->assertFalse($storage->isPublicClient($confidentialClientId)); + } + /** @dataProvider provideStorage */ public function testSaveClient(ClientInterface $storage) { From 7c3fb1ea869cbd3bb4b11ae880905c27b5f79450 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 6 Oct 2015 14:12:29 -0700 Subject: [PATCH 04/26] add php7 to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f297d3f40..cf9479775 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ php: - 5.4 - 5.5 - 5.6 +- 7 - hhvm env: global: From e2aff18da2834759f0a4e05c6a068717f1e1cc79 Mon Sep 17 00:00:00 2001 From: afilippov1985 Date: Tue, 17 Nov 2015 14:22:19 +0400 Subject: [PATCH 05/26] Bug in client's scope restriction If set `scope` column in `oauth_clients` table to empty string, then client allowed to any scope even if it not exists in `oauth_scopes` table. --- src/OAuth2/Controller/AuthorizeController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index 7ea3fb8cf..fe175ee0c 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -236,8 +236,8 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte // restrict scope by client specific scope if applicable, // otherwise verify the scope exists $clientScope = $this->clientStorage->getClientScope($client_id); - if ((is_null($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) - || ($clientScope && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { + if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) + || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); return false; From df182af6e41839e4b4117682eb774c1c2825cb58 Mon Sep 17 00:00:00 2001 From: "ashiina@mac-local" Date: Wed, 18 Nov 2015 17:23:50 +0900 Subject: [PATCH 06/26] Pdo,DynamoDB,Cassandra: Implemented method to change the password hashing algorithm --- src/OAuth2/Storage/Cassandra.php | 10 ++++++++-- src/OAuth2/Storage/DynamoDB.php | 10 ++++++++-- src/OAuth2/Storage/Pdo.php | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index f8596ceb0..5f7068af0 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -182,7 +182,13 @@ public function checkUserCredentials($username, $password) // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { - return $user['password'] == sha1($password); + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); } public function getUserDetails($username) @@ -204,7 +210,7 @@ public function getUser($username) public function setUser($username, $password, $first_name = null, $last_name = null) { - $password = sha1($password); + $password = $this->hashPassword($password); return $this->setValue( $this->config['user_key'] . $username, diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index d7f729276..a1d35baff 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -342,7 +342,13 @@ public function unsetRefreshToken($refresh_token) // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { - return $user['password'] == sha1($password); + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); } public function getUser($username) @@ -363,7 +369,7 @@ public function getUser($username) public function setUser($username, $password, $first_name = null, $last_name = null) { // do not store in plaintext - $password = sha1($password); + $password = $this->hashPassword($password); $clientData = compact('username', 'password', 'first_name', 'last_name'); $clientData = array_filter($clientData, 'self::isNotEmpty'); diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index e7f35f648..4685b8530 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -307,7 +307,13 @@ public function unsetRefreshToken($refresh_token) // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { - return $user['password'] == sha1($password); + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); } public function getUser($username) @@ -328,7 +334,7 @@ public function getUser($username) public function setUser($username, $password, $firstName = null, $lastName = null) { // do not store in plaintext - $password = sha1($password); + $password = $this->hashPassword($password); // if it exists, update it. if ($this->getUser($username)) { From 1ef7fa85ac4ffb0dd2b909efbcd47a86e5fdb612 Mon Sep 17 00:00:00 2001 From: Andrey Filippov Date: Thu, 19 Nov 2015 10:33:21 +0400 Subject: [PATCH 07/26] Fix typo in PDO storage DDL --- src/OAuth2/Storage/Pdo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index e7f35f648..9b030b058 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -525,7 +525,7 @@ public function getBuildSql($dbName = 'oauth2_server_php') CREATE TABLE {$this->config['jti_table']} ( issuer VARCHAR(80) NOT NULL, subject VARCHAR(80), - audiance VARCHAR(80), + audience VARCHAR(80), expires TIMESTAMP NOT NULL, jti VARCHAR(2000) NOT NULL ); From 71f50ea281e1d5c42d633c134d164d21047fde00 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 8 Dec 2015 11:09:32 -0800 Subject: [PATCH 08/26] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cf9479775..7b30e7f6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ php: - 5.4 - 5.5 - 5.6 -- 7 +- 7.0 - hhvm env: global: From 4c048fd4e2b12f73046a5db21e3ca68f581597c9 Mon Sep 17 00:00:00 2001 From: Andrey Filippov Date: Mon, 14 Dec 2015 14:54:03 +0400 Subject: [PATCH 09/26] Add test data for CouchbaseDB and Mongo --- test/lib/OAuth2/Storage/Bootstrap.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index efb6644c2..4ac9022b1 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -421,8 +421,10 @@ private function createCouchbaseDB(\Couchbase $db) ))); $db->set('oauth_users-testuser',json_encode(array( - 'username' => "testuser", - 'password' => "password" + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, ))); $db->set('oauth_jwt-oauth_test_client',json_encode(array( @@ -460,8 +462,10 @@ private function createMongoDb(\MongoDB $db) )); $db->oauth_users->insert(array( - 'username' => "testuser", - 'password' => "password" + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, )); $db->oauth_jwt->insert(array( From c7eee7a840878ac4bf94c191d998e3d043a77e83 Mon Sep 17 00:00:00 2001 From: Maks Date: Mon, 9 Nov 2015 20:23:08 +0000 Subject: [PATCH 10/26] [composer] Consolidate dev dependencies [Travis] Cache vendors [Travis] Add PHP 7 to test matrix [Travis] Speedup tests (disable Xdebug) --- .travis.yml | 14 +++++++++----- composer.json | 6 ++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b30e7f6c..dd4aae4a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,15 @@ language: php sudo: false +cache: + directories: + - $HOME/.composer/cache + - vendor php: - 5.3 - 5.4 - 5.5 - 5.6 -- 7.0 +- 7 - hhvm env: global: @@ -16,11 +20,11 @@ services: - mongodb - redis-server - cassandra +before_install: +- phpenv config-rm xdebug.ini || return 0 +install: +- composer install --no-interaction 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:~2.8' -- composer require 'firebase/php-jwt:~2.2' after_script: - php test/cleanup.php diff --git a/composer.json b/composer.json index 853c04164..b1f28c707 100644 --- a/composer.json +++ b/composer.json @@ -23,5 +23,11 @@ "thobbs/phpcassa": "Required to use the Cassandra storage engine", "aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine", "firebase/php-jwt": "~2.2 is required to use JWT features" + }, + "require-dev": { + "aws/aws-sdk-php": "~2.8", + "firebase/php-jwt": "~2.2", + "predis/predis": "dev-master", + "thobbs/phpcassa": "dev-master" } } From e6b1e69b92eb05d752cd6ee566526a774ec9b2ab Mon Sep 17 00:00:00 2001 From: Maks Date: Mon, 9 Nov 2015 22:30:44 +0000 Subject: [PATCH 11/26] Fix for #663 and Test case --- src/OAuth2/Controller/AuthorizeController.php | 7 ++++++- .../Controller/AuthorizeControllerTest.php | 16 ++++++++++++++++ test/config/storage.json | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index fe175ee0c..a9a722587 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -341,9 +341,14 @@ protected function validateRedirectUri($inputUri, $registeredUriString) return true; } } else { + $registered_uri_length = strlen($registered_uri); + if ($registered_uri_length === 0) { + return false; + } + // the input uri is validated against the registered uri using case-insensitive match of the initial string // i.e. additional query parameters may be applied - if (strcasecmp(substr($inputUri, 0, strlen($registered_uri)), $registered_uri) === 0) { + if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) { return true; } } diff --git a/test/OAuth2/Controller/AuthorizeControllerTest.php b/test/OAuth2/Controller/AuthorizeControllerTest.php index c2745d671..3bfc760e4 100644 --- a/test/OAuth2/Controller/AuthorizeControllerTest.php +++ b/test/OAuth2/Controller/AuthorizeControllerTest.php @@ -178,6 +178,22 @@ public function testInvalidRedirectUri() $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); } + public function testInvalidRedirectUriApprovedByBuggyRegisteredUri() + { + $server = $this->getTestServer(); + $server->setConfig('require_exact_redirect_uri', false); + $request = new Request(array( + 'client_id' => 'Test Client ID with Buggy Redirect Uri', // valid client id + 'redirect_uri' => 'http://adobe.com', // invalid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + public function testNoRedirectUriWithMultipleRedirectUris() { $server = $this->getTestServer(); diff --git a/test/config/storage.json b/test/config/storage.json index 2d43c0789..a31d3bca2 100644 --- a/test/config/storage.json +++ b/test/config/storage.json @@ -42,6 +42,10 @@ "client_secret": "TestSecret2", "redirect_uri": "http://brentertainment.com" }, + "Test Client ID with Buggy Redirect Uri": { + "client_secret": "TestSecret2", + "redirect_uri": " http://brentertainment.com" + }, "Test Client ID with Multiple Redirect Uris": { "client_secret": "TestSecret3", "redirect_uri": "http://brentertainment.com http://morehazards.com" From 9af98ef1d9d88b62ad75731faaa3a85f19f7c172 Mon Sep 17 00:00:00 2001 From: Kenneth Shaw Date: Tue, 12 Jan 2016 14:03:52 +0700 Subject: [PATCH 12/26] Fixing minor typo in Implicit grant type test --- test/OAuth2/GrantType/ImplicitTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OAuth2/GrantType/ImplicitTest.php b/test/OAuth2/GrantType/ImplicitTest.php index a312ebc99..a47aae3e8 100644 --- a/test/OAuth2/GrantType/ImplicitTest.php +++ b/test/OAuth2/GrantType/ImplicitTest.php @@ -125,7 +125,7 @@ public function testSuccessfulRequestStripsExtraParameters() $this->assertArrayHasKey('fragment', $parts); parse_str($parts['fragment'], $params); - $this->assertFalse(isset($parmas['fake'])); + $this->assertFalse(isset($params['fake'])); $this->assertArrayHasKey('state', $params); $this->assertEquals($params['state'], 'test'); } From 70e3b8d1ce42ff7e737bf7f36f10e5db289dfd2e Mon Sep 17 00:00:00 2001 From: Ken Morishita Date: Tue, 13 Oct 2015 16:02:04 +0900 Subject: [PATCH 13/26] fix: Token Response's Content-Type to application/json fix: at_hash digest computation('hash' function must be used with raw_output=true ) --- src/OAuth2/Controller/TokenController.php | 6 +++++- src/OAuth2/OpenID/ResponseType/IdToken.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index 2e0750f6a..42dab892f 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -51,7 +51,11 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface // server MUST disable caching in headers when tokens are involved $response->setStatusCode(200); $response->addParameters($token); - $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache')); + $response->addHttpHeaders(array( + 'Cache-Control' => 'no-store', + 'Pragma' => 'no-cache', + 'Content-Type' => 'application/json' + )); } } diff --git a/src/OAuth2/OpenID/ResponseType/IdToken.php b/src/OAuth2/OpenID/ResponseType/IdToken.php index 469615586..97777fbf2 100644 --- a/src/OAuth2/OpenID/ResponseType/IdToken.php +++ b/src/OAuth2/OpenID/ResponseType/IdToken.php @@ -84,7 +84,7 @@ protected function createAtHash($access_token, $client_id = null) // maps HS256 and RS256 to sha256, etc. $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); $hash_algorithm = 'sha' . substr($algorithm, 2); - $hash = hash($hash_algorithm, $access_token); + $hash = hash($hash_algorithm, $access_token, true); $at_hash = substr($hash, 0, strlen($hash) / 2); return $this->encryptionUtil->urlSafeB64Encode($at_hash); From 0649e898437b81b3df58547e2d4776433f7cd0d5 Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 8 Jun 2016 21:28:13 +1000 Subject: [PATCH 14/26] Add missing array_merge() parameter --- src/OAuth2/Storage/Memory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 4f0859deb..52afcab9c 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -142,7 +142,7 @@ public function getUserClaims($user_id, $claims) // address is an object with subfields $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); } else { - $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails)); + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); } } } From 8a68f3810b9add30108bd030629f4b478760d40e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 15 Jul 2016 10:57:03 -0700 Subject: [PATCH 15/26] Fix UserClaims for CodeIdToken (#749) Remove dereferencing for support PHP 5.3 adds tests --- .../OpenID/ResponseType/CodeIdToken.php | 4 +- .../OpenID/ResponseType/CodeIdTokenTest.php | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/OAuth2/OpenID/ResponseType/CodeIdToken.php b/src/OAuth2/OpenID/ResponseType/CodeIdToken.php index 9bc3322e8..ac7764d6c 100644 --- a/src/OAuth2/OpenID/ResponseType/CodeIdToken.php +++ b/src/OAuth2/OpenID/ResponseType/CodeIdToken.php @@ -16,8 +16,8 @@ public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterfa public function getAuthorizeResponse($params, $user_id = null) { $result = $this->authCode->getAuthorizeResponse($params, $user_id); - $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce']); - $result[1]['query']['id_token'] = $id_token; + $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id); + $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token']; return $result; } diff --git a/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php b/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php index 5daaaa68e..b0311434a 100644 --- a/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php +++ b/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php @@ -67,6 +67,96 @@ public function testHandleAuthorizeRequest() $this->assertEquals($duration, 3600); } + public function testUserClaimsWithUserId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $userId = 'testuser'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // 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); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('email', $claims); + $this->assertArrayHasKey('email_verified', $claims); + $this->assertNotNull($claims['email']); + $this->assertNotNull($claims['email_verified']); + } + + public function testUserClaimsWithoutUserId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $userId = null; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // 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); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayNotHasKey('email', $claims); + $this->assertArrayNotHasKey('email_verified', $claims); + } + private function getTestServer($config = array()) { $config += array( @@ -77,6 +167,7 @@ private function getTestServer($config = array()) ); $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $memoryStorage->supportedScopes[] = 'email'; $responseTypes = array( 'code' => $code = new AuthorizationCode($memoryStorage), 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), From aa268911bbbdefbaad1c7d3b67cfe3dfc98f6c54 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 27 Jul 2016 09:43:51 -0700 Subject: [PATCH 16/26] Issue 725 (#729) * ensures unsetAccessToken and unsetRefreshToken return a bool --- src/OAuth2/Storage/AccessTokenInterface.php | 1 + src/OAuth2/Storage/Cassandra.php | 17 +++++++++----- src/OAuth2/Storage/DynamoDB.php | 5 ++-- src/OAuth2/Storage/Memory.php | 16 +++++++++++-- src/OAuth2/Storage/Mongo.php | 12 +++++++--- src/OAuth2/Storage/Pdo.php | 8 +++++-- src/OAuth2/Storage/Redis.php | 8 +++++-- test/OAuth2/Storage/AccessTokenTest.php | 26 ++++++++++++++++++--- 8 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php index ac081bb6c..1819158af 100644 --- a/src/OAuth2/Storage/AccessTokenInterface.php +++ b/src/OAuth2/Storage/AccessTokenInterface.php @@ -55,6 +55,7 @@ public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $sc * @param $access_token * Access token to be expired. * + * @return BOOL true if an access token was unset, false if not * @ingroup oauth2_section_6 * * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index 5f7068af0..602e8a058 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -136,14 +136,19 @@ protected function expireValue($key) unset($this->cache[$key]); $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); - try { - // __data key set as C* requires a field - $cf->remove($key, array('__data')); - } catch (\Exception $e) { - return false; + + if ($cf->get_count($key) > 0) { + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch (\Exception $e) { + return false; + } + + return true; } - return true; + return false; } /* AuthorizationCodeInterface */ diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index a1d35baff..8347ab258 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -186,10 +186,11 @@ 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)) + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)), + 'ReturnValues' => 'ALL_OLD', )); - return true; + return null !== $result->get('Attributes'); } /* OAuth2\Storage\AuthorizationCodeInterface */ diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 52afcab9c..42d833ccb 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -236,7 +236,13 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - unset($this->refreshTokens[$refresh_token]); + if (isset($this->refreshTokens[$refresh_token])) { + unset($this->refreshTokens[$refresh_token]); + + return true; + } + + return false; } public function setRefreshTokens($refresh_tokens) @@ -259,7 +265,13 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s public function unsetAccessToken($access_token) { - unset($this->accessTokens[$access_token]); + if (isset($this->accessTokens[$access_token])) { + unset($this->accessTokens[$access_token]); + + return true; + } + + return false; } public function scopeExists($scope) diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 95104477a..cef35e5e9 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -161,7 +161,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s public function unsetAccessToken($access_token) { - $this->collection('access_token_table')->remove(array('access_token' => $access_token)); + $result = $this->collection('access_token_table')->remove(array( + 'access_token' => $access_token + ), array('w' => 1)); + + return $result['n'] > 0; } @@ -254,9 +258,11 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - $this->collection('refresh_token_table')->remove(array('refresh_token' => $refresh_token)); + $result = $this->collection('refresh_token_table')->remove(array( + 'refresh_token' => $refresh_token + ), array('w' => 1)); - return true; + return $result['n'] > 0; } // plaintext passwords are bad! Override this for your application diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index f8948835b..ae5107e29 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -160,7 +160,9 @@ 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')); + $stmt->execute(compact('access_token')); + + return $stmt->rowCount() > 0; } /* OAuth2\Storage\AuthorizationCodeInterface */ @@ -301,7 +303,9 @@ public function unsetRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); - return $stmt->execute(compact('refresh_token')); + $stmt->execute(compact('refresh_token')); + + return $stmt->rowCount() > 0; } // plaintext passwords are bad! Override this for your application diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index b9a6939b2..e6294e22d 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -209,7 +209,9 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + + return $result > 0; } /* AccessTokenInterface */ @@ -229,7 +231,9 @@ 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); + $result = $this->expireValue($this->config['access_token_key'] . $access_token); + + return $result > 0; } /* ScopeInterface */ diff --git a/test/OAuth2/Storage/AccessTokenTest.php b/test/OAuth2/Storage/AccessTokenTest.php index 345daaee3..b34e0bfc0 100644 --- a/test/OAuth2/Storage/AccessTokenTest.php +++ b/test/OAuth2/Storage/AccessTokenTest.php @@ -64,7 +64,7 @@ public function testUnsetAccessToken(AccessTokenInterface $storage) return; } - // assert token we are unset does not exist + // assert token we are about to unset does not exist $token = $storage->getAccessToken('revokabletoken'); $this->assertFalse($token); @@ -73,10 +73,30 @@ public function testUnsetAccessToken(AccessTokenInterface $storage) $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires); $this->assertTrue($success); - $storage->unsetAccessToken('revokabletoken'); + // assert unsetAccessToken returns true + $result = $storage->unsetAccessToken('revokabletoken'); + $this->assertTrue($result); - // assert token we are unset does not exist + // assert token we unset does not exist $token = $storage->getAccessToken('revokabletoken'); $this->assertFalse($token); } + + /** @dataProvider provideStorage */ + public function testUnsetAccessTokenReturnsFalse(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to unset does not exist + $token = $storage->getAccessToken('nonexistanttoken'); + $this->assertFalse($token); + + // assert unsetAccessToken returns false + $result = $storage->unsetAccessToken('nonexistanttoken'); + $this->assertFalse($result); + } } From 679167c8b14552315de5e7a45514135bd50ea767 Mon Sep 17 00:00:00 2001 From: hongjinlin <89757630@qq.com> Date: Wed, 16 Nov 2016 09:27:24 +0800 Subject: [PATCH 17/26] Remove extra semicolon (#781) --- src/OAuth2/Storage/Cassandra.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index 602e8a058..c5048c08d 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -240,7 +240,7 @@ public function isPublicClient($client_id) return false; } - return empty($client['client_secret']);; + return empty($client['client_secret']); } /* ClientInterface */ From 94caf2e255caaa6f377c284a351b416916a12b87 Mon Sep 17 00:00:00 2001 From: Chad Gray Date: Thu, 1 Dec 2016 14:54:27 -0500 Subject: [PATCH 18/26] Add .gitattributes files (#783) The .gitattributes file will prevent items such as test files, yaml files, etc from being included when this library is imported via composer --- .gitattributes | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..3c4c6fced --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Exclude unused files +# see: https://redd.it/2jzp6k +/tests export-ignore +.travis.yml export-ignore +.gitattributes export-ignore +.gitignore export-ignore +phpunit.xml export-ignore From dfef78a214423edfa64e43eb33d0b85de90aff84 Mon Sep 17 00:00:00 2001 From: Piotr G Date: Fri, 23 Dec 2016 00:44:17 +0100 Subject: [PATCH 19/26] RFC6750 compatibility (#784) --- src/OAuth2/Controller/ResourceController.php | 2 +- test/OAuth2/Controller/ResourceControllerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OAuth2/Controller/ResourceController.php b/src/OAuth2/Controller/ResourceController.php index e8588188f..3cfaaaf12 100644 --- a/src/OAuth2/Controller/ResourceController.php +++ b/src/OAuth2/Controller/ResourceController.php @@ -83,7 +83,7 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")'); } elseif (time() > $token["expires"]) { - $response->setError(401, 'expired_token', 'The access token provided has expired'); + $response->setError(401, 'invalid_token', 'The access token provided has expired'); } else { return $token; } diff --git a/test/OAuth2/Controller/ResourceControllerTest.php b/test/OAuth2/Controller/ResourceControllerTest.php index ee6d96ff8..ca602939a 100644 --- a/test/OAuth2/Controller/ResourceControllerTest.php +++ b/test/OAuth2/Controller/ResourceControllerTest.php @@ -100,7 +100,7 @@ public function testExpiredToken() $this->assertFalse($allow); $this->assertEquals($response->getStatusCode(), 401); - $this->assertEquals($response->getParameter('error'), 'expired_token'); + $this->assertEquals($response->getParameter('error'), 'invalid_token'); $this->assertEquals($response->getParameter('error_description'), 'The access token provided has expired'); } From c1438af70abfaf67e457f61f2762c7d170f6b3a4 Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Fri, 23 Dec 2016 00:46:04 +0100 Subject: [PATCH 20/26] Fix "redirect_uri_mismatch" for URIs with encoded characters (#776) * add test to showcase a bug when comparaing redirect_uri in grant type "code" * fix redirect_uri comparaison with grant type "code" --- src/OAuth2/GrantType/AuthorizationCode.php | 2 +- test/OAuth2/GrantType/AuthorizationCodeTest.php | 16 ++++++++++++++++ test/config/storage.json | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/OAuth2/GrantType/AuthorizationCode.php b/src/OAuth2/GrantType/AuthorizationCode.php index e8995204c..9069c0020 100644 --- a/src/OAuth2/GrantType/AuthorizationCode.php +++ b/src/OAuth2/GrantType/AuthorizationCode.php @@ -49,7 +49,7 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 */ if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { - if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != urldecode($authCode['redirect_uri'])) { $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); return false; diff --git a/test/OAuth2/GrantType/AuthorizationCodeTest.php b/test/OAuth2/GrantType/AuthorizationCodeTest.php index 740989635..356b8e53c 100644 --- a/test/OAuth2/GrantType/AuthorizationCodeTest.php +++ b/test/OAuth2/GrantType/AuthorizationCodeTest.php @@ -93,6 +93,22 @@ public function testValidCode() $this->assertArrayHasKey('access_token', $token); } + public function testValidRedirectUri() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://brentertainment.com/voil%C3%A0', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-redirect-uri', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + public function testValidCodeNoScope() { $server = $this->getTestServer(); diff --git a/test/config/storage.json b/test/config/storage.json index a31d3bca2..52d3f2399 100644 --- a/test/config/storage.json +++ b/test/config/storage.json @@ -32,6 +32,13 @@ "redirect_uri": "", "expires": "9999999999", "id_token": "test_id_token" + }, + "testcode-redirect-uri": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "http://brentertainment.com/voil%C3%A0", + "expires": "9999999999", + "id_token": "IDTOKEN" } }, "client_credentials" : { From eaf82a71fb23918c9b58e1ed6930991818277446 Mon Sep 17 00:00:00 2001 From: Tymoteusz Motylewski Date: Fri, 23 Dec 2016 00:55:07 +0100 Subject: [PATCH 21/26] Fix doc comments and double quoted strings (#762) Fixed some doc comments to make working with the project easier in the IDE. Also changed some strings to single quoted to prevent interpreting backslash as escape character. --- src/OAuth2/Controller/AuthorizeController.php | 5 + src/OAuth2/Controller/TokenController.php | 35 +++++-- src/OAuth2/GrantType/AuthorizationCode.php | 2 +- src/OAuth2/Server.php | 99 ++++++++++++++----- 4 files changed, 105 insertions(+), 36 deletions(-) diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index a9a722587..bda41d288 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -126,6 +126,11 @@ protected function buildAuthorizeParameters($request, $response, $user_id) return $params; } + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) { // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index 42dab892f..5d2d731fe 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -12,14 +12,33 @@ use OAuth2\ResponseInterface; /** - * @see OAuth2\Controller\TokenControllerInterface + * @see \OAuth2\Controller\TokenControllerInterface */ class TokenController implements TokenControllerInterface { + /** + * @var AccessTokenInterface + */ protected $accessToken; + + /** + * @var array + */ protected $grantTypes; + + /** + * @var ClientAssertionTypeInterface + */ protected $clientAssertionType; + + /** + * @var Scope|ScopeInterface + */ protected $scopeUtil; + + /** + * @var ClientInterface + */ protected $clientStorage; public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null) @@ -64,11 +83,11 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface * This would be called from the "/token" endpoint as defined in the spec. * You can call your endpoint whatever you want. * - * @param $request - RequestInterface - * Request object to grant access token + * @param RequestInterface $request Request object to grant access token + * @param ResponseInterface $response * - * @throws InvalidArgumentException - * @throws LogicException + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-10.6 @@ -208,10 +227,8 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r /** * addGrantType * - * @param grantType - OAuth2\GrantTypeInterface - * the grant type to add for the specified identifier - * @param identifier - string - * a string passed in as "grant_type" in the response that will call this grantType + * @param GrantTypeInterface $grantType the grant type to add for the specified identifier + * @param string $identifier a string passed in as "grant_type" in the response that will call this grantType */ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) { diff --git a/src/OAuth2/GrantType/AuthorizationCode.php b/src/OAuth2/GrantType/AuthorizationCode.php index 9069c0020..cae9f787d 100644 --- a/src/OAuth2/GrantType/AuthorizationCode.php +++ b/src/OAuth2/GrantType/AuthorizationCode.php @@ -17,7 +17,7 @@ class AuthorizationCode implements GrantTypeInterface protected $authCode; /** - * @param OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information + * @param \OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information */ public function __construct(AuthorizationCodeInterface $storage) { diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index 171a4f069..9cfcb83a5 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -47,20 +47,50 @@ class Server implements ResourceControllerInterface, UserInfoControllerInterface { // misc properties + /** + * @var Response + */ protected $response; + + /** + * @var array + */ protected $config; + + /** + * @var array + */ protected $storages; // servers + /** + * @var AuthorizeControllerInterface + */ protected $authorizeController; + + /** + * @var TokenControllerInterface + */ protected $tokenController; + + /** + * @var ResourceControllerInterface + */ protected $resourceController; + + /** + * @var UserInfoControllerInterface + */ protected $userInfoController; // config classes protected $grantTypes; protected $responseTypes; protected $tokenType; + + /** + * @var ScopeInterface + */ protected $scopeUtil; protected $clientAssertionType; @@ -92,9 +122,9 @@ class Server implements ResourceControllerInterface, * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens * @param array $responseTypes Response types to use. array keys should be "code" and and "token" for * Access Token and Authorization Code response types - * @param OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" - * @param OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope - * @param OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic + * @param \OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" + * @param \OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope + * @param \OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic * * @ingroup oauth2_section_7 */ @@ -180,6 +210,8 @@ public function getUserInfoController() /** * every getter deserves a setter + * + * @param AuthorizeControllerInterface $authorizeController */ public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) { @@ -188,6 +220,8 @@ public function setAuthorizeController(AuthorizeControllerInterface $authorizeCo /** * every getter deserves a setter + * + * @param TokenControllerInterface $tokenController */ public function setTokenController(TokenControllerInterface $tokenController) { @@ -196,6 +230,8 @@ public function setTokenController(TokenControllerInterface $tokenController) /** * every getter deserves a setter + * + * @param ResourceControllerInterface $resourceController */ public function setResourceController(ResourceControllerInterface $resourceController) { @@ -204,6 +240,8 @@ public function setResourceController(ResourceControllerInterface $resourceContr /** * every getter deserves a setter + * + * @param UserInfoControllerInterface $userInfoController */ public function setUserInfoController(UserInfoControllerInterface $userInfoController) { @@ -214,14 +252,16 @@ public function setUserInfoController(UserInfoControllerInterface $userInfoContr * Return claims about the authenticated end-user. * This would be called from the "/UserInfo" endpoint as defined in the spec. * - * @param $request - OAuth2\RequestInterface + * @param $request - \OAuth2\RequestInterface * Request object to grant access token * - * @param $response - OAuth2\ResponseInterface + * @param $response - \OAuth2\ResponseInterface * Response object containing error messages (failure) or user claims (success) * - * @throws InvalidArgumentException - * @throws LogicException + * @return ResponseInterface + * + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo */ @@ -238,14 +278,16 @@ public function handleUserInfoRequest(RequestInterface $request, ResponseInterfa * This would be called from the "/token" endpoint as defined in the spec. * Obviously, you can call your endpoint whatever you want. * - * @param $request - OAuth2\RequestInterface + * @param $request - \OAuth2\RequestInterface * Request object to grant access token * - * @param $response - OAuth2\ResponseInterface + * @param $response - \OAuth2\ResponseInterface * Response object containing error messages (failure) or access token (success) * - * @throws InvalidArgumentException - * @throws LogicException + * @return ResponseInterface + * + * @throws \InvalidArgumentException + * @throws \LogicException * * @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-10.6 @@ -306,11 +348,14 @@ public function handleRevokeRequest(RequestInterface $request, ResponseInterface * list of space-delimited strings. * - state: (optional) An opaque value used by the client to maintain * state between the request and callback. + * @param ResponseInterface $response * @param $is_authorized * TRUE or FALSE depending on whether the user authorized the access. * @param $user_id * Identifier of user who authorized the client * + * @return Response + * * @see http://tools.ietf.org/html/rfc6749#section-4 * * @ingroup oauth2_section_4 @@ -464,6 +509,8 @@ public function getScopeUtil() /** * every getter deserves a setter + * + * @param ScopeInterface $scopeUtil */ public function setScopeUtil($scopeUtil) { @@ -473,7 +520,7 @@ public function setScopeUtil($scopeUtil) protected function createDefaultAuthorizeController() { if (!isset($this->storages['client'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the authorize server"); + throw new \LogicException('You must supply a storage object implementing \OAuth2\Storage\ClientInterface to use the authorize server'); } if (0 == count($this->responseTypes)) { $this->responseTypes = $this->getDefaultResponseTypes(); @@ -505,7 +552,7 @@ protected function createDefaultTokenController() foreach ($this->grantTypes as $grantType) { if (!$grantType instanceof ClientAssertionTypeInterface) { if (!isset($this->storages['client_credentials'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server'); } $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); @@ -515,7 +562,7 @@ protected function createDefaultTokenController() } if (!isset($this->storages['client'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server'); } $accessTokenResponseType = $this->getAccessTokenResponseType(); @@ -531,7 +578,7 @@ protected function createDefaultResourceController() $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 JwtAccessTokens 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) { @@ -551,11 +598,11 @@ protected function createDefaultUserInfoController() $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 JwtAccessTokens to use the UserInfo server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server'); } if (!isset($this->storages['user_claims'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server"); + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server'); } if (!$this->tokenType) { @@ -593,7 +640,7 @@ protected function getDefaultResponseTypes() $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); if ($this->config['use_openid_connect']) { if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { - throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); } $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']); @@ -603,7 +650,7 @@ protected function getDefaultResponseTypes() } if (count($responseTypes) == 0) { - throw new \LogicException("You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set 'allow_implicit' to true and implement a OAuth2\Storage\AccessTokenInterface storage object"); + throw new \LogicException('You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set "allow_implicit" to true and implement a OAuth2\Storage\AccessTokenInterface storage object'); } return $responseTypes; @@ -630,7 +677,7 @@ protected function getDefaultGrantTypes() if (isset($this->storages['authorization_code'])) { if ($this->config['use_openid_connect']) { if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { - throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true"); + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); } $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); } else { @@ -639,7 +686,7 @@ protected function getDefaultGrantTypes() } if (count($grantTypes) == 0) { - throw new \LogicException("Unable to build default grant types - You must supply an array of grant_types in the constructor"); + throw new \LogicException('Unable to build default grant types - You must supply an array of grant_types in the constructor'); } return $grantTypes; @@ -682,7 +729,7 @@ protected function getIdTokenTokenResponseType() protected function createDefaultJwtAccessTokenStorage() { if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); } $tokenStorage = null; if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { @@ -698,7 +745,7 @@ protected function createDefaultJwtAccessTokenStorage() protected function createDefaultJwtAccessTokenResponseType() { if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); } $tokenStorage = null; @@ -719,7 +766,7 @@ protected function createDefaultJwtAccessTokenResponseType() protected function createDefaultAccessTokenResponseType() { if (!isset($this->storages['access_token'])) { - throw new \LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server"); + throw new \LogicException('You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server'); } $refreshStorage = null; @@ -736,10 +783,10 @@ protected function createDefaultAccessTokenResponseType() protected function createDefaultIdTokenResponseType() { if (!isset($this->storages['user_claims'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect"); + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect'); } if (!isset($this->storages['public_key'])) { - throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect'); } $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); From bdb91b3868134d6010eceb144e6d23e374ece602 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 22 Dec 2016 15:58:29 -0800 Subject: [PATCH 22/26] no access token supplied to resource controller results in emptry request body (#759) --- src/OAuth2/Response.php | 2 +- test/OAuth2/Controller/ResourceControllerTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OAuth2/Response.php b/src/OAuth2/Response.php index d8eabe79e..fc1e62a98 100644 --- a/src/OAuth2/Response.php +++ b/src/OAuth2/Response.php @@ -176,7 +176,7 @@ public function getResponseBody($format = 'json') { switch ($format) { case 'json': - return json_encode($this->parameters); + return $this->parameters ? json_encode($this->parameters) : ''; case 'xml': // this only works for single-level arrays $xml = new \SimpleXMLElement(''); diff --git a/test/OAuth2/Controller/ResourceControllerTest.php b/test/OAuth2/Controller/ResourceControllerTest.php index ca602939a..b277514a5 100644 --- a/test/OAuth2/Controller/ResourceControllerTest.php +++ b/test/OAuth2/Controller/ResourceControllerTest.php @@ -20,6 +20,7 @@ public function testNoAccessToken() $this->assertEquals($response->getStatusCode(), 401); $this->assertNull($response->getParameter('error')); $this->assertNull($response->getParameter('error_description')); + $this->assertEquals('', $response->getResponseBody()); } public function testMalformedHeader() From 912477710cc09773b61cc43690427191fbfacfec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C3=A0o=20Ho=C3=A0ng=20S=C6=A1n?= Date: Fri, 23 Dec 2016 06:48:02 +0700 Subject: [PATCH 23/26] Use OpenSSL random method before attempting Mcrypt's. (#773) --- src/OAuth2/ResponseType/AccessToken.php | 8 ++++---- src/OAuth2/ResponseType/AuthorizationCode.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OAuth2/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php index b235ad0c5..98f51218f 100644 --- a/src/OAuth2/ResponseType/AccessToken.php +++ b/src/OAuth2/ResponseType/AccessToken.php @@ -114,14 +114,14 @@ public function createAccessToken($client_id, $user_id, $scope = null, $includeR */ protected function generateAccessToken() { - if (function_exists('mcrypt_create_iv')) { - $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(20); if ($randomData !== false && strlen($randomData) === 20) { return bin2hex($randomData); } } - if (function_exists('openssl_random_pseudo_bytes')) { - $randomData = openssl_random_pseudo_bytes(20); + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); if ($randomData !== false && strlen($randomData) === 20) { return bin2hex($randomData); } diff --git a/src/OAuth2/ResponseType/AuthorizationCode.php b/src/OAuth2/ResponseType/AuthorizationCode.php index 6a305fd75..52aeb4be5 100644 --- a/src/OAuth2/ResponseType/AuthorizationCode.php +++ b/src/OAuth2/ResponseType/AuthorizationCode.php @@ -85,10 +85,10 @@ public function enforceRedirect() protected function generateAuthorizationCode() { $tokenLen = 40; - if (function_exists('mcrypt_create_iv')) { - $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); - } elseif (function_exists('openssl_random_pseudo_bytes')) { + if (function_exists('openssl_random_pseudo_bytes')) { $randomData = openssl_random_pseudo_bytes(100); + } elseif (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); } else { From d9f23541fd1c26a316cf109b259ab885f8f58fa1 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 22 Dec 2016 16:38:45 -0800 Subject: [PATCH 24/26] Adds PHP 7.1 and properly runs mongo.so tests on php 5.6 and below. (#789) --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd4aae4a6..6bc9251da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,11 @@ php: - 5.4 - 5.5 - 5.6 -- 7 +- 7.0 +- 7.1 - hhvm env: global: - - SKIP_MONGO_TESTS=1 - secure: Bc5ZqvZ1YYpoPZNNuU2eCB8DS6vBYrAdfBtTenBs5NSxzb+Vjven4kWakbzaMvZjb/Ib7Uph7DGuOtJXpmxnvBXPLd707LZ89oFWN/yqQlZKCcm8iErvJCB5XL+/ONHj2iPdR242HJweMcat6bMCwbVWoNDidjtWMH0U2mYFy3M= - secure: R3bXlymyFiY2k2jf7+fv/J8i34wtXTkmD4mCr5Ps/U+vn9axm2VtvR2Nj+r7LbRjn61gzFE/xIVjYft/wOyBOYwysrfriydrnRVS0owh6y+7EyOyQWbRX11vVQMf8o31QCQE5BY58V5AJZW3MjoOL0FVlTgySJiJvdw6Pv18v+E= services: @@ -26,5 +26,6 @@ install: - composer install --no-interaction before_script: - psql -c 'create database oauth2_server_php;' -U postgres +- phpenv version-name | grep ^5.[3-6] && echo "extension=mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true after_script: - php test/cleanup.php From b24f30db3e66f455f4daca0b933fa748e1c45da6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 6 Jan 2017 15:05:12 -0800 Subject: [PATCH 25/26] Add mongo db (#790) * add mongodb extension for php 7.x to travis * removes mongodb library for php5 from travis * removes HHVM from travis --- .travis.yml | 6 +- composer.json | 19 +- src/OAuth2/Storage/Mongo.php | 53 ++++ src/OAuth2/Storage/MongoDB.php | 380 ++++++++++++++++++++++++++ test/lib/OAuth2/Storage/BaseTest.php | 2 + test/lib/OAuth2/Storage/Bootstrap.php | 103 ++++++- 6 files changed, 539 insertions(+), 24 deletions(-) create mode 100644 src/OAuth2/Storage/MongoDB.php diff --git a/.travis.yml b/.travis.yml index 6bc9251da..77f50b57b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ php: - 5.6 - 7.0 - 7.1 -- hhvm env: global: - secure: Bc5ZqvZ1YYpoPZNNuU2eCB8DS6vBYrAdfBtTenBs5NSxzb+Vjven4kWakbzaMvZjb/Ib7Uph7DGuOtJXpmxnvBXPLd707LZ89oFWN/yqQlZKCcm8iErvJCB5XL+/ONHj2iPdR242HJweMcat6bMCwbVWoNDidjtWMH0U2mYFy3M= @@ -22,10 +21,11 @@ services: - cassandra before_install: - phpenv config-rm xdebug.ini || return 0 +- phpenv version-name | grep ^5.[3-6] && composer remove mongodb/mongodb --dev && echo "extension=mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true +- phpenv version-name | grep ^7 && echo "extension=mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true install: -- composer install --no-interaction +- composer install before_script: - psql -c 'create database oauth2_server_php;' -U postgres -- phpenv version-name | grep ^5.[3-6] && echo "extension=mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true after_script: - php test/cleanup.php diff --git a/composer.json b/composer.json index b1f28c707..561699f5e 100644 --- a/composer.json +++ b/composer.json @@ -12,22 +12,23 @@ } ], "homepage": "http://github.com/bshaffer/oauth2-server-php", - "require":{ - "php":">=5.3.9" - }, "autoload": { "psr-0": { "OAuth2": "src/" } }, - "suggest": { - "predis/predis": "Required to use the Redis storage engine", - "thobbs/phpcassa": "Required to use the Cassandra storage engine", - "aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine", - "firebase/php-jwt": "~2.2 is required to use JWT features" + "require":{ + "php":">=5.3.9" }, "require-dev": { "aws/aws-sdk-php": "~2.8", "firebase/php-jwt": "~2.2", "predis/predis": "dev-master", - "thobbs/phpcassa": "dev-master" + "thobbs/phpcassa": "dev-master", + "mongodb/mongodb": "^1.1" + }, + "suggest": { + "predis/predis": "Required to use Redis storage", + "thobbs/phpcassa": "Required to use Cassandra storage", + "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", + "firebase/php-jwt": "~1.1 is required to use MondoDB storage" } } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index cef35e5e9..eea06e315 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -22,6 +22,7 @@ class Mongo implements AuthorizationCodeInterface, UserCredentialsInterface, RefreshTokenInterface, JwtBearerInterface, + PublicKeyInterface, OpenIDAuthorizationCodeInterface { protected $db; @@ -46,6 +47,7 @@ public function __construct($connection, $config = array()) 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', + 'key_table' => 'oauth_keys', 'jwt_table' => 'oauth_jwt', ), $config); } @@ -336,4 +338,55 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti) //TODO: Needs mongodb implementation. throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } } diff --git a/src/OAuth2/Storage/MongoDB.php b/src/OAuth2/Storage/MongoDB.php new file mode 100644 index 000000000..64f740fc1 --- /dev/null +++ b/src/OAuth2/Storage/MongoDB.php @@ -0,0 +1,380 @@ + + */ +class MongoDB implements AuthorizationCodeInterface, + UserCredentialsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof Database) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new Client($server); + $this->db = $m->selectDatabase($connection['database']); + } + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'key_table' => 'oauth_keys', + ), $config); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $result = $this->collection('client_table')->updateOne( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + return $result->getMatchedCount() > 0; + } + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $result = $this->collection('client_table')->insertOne($client); + return $result->getInsertedCount() > 0; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + return in_array($grant_type, $grant_types); + } + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $result = $this->collection('access_token_table')->updateOne( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $result = $this->collection('access_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->deleteOne(array( + 'access_token' => $access_token + )); + return $result->getDeletedCount() > 0; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array( + 'authorization_code' => $code + )); + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $result = $this->collection('code_table')->updateOne( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $result = $this->collection('code_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function expireAuthorizationCode($code) + { + $result = $this->collection('code_table')->deleteOne(array( + 'authorization_code' => $code + )); + return $result->getDeletedCount() > 0; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array( + 'refresh_token' => $refresh_token + )); + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $result = $this->collection('refresh_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->deleteOne(array( + 'refresh_token' => $refresh_token + )); + return $result->getDeletedCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $result = $this->collection('user_table')->updateOne( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + + return $result->getMatchedCount() > 0; + } + + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $result = $this->collection('user_table')->insertOne($user); + return $result->getInsertedCount() > 0; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } +} \ No newline at end of file diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index 921d52500..f0b1274a2 100755 --- a/test/lib/OAuth2/Storage/BaseTest.php +++ b/test/lib/OAuth2/Storage/BaseTest.php @@ -11,6 +11,7 @@ public function provideStorage() $mysql = Bootstrap::getInstance()->getMysqlPdo(); $postgres = Bootstrap::getInstance()->getPostgresPdo(); $mongo = Bootstrap::getInstance()->getMongo(); + $mongoDb = Bootstrap::getInstance()->getMongoDB(); $redis = Bootstrap::getInstance()->getRedisStorage(); $cassandra = Bootstrap::getInstance()->getCassandraStorage(); $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); @@ -25,6 +26,7 @@ public function provideStorage() array($mysql), array($postgres), array($mongo), + array($mongoDb), array($redis), array($cassandra), array($dynamodb), diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 4ac9022b1..3d7bdd4e9 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -11,6 +11,7 @@ class Bootstrap private $sqlite; private $postgres; private $mongo; + private $mongoDb; private $redis; private $cassandra; private $configDir; @@ -137,13 +138,12 @@ public function getMysqlPdo() public function getMongo() { if (!$this->mongo) { - $skipMongo = $this->getEnvVar('SKIP_MONGO_TESTS'); - if (!$skipMongo && class_exists('MongoClient')) { + if (class_exists('MongoClient')) { $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false)); if ($this->testMongoConnection($mongo)) { - $db = $mongo->oauth2_server_php; - $this->removeMongoDb($db); - $this->createMongoDb($db); + $db = $mongo->oauth2_server_php_legacy; + $this->removeMongo($db); + $this->createMongo($db); $this->mongo = new Mongo($db); } else { @@ -157,6 +157,28 @@ public function getMongo() return $this->mongo; } + public function getMongoDb() + { + if (!$this->mongoDb) { + if (class_exists('MongoDB\Client')) { + $mongoDb = new \MongoDB\Client('mongodb://localhost:27017'); + if ($this->testMongoDBConnection($mongoDb)) { + $db = $mongoDb->oauth2_server_php; + $this->removeMongoDb($db); + $this->createMongoDb($db); + + $this->mongoDb = new MongoDB($db); + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Unable to connect to mongo server on "localhost:27017"'); + } + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Missing MongoDB php extension. Please install mongodb.so'); + } + } + + return $this->mongoDb; + } + private function testMongoConnection(\MongoClient $mongo) { try { @@ -168,6 +190,11 @@ private function testMongoConnection(\MongoClient $mongo) return true; } + private function testMongoDBConnection(\MongoDB\Client $mongo) + { + return true; + } + public function getCouchbase() { if (!$this->couchbase) { @@ -442,7 +469,7 @@ private function clearCouchbase(\Couchbase $cb) $cb->delete('oauth_refresh_tokens-refreshtoken'); } - private function createMongoDb(\MongoDB $db) + private function createMongo(\MongoDB $db) { $db->oauth_clients->insert(array( 'client_id' => "oauth_test_client", @@ -468,13 +495,70 @@ private function createMongoDb(\MongoDB $db) 'email_verified' => true, )); + $db->oauth_keys->insert(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + $db->oauth_jwt->insert(array( 'client_id' => 'oauth_test_client', - 'key' => $this->getTestPublicKey(), + 'key' => $this->getTestPublicKey(), 'subject' => 'test_subject', )); } + public function removeMongo(\MongoDB $db) + { + $db->drop(); + } + + private function createMongoDB(\MongoDB\Database $db) + { + $db->oauth_clients->insertOne(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + )); + + $db->oauth_access_tokens->insertOne(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + + $db->oauth_authorization_codes->insertOne(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + + $db->oauth_users->insertOne(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + + $db->oauth_keys->insertOne(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + + $db->oauth_jwt->insertOne(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + } + + public function removeMongoDB(\MongoDB\Database $db) + { + $db->drop(); + } + private function createRedisDb(Redis $storage) { $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); @@ -500,11 +584,6 @@ private function createRedisDb(Redis $storage) $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); } - public function removeMongoDb(\MongoDB $db) - { - $db->drop(); - } - public function getTestPublicKey() { return file_get_contents(__DIR__.'/../../../config/keys/id_rsa.pub'); From bef1972a24aa588dcc2cfd16332402f9a9ffb75b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 6 Jan 2017 15:19:04 -0800 Subject: [PATCH 26/26] adds changelog for 1.9.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d925e06..e77d5da7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ 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.9.0 (2016-01-06) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/788 + + * bug #645 - Allow null for client_secret + * bug #651 - Fix bug in isPublicClient of Cassandra Storage + * bug #670 - Bug in client's scope restriction + * bug #672 - Implemented method to override the password hashing algorithm + * bug #698 - Fix Token Response's Content-Type to application/json + * bug #729 - Ensures unsetAccessToken and unsetRefreshToken return a bool + * bug #749 - Fix UserClaims for CodeIdToken + * bug #784 - RFC6750 compatibility + * bug #776 - Fix "redirect_uri_mismatch" for URIs with encoded characters + * bug #759 - no access token supplied to resource controller results in empty request body + * bug #773 - Use OpenSSL random method before attempting Mcrypt's. + * bug #790 - Add mongo db + * 1.8.0 (2015-09-18) PR: https://github.com/bshaffer/oauth2-server-php/pull/643