diff options
| author | Brent Shaffer <bshafs@gmail.com> | 2014-01-04 00:11:34 +0400 |
|---|---|---|
| committer | Brent Shaffer <bshafs@gmail.com> | 2014-01-04 00:11:34 +0400 |
| commit | 6109ecc305ad476f7f296dee40c52b82601e83b2 (patch) | |
| tree | 6f02b7f1247d430b736cece53e1462b32d2d016f | |
| parent | 31bf513f322e60684a699f67b79f7f1ae5de95ff (diff) | |
| parent | e46a0ccaf1a43a0412000a56fc9a41a713205754 (diff) | |
| download | oauth2-server-php-1.2.tar.xz | |
Merge pull request #288 from bshaffer/developv1.2
Create Tag v1.2
31 files changed, 344 insertions, 168 deletions
@@ -1,2 +1,3 @@ # Test Files # -test/config/test.sqlite
\ No newline at end of file +test/config/test.sqlite +vendor diff --git a/.travis.yml b/.travis.yml index 1d1e6d1..8abddff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,13 @@ php: env: - SKIP_MONGO_TESTS=1 -services: mongodb +services: + - mongodb + - redis-server before_script: - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - composer require predis/predis:dev-master after_failure: - ps ax | grep mongo diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3270fe1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,66 @@ +CHANGELOG for 1.x +================= + +This changelog references the relevant changes (bug and security fixes) done +in 1.x minor versions. + +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 +To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1 + +* 1.2 (2014-01-03) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/288 + + * bug #285 changed response header from 200 to 401 when empty token received + * bug #286 adds documentation and links to spec for not including error messages when no token is supplied + * bug #280 ensures PHP warnings do not get thrown as a result of an invalid argument to $jwt->decode() + * bug #279 predis wrong number of arguments + * bug #277 Securing JS WebApp client secret w/ password grant type + +* 1.1 (2013-12-17) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/276 + + * bug #278 adds refresh token configuration to Server class + * bug #274 Supplying a null client_id and client_secret grants API access + * bug #244 [MongoStorage] More detailed implementation info + * bug #268 Implement jti for JWT Bearer tokens to prevent replay attacks. + * bug #266 Removing unused argument to getAccessTokenData + * bug #247 Make Bearer token type consistent + * bug #253 Fixing CryptoToken refresh token lifetime + * bug #246 refactors public key logic to be more intuitive + * bug #245 adds support for JSON crypto tokens + * bug #230 Remove unused columns in oauth_clients + * bug #215 makes Redis Scope Storage obey the same paradigm as PDO + * bug #228 removes scope group + * bug #227 squelches open basedir restriction error + * bug #223 Updated docblocks for RefreshTokenInterface.php + * bug #224 Adds protected properties + * bug #217 Implement ScopeInterface for PDO, Redis + +* 1.0 (2013-08-12) + + * bug #203 Add redirect\_status_code config param for AuthorizeController + * bug #205 ensures unnecessary ? is not set when ** bug + * bug #204 Fixed call to LogicException + * bug #202 Add explode to checkRestrictedGrant in PDO Storage + * bug #197 adds support for 'false' default scope ** bug + * bug #192 reference errors and adds tests + * bug #194 makes some appropriate properties ** bug + * bug #191 passes config to HttpBasic + * bug #190 validates client credentials before ** bug + * bug #171 Fix wrong redirect following authorization step + * bug #187 client_id is now passed to getDefaultScope(). + * bug #176 Require refresh_token in getRefreshToken response + * bug #174 make user\_id not required for refresh_token grant + * bug #173 Duplication in JwtBearer Grant + * bug #168 user\_id not required for authorization_code grant + * bug #133 hardens default security for user object + * bug #163 allows redirect\_uri on authorization_code to be NULL in docs example + * bug #162 adds getToken on ResourceController for convenience + * bug #161 fixes fatal error + * bug #163 Invalid redirect_uri handling + * bug #156 user\_id in OAuth2\_Storage_AuthorizationCodeInterface::getAuthorizationCode() response + * bug #157 Fix for extending access and refresh tokens + * bug #154 ResponseInterface: getParameter method is used in the library but not defined in the interface + * bug #148 Add more detail to examples in Readme.md diff --git a/composer.json b/composer.json index a8e5b46..2503439 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ ], "homepage": "http://github.com/bshaffer/oauth2-server-php", "require":{ - "php":">=5.2.0" + "php":">=5.3.9" }, "autoload": { "psr-0": { "OAuth2": "src/" } diff --git a/src/OAuth2/ClientAssertionType/HttpBasic.php b/src/OAuth2/ClientAssertionType/HttpBasic.php index 2e8c89d..d755e1b 100644 --- a/src/OAuth2/ClientAssertionType/HttpBasic.php +++ b/src/OAuth2/ClientAssertionType/HttpBasic.php @@ -22,7 +22,8 @@ class HttpBasic implements ClientAssertionTypeInterface { $this->storage = $storage; $this->config = array_merge(array( - 'allow_credentials_in_request_body' => true + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, ), $config); } @@ -32,11 +33,23 @@ class HttpBasic implements ClientAssertionTypeInterface return false; } - if (!isset($clientData['client_id']) || !isset($clientData['client_secret'])) { - throw new \LogicException('the clientData array must have "client_id" and "client_secret" values set.'); + if (!isset($clientData['client_id'])) { + throw new \LogicException('the clientData array must have "client_id" set'); } - if ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { + if (!$this->config['allow_public_clients']) { + $response->setError(400, 'invalid_client', 'client credentials are required'); + + return false; + } + + if (!$this->storage->isPublicClient($clientData['client_id'])) { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + + return false; + } + } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { $response->setError(400, 'invalid_client', 'The client credentials are invalid'); return false; @@ -70,7 +83,7 @@ class HttpBasic implements ClientAssertionTypeInterface * @code * return array( * "client_id" => CLIENT_ID, // REQUIRED the client id - * "client_secret" => CLIENT_SECRET, // REQUIRED the client secret + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) * ); * @endcode * @@ -91,13 +104,13 @@ class HttpBasic implements ClientAssertionTypeInterface * client_secret can be null if the client's password is an empty string * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 */ - - return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret', '')); + return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); } } if ($response) { - $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers or body'); + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); } return null; diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index a615354..277b477 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -86,6 +86,7 @@ class TokenController implements TokenControllerInterface return null; } + if (!isset($this->grantTypes[$grantTypeIdentifier])) { /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); diff --git a/src/OAuth2/Encryption/Jwt.php b/src/OAuth2/Encryption/Jwt.php index 641a2e9..6bde68e 100644 --- a/src/OAuth2/Encryption/Jwt.php +++ b/src/OAuth2/Encryption/Jwt.php @@ -27,6 +27,10 @@ class Jwt implements EncryptionInterface public function decode($jwt, $key = null, $verify = true) { + if (!strpos($jwt, '.')) { + return false; + } + $tks = explode('.', $jwt); if (count($tks) != 3) { diff --git a/src/OAuth2/GrantType/ClientCredentials.php b/src/OAuth2/GrantType/ClientCredentials.php index 0a73e53..d18bb8c 100644 --- a/src/OAuth2/GrantType/ClientCredentials.php +++ b/src/OAuth2/GrantType/ClientCredentials.php @@ -4,6 +4,7 @@ namespace OAuth2\GrantType; use OAuth2\ClientAssertionType\HttpBasic; use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\Storage\ClientCredentialsInterface; /** * @author Brent Shaffer <bshafs at gmail dot com> @@ -14,6 +15,18 @@ class ClientCredentials extends HttpBasic implements GrantTypeInterface { private $clientData; + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + public function getQuerystringIdentifier() { return 'client_credentials'; @@ -35,8 +48,9 @@ class ClientCredentials extends HttpBasic implements GrantTypeInterface public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { - /* + /** * Client Credentials Grant does NOT include a refresh token + * * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 */ $includeRefreshToken = false; diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index 2d12c5b..12dfd3d 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -103,6 +103,7 @@ class Server implements ResourceControllerInterface, 'require_exact_redirect_uri' => true, 'allow_implicit' => false, 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, 'always_issue_new_refresh_token' => false, ), $config); @@ -400,7 +401,7 @@ class Server implements ResourceControllerInterface, if (!isset($this->storages['client_credentials'])) { 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('allow_credentials_in_request_body' => '')); + $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); break; } @@ -459,7 +460,8 @@ class Server implements ResourceControllerInterface, } if (isset($this->storages['client_credentials'])) { - $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials']); + $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config); } if (isset($this->storages['refresh_token'])) { diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php index b538ad2..dd687e4 100644 --- a/src/OAuth2/Storage/AccessTokenInterface.php +++ b/src/OAuth2/Storage/AccessTokenInterface.php @@ -23,6 +23,7 @@ interface AccessTokenInterface * is invalid: * - client_id: Stored client identifier. * - expires: Stored expiration in unix timestamp. + * - user_id: (optional) Stored user identifier. * - scope: (optional) Stored scope values in space-separated string. * * @ingroup oauth2_section_7 diff --git a/src/OAuth2/Storage/ClientCredentialsInterface.php b/src/OAuth2/Storage/ClientCredentialsInterface.php index 530157c..3318c69 100644 --- a/src/OAuth2/Storage/ClientCredentialsInterface.php +++ b/src/OAuth2/Storage/ClientCredentialsInterface.php @@ -28,4 +28,22 @@ interface ClientCredentialsInterface extends ClientInterface * @ingroup oauth2_section_3 */ public function checkClientCredentials($client_id, $client_secret = null); + + /** + * Determine if the client is a "public" client, and therefore + * does not require passing credentials for certain grant types + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * TRUE if the client is public, and FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/257 + * + * @ingroup oauth2_section_2 + */ + public function isPublicClient($client_id); } diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 7f5a0c7..ee59703 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -131,6 +131,16 @@ class Memory implements AuthorizationCodeInterface, return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; } + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ public function getClientDetails($client_id) { if (!isset($this->clientCredentials[$client_id])) { diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 3cc70c6..e198dfc 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -67,6 +67,16 @@ class Mongo implements AuthorizationCodeInterface, 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)); @@ -304,6 +314,6 @@ class Mongo implements AuthorizationCodeInterface, public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs mongodb implementation. - throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); } } diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index cd19b4c..1c2b522 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -59,7 +59,7 @@ class Pdo implements AuthorizationCodeInterface, ), $config); } - /* OAuth2_Storage_ClientCredentialsInterface */ + /* OAuth2\Storage\ClientCredentialsInterface */ public function checkClientCredentials($client_id, $client_secret = null) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -70,6 +70,19 @@ class Pdo implements AuthorizationCodeInterface, return $result && $result['client_secret'] == $client_secret; } + public function isPublicClient($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + if (!$result = $stmt->fetch()) { + return false; + } + + return empty($result['client_secret']);; + } + + /* OAuth2\Storage\ClientInterface */ public function getClientDetails($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -103,7 +116,7 @@ class Pdo implements AuthorizationCodeInterface, return true; } - /* OAuth2_Storage_AccessTokenInterface */ + /* OAuth2\Storage\AccessTokenInterface */ public function getAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); @@ -132,7 +145,7 @@ class Pdo implements AuthorizationCodeInterface, return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } - /* OAuth2_Storage_AuthorizationCodeInterface */ + /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); @@ -168,7 +181,7 @@ class Pdo implements AuthorizationCodeInterface, return $stmt->execute(compact('code')); } - /* OAuth2_Storage_UserCredentialsInterface */ + /* OAuth2\Storage\UserCredentialsInterface */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { @@ -183,7 +196,7 @@ class Pdo implements AuthorizationCodeInterface, return $this->getUser($username); } - /* OAuth2_Storage_RefreshTokenInterface */ + /* OAuth2\Storage\RefreshTokenInterface */ public function getRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); @@ -254,8 +267,8 @@ class Pdo implements AuthorizationCodeInterface, public function scopeExists($scope, $client_id = null) { $scope = explode(' ', $scope); - $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type="supported" AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); - $stmt->execute(compact('client_id')); + $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type=:type AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); + $stmt->execute(array('client_id' => $client_id, 'type'=> 'supported')); if ($result = $stmt->fetch()) { $supportedScope = explode(' ', $result['scope']); @@ -268,8 +281,8 @@ class Pdo implements AuthorizationCodeInterface, public function getDefaultScope($client_id = null) { - $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type="default" AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); - $stmt->execute(compact('client_id')); + $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type=:type AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); + $stmt->execute(array('client_id' => $client_id, 'type' => 'default')); if ($result = $stmt->fetch()) { return $result['scope']; diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index 55d300c..0b2badc 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -67,18 +67,21 @@ class Redis implements AuthorizationCodeInterface, $str = json_encode($value); if ($expire > 0) { $seconds = $expire - time(); - - return $this->redis->setex($key, $seconds, $str); + $ret = $this->redis->setex($key, $seconds, $str); } else { - return $this->redis->set($key, $str); + $ret = $this->redis->set($key, $str); } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return $ret->getPayload() == 'OK'; } protected function expireValue($key) { unset($this->cache[$key]); - return $this->redis->expire($key); + return $this->redis->delete($key); } /* AuthorizationCodeInterface */ @@ -148,6 +151,16 @@ class Redis implements AuthorizationCodeInterface, && $client['client_secret'] == $client_secret; } + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']);; + } + + /* ClientInterface */ public function getClientDetails($client_id) { return $this->getValue($this->config['client_key'] . $client_id); @@ -230,6 +243,21 @@ class Redis implements AuthorizationCodeInterface, return $result; } + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + /*JWTBearerInterface */ public function getClientKey($client_id, $subject) { @@ -244,12 +272,12 @@ class Redis implements AuthorizationCodeInterface, public function getJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs redis implementation. - throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); } public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs redis implementation. - throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); } } diff --git a/src/OAuth2/TokenType/Bearer.php b/src/OAuth2/TokenType/Bearer.php index 4355275..f437a0b 100644 --- a/src/OAuth2/TokenType/Bearer.php +++ b/src/OAuth2/TokenType/Bearer.php @@ -51,13 +51,25 @@ class Bearer implements TokenTypeInterface { $headers = $request->headers('AUTHORIZATION'); - // Check that exactly one method was used - $methodsUsed = !empty($headers) + !is_null($request->query($this->config['token_param_name'])) + !is_null($request->request($this->config['token_param_name'])); + /** + * Ensure more than one method is not used for including an + * access token + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + $methodsUsed = !empty($headers) + (bool)($request->query($this->config['token_param_name'])) + (bool)($request->request($this->config['token_param_name'])); if ($methodsUsed > 1) { $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); return null; } + + /** + * If no authentication is provided, set the status code + * to 401 and return no other error information + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ if ($methodsUsed == 0) { $response->setStatusCode(401); diff --git a/test/OAuth2/Controller/TokenControllerTest.php b/test/OAuth2/Controller/TokenControllerTest.php index 6cd7416..183e392 100644 --- a/test/OAuth2/Controller/TokenControllerTest.php +++ b/test/OAuth2/Controller/TokenControllerTest.php @@ -6,6 +6,7 @@ use OAuth2\Storage\Bootstrap; use OAuth2\Server; use OAuth2\GrantType\AuthorizationCode; use OAuth2\GrantType\ClientCredentials; +use OAuth2\GrantType\UserCredentials; use OAuth2\Scope; use OAuth2\Request\TestRequest; use OAuth2\Response; @@ -52,7 +53,7 @@ class Controller_TokenControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($response->getParameter('error_description'), 'Client credentials were not found in the headers or body'); } - public function testNoClientSecret() + public function testNoClientSecretWithConfidentialClient() { // add the test parameters in memory $server = $this->getTestServer(); @@ -65,7 +66,7 @@ class Controller_TokenControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($response->getStatusCode(), 400); $this->assertEquals($response->getParameter('error'), 'invalid_client'); - $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret'); } public function testNoClientSecretWithEmptySecret() @@ -132,9 +133,9 @@ class Controller_TokenControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($response->getStatusCode(), 200); $this->assertNull($response->getParameter('error')); $this->assertNull($response->getParameter('error_description')); - $this->assertNotNUll($response->getParameter('access_token')); - $this->assertNotNUll($response->getParameter('expires_in')); - $this->assertNotNUll($response->getParameter('token_type')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); } public function testValidClientIdScope() @@ -198,6 +199,30 @@ class Controller_TokenControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($response->getParameter('error_description'), 'This application requires you specify a scope parameter'); } + public function testCanReceiveAccessTokenUsingPasswordGrantTypeWithoutClientSecret() + { + // add the test parameters in memory + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new UserCredentials($storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID For Password Grant', // valid client id + 'username' => 'johndoe', // valid username + 'password' => 'password', // valid password for username + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1)); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); + } + public function testCreateController() { $storage = Bootstrap::getInstance()->getMemoryStorage(); diff --git a/test/OAuth2/Encryption/JwtTest.php b/test/OAuth2/Encryption/JwtTest.php index b10c61b..20d3128 100644 --- a/test/OAuth2/Encryption/JwtTest.php +++ b/test/OAuth2/Encryption/JwtTest.php @@ -50,6 +50,14 @@ EOD; $this->assertEquals($params, $payload); } + public function testInvalidJwt() + { + $jwtUtil = new Jwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + public function provideClientCredentials() { $storage = Bootstrap::getInstance()->getMemoryStorage(); diff --git a/test/OAuth2/ServerTest.php b/test/OAuth2/ServerTest.php index b61f851..8794b42 100644 --- a/test/OAuth2/ServerTest.php +++ b/test/OAuth2/ServerTest.php @@ -253,7 +253,10 @@ class ServerTest extends \PHPUnit_Framework_TestCase { // create mock storage $storage = Bootstrap::getInstance()->getMemoryStorage(); - $server = new Server(array($storage), array('allow_credentials_in_request_body' => false)); + $server = new Server(array($storage), array( + 'allow_credentials_in_request_body' => false, + 'allow_public_clients' => false + )); $server->getTokenController(); $httpBasic = $server->getClientAssertionType(); @@ -261,8 +264,10 @@ class ServerTest extends \PHPUnit_Framework_TestCase $prop = $reflection->getProperty('config'); $prop->setAccessible(true); - $config = $prop->getValue($httpBasic); // get the private "storages" property + $config = $prop->getValue($httpBasic); // get the private "config" property + $this->assertEquals($config['allow_credentials_in_request_body'], false); + $this->assertEquals($config['allow_public_clients'], false); } public function testRefreshTokenConfig() diff --git a/test/OAuth2/Storage/AccessTokenTest.php b/test/OAuth2/Storage/AccessTokenTest.php index 17c9cd9..ba54b34 100644 --- a/test/OAuth2/Storage/AccessTokenTest.php +++ b/test/OAuth2/Storage/AccessTokenTest.php @@ -8,7 +8,7 @@ class AccessTokenTest extends BaseTest public function testSetAccessToken(AccessTokenInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/OAuth2/Storage/AuthorizationCodeTest.php b/test/OAuth2/Storage/AuthorizationCodeTest.php index 9f57714..7dacbf4 100644 --- a/test/OAuth2/Storage/AuthorizationCodeTest.php +++ b/test/OAuth2/Storage/AuthorizationCodeTest.php @@ -8,7 +8,7 @@ class AuthorizationCodeTest extends BaseTest public function testGetAuthorizationCode(AuthorizationCodeInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } @@ -25,7 +25,7 @@ class AuthorizationCodeTest extends BaseTest public function testSetAuthorizationCode(AuthorizationCodeInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/OAuth2/Storage/ClientCredentialsTest.php b/test/OAuth2/Storage/ClientCredentialsTest.php index ecaf6f6..21d6c14 100644 --- a/test/OAuth2/Storage/ClientCredentialsTest.php +++ b/test/OAuth2/Storage/ClientCredentialsTest.php @@ -8,7 +8,7 @@ class ClientCredentialsTest extends BaseTest public function testCheckClientCredentials(ClientCredentialsInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/OAuth2/Storage/ClientTest.php b/test/OAuth2/Storage/ClientTest.php index 4d95c6c..e370e38 100644 --- a/test/OAuth2/Storage/ClientTest.php +++ b/test/OAuth2/Storage/ClientTest.php @@ -8,7 +8,7 @@ class ClientTest extends BaseTest public function testGetClientDetails(ClientInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } @@ -28,7 +28,7 @@ class ClientTest extends BaseTest public function testCheckRestrictedGrantType(ClientInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } @@ -46,7 +46,7 @@ class ClientTest extends BaseTest public function testGetAccessToken(ClientInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } @@ -63,7 +63,7 @@ class ClientTest extends BaseTest public function testSaveClient(ClientInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/OAuth2/Storage/PublicKeyTest.php b/test/OAuth2/Storage/PublicKeyTest.php index d08c9c0..0e2bfd6 100644 --- a/test/OAuth2/Storage/PublicKeyTest.php +++ b/test/OAuth2/Storage/PublicKeyTest.php @@ -7,8 +7,13 @@ class PublicKeyTest extends BaseTest /** @dataProvider provideStorage */ public function testSetAccessToken($storage) { - if (is_null($storage) || !$storage instanceof PublicKeyInterface) { - return $this->markTestSkipped('Invalid storage for public key test'); + if (is_null($storage)) { + return $this->markTestSkipped('Skipped Storage'); + } + + if (!$storage instanceof PublicKeyInterface) { + // incompatible storage + return; } $configDir = Bootstrap::getInstance()->getConfigDir(); diff --git a/test/OAuth2/Storage/RefreshTokenTest.php b/test/OAuth2/Storage/RefreshTokenTest.php index 7777bb9..a356cc5 100644 --- a/test/OAuth2/Storage/RefreshTokenTest.php +++ b/test/OAuth2/Storage/RefreshTokenTest.php @@ -8,7 +8,7 @@ class RefreshTokenTest extends BaseTest public function testSetRefreshToken(RefreshTokenInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/OAuth2/Storage/ScopeTest.php b/test/OAuth2/Storage/ScopeTest.php index a34ce54..83d9b6f 100644 --- a/test/OAuth2/Storage/ScopeTest.php +++ b/test/OAuth2/Storage/ScopeTest.php @@ -10,14 +10,11 @@ class ScopeTest extends BaseTest public function testScopeExists($storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); - - return; + return $this->markTestSkipped('Skipped Storage'); } if (!$storage instanceof ScopeInterface) { - $this->markTestSkipped('Skipping incompatible storage'); - + // incompatible storage return; } @@ -32,14 +29,11 @@ class ScopeTest extends BaseTest public function testGetDefaultScope($storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); - - return; + return $this->markTestSkipped('Skipped Storage'); } if (!$storage instanceof ScopeInterface) { - $this->markTestSkipped('Skipping incompatible storage'); - + // incompatible storage return; } @@ -52,14 +46,11 @@ class ScopeTest extends BaseTest public function testClientScopeExists($storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); - - return; + return $this->markTestSkipped('Skipped Storage'); } if (!$storage instanceof ScopeInterface) { - $this->markTestSkipped('Skipping incompatible storage'); - + // incompatible storage return; } @@ -75,14 +66,11 @@ class ScopeTest extends BaseTest public function testGetDefaultClientScope($storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); - - return; + return $this->markTestSkipped('Skipped Storage'); } if (!$storage instanceof ScopeInterface) { - $this->markTestSkipped('Skipping incompatible storage'); - + // incompatible storage return; } diff --git a/test/OAuth2/Storage/UserCredentialsTest.php b/test/OAuth2/Storage/UserCredentialsTest.php index ee1c721..78fa804 100644 --- a/test/OAuth2/Storage/UserCredentialsTest.php +++ b/test/OAuth2/Storage/UserCredentialsTest.php @@ -8,7 +8,7 @@ class UserCredentialsTest extends BaseTest public function testCheckUserCredentials(UserCredentialsInterface $storage = null) { if (is_null($storage)) { - $this->markTestSkipped('Unable to load class Mongo_Client'); + $this->markTestSkipped('Skipped Storage'); return; } diff --git a/test/bootstrap.php b/test/bootstrap.php index a967dea..0a4af07 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -5,3 +5,8 @@ OAuth2\Autoloader::register(); // register test classes OAuth2\Autoloader::register(dirname(__FILE__).'/lib'); + +// register vendors if possible +if (file_exists(__DIR__.'/../vendor/autoload.php')) { + require_once(__DIR__.'/../vendor/autoload.php'); +} diff --git a/test/config/storage.json b/test/config/storage.json index f2db151..23a3988 100644 --- a/test/config/storage.json +++ b/test/config/storage.json @@ -48,6 +48,10 @@ "Test Client ID Empty Secret": { "client_secret": "" }, + "Test Client ID For Password Grant": { + "grant_types": "password", + "client_secret": "" + }, "Client ID With User ID": { "client_secret": "TestSecret", "user_id": "brent@brentertainment.com" @@ -63,6 +67,9 @@ }, "testusername": { "password": "testpass" + }, + "johndoe": { + "password": "password" } }, "refresh_tokens" : { diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 673a01a..d1e7818 100644 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -8,6 +8,7 @@ class Bootstrap private $mysql; private $sqlite; private $mongo; + private $redis; private $configDir; public function __construct() @@ -45,7 +46,16 @@ class Bootstrap public function getRedisStorage() { - return new Redis(new MockRedisClient()); + if (!$this->redis) { + if (class_exists('Predis\Client')) { + $redis = new \Predis\Client(); + $redis->flushdb(); + $this->redis = new Redis($redis); + $this->createRedisDb($this->redis); + } + } + + return $this->redis; } public function getMysqlPdo() @@ -159,6 +169,29 @@ class Bootstrap $db->oauth_users->insert(array('username' => "testuser", 'password' => "password")); } + private function createRedisDb(Redis $storage) + { + $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); + $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); + $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); + $storage->setUser("testuser", "password"); + + $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4'); + $storage->setScope('defaultscope1 defaultscope2', null, 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2'); + $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default'); + } + public function removeMongoDb(\MongoDB $db) { $db->drop(); diff --git a/test/lib/OAuth2/Storage/MockRedisClient.php b/test/lib/OAuth2/Storage/MockRedisClient.php deleted file mode 100644 index dbfe264..0000000 --- a/test/lib/OAuth2/Storage/MockRedisClient.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php - -namespace OAuth2\Storage; - -class MockRedisClient -{ - public function __construct() - { - $data = array( - 'oauth_clients:oauth_test_client' => array( - 'client_id' => 'oauth_test_client', - 'client_secret' => "testpass", - 'redirect_uri' => '', - 'grant_types' => 'implicit password', - ), - 'oauth_clients:Test Client ID' => array( - 'client_id' => 'Test Client ID', - 'client_secret' => "TestSecret" - ), - 'oauth_clients:Test Client ID 2' => array( - 'client_id' => 'Test Client ID 2', - 'client_secret' => "TestSecret", - ), - 'oauth_clients:Test Default Scope Client ID' => array( - 'client_id' => 'Test Default Scope Client ID', - 'client_secret' => "TestSecret", - ), - 'oauth_clients:Test Default Scope Client ID 2' => array( - 'client_id' => 'Test Default Scope Client ID 2', - 'client_secret' => "TestSecret", - ), - 'oauth_access_tokens:testtoken' => array( - 'access_token' => 'testtoken', - 'client_id' => "Some Client", - 'user_id' => '', - 'expires' => 0, - 'scope' => '' - ), - 'oauth_authorization_codes:testcode' => array( - 'client_id' => "Some Client", - 'authorization_code' => 'testcode', - 'user_id' => '', - 'redirect_uri' => '', - 'expires' => 0, - 'scope' => '' - ), - 'oauth_users:testuser' => array( - 'user_id' => 'testuser', - 'password' => "password", - 'first_name' => '', - 'last_name' => '' - ), - 'oauth_scopes:supported:global' => 'supportedscope1 supportedscope2 supportedscope3 supportedscope4', - 'oauth_scopes:default:global' => 'defaultscope1 defaultscope2', - 'oauth_scopes:supported:Test Client ID' => 'clientscope1 clientscope2', - 'oauth_scopes:default:Test Client ID' => 'clientscope1 clientscope2', - 'oauth_scopes:supported:Test Client ID 2' => 'clientscope3', - 'oauth_scopes:default:Test Client ID 2' => 'clientscope3', - 'oauth_scopes:supported:Test Default Scope Client ID' => 'clientscope1 clientscope2', - 'oauth_scopes:default:Test Default Scope Client ID' => 'clientscope1 clientscope2', - 'oauth_scopes:supported:Test Default Scope Client ID 2' => 'clientscope3', - 'oauth_scopes:default:Test Default Scope Client ID 2' => 'clientscope3', - ); - foreach ($data as $name => $val) { - $data[$name] = json_encode($val); - } - $this->data = $data; - } - - public function get($key) - { - return isset($this->data[$key]) ? $this->data[$key] : null; - } - - public function set ($key, $value) - { - $this->data[$key] = $value; - - return true; - } - - public function setex($key, $expires, $value) - { - - $this->data[$key] = $value; - - return true; - } - - public function expire($key) - { - unset($this->data[$key]); - - return true; - } -} |
