diff --git a/.gitignore b/.gitignore index 897c80b1..e22101f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ phpunit.xml examples/public.key examples/private.key build +*.orig diff --git a/.styleci.yml b/.styleci.yml index d3498157..41f0dd0a 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -4,6 +4,7 @@ enabled: - binary_operator_spaces - blank_line_before_return - concat_with_spaces + - fully_qualified_strict_types - function_typehint_space - hash_to_slash_comment - include @@ -40,7 +41,6 @@ enabled: - print_to_echo - short_array_syntax - short_scalar_cast - - simplified_null_return - single_quote - spaces_cast - standardize_not_equal diff --git a/.travis.yml b/.travis.yml index 22b7fc4f..b773c2aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,9 @@ env: - DEPENDENCIES="--prefer-lowest --prefer-stable" php: - - 7.0 - 7.1 - 7.2 + - 7.3 install: - composer update --no-interaction --prefer-dist $DEPENDENCIES diff --git a/CHANGELOG.md b/CHANGELOG.md index f74b4849..8afc6606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- Flag, `requireCodeChallengeForPublicClients`, used to reject public clients that do not provide a code challenge for the Auth Code Grant (PR #938) +- Flag, `requireCodeChallengeForPublicClients`, used to reject public clients that do not provide a code challenge for the Auth Code Grant; use AuthCodeGrant::disableRequireCodeCallengeForPublicClients() to turn off this requirement (PR #938) - Public clients can now use the Auth Code Grant (PR #938) -- `isConfidential` property added to `ClientEntity` to identify type of client (PR #938) +- `isConfidential` getter added to `ClientEntity` to identify type of client (PR #938) - Function `validateClient()` added to validate clients which was previously performed by the `getClientEntity()` function (PR #938) ### Changed -- Replace `convertToJWT()` interface with a more generic `__toString()` to improve extensibility (PR #874) +- Replace `convertToJWT()` interface with a more generic `__toString()` to improve extensibility; AccessTokenEntityInterface now requires `setPrivateKey(CryptKey $privateKey)` so `__toString()` has everything it needs to work (PR #874) - The `invalidClient()` function accepts a PSR-7 compliant `$serverRequest` argument to avoid accessing the `$_SERVER` global variable and improve testing (PR #899) - `issueAccessToken()` in the Abstract Grant no longer sets access token client, user ID or scopes. These values should already have been set when calling `getNewToken()` (PR #919) - No longer need to enable PKCE with `enableCodeExchangeProof` flag. Any client sending a code challenge will initiate PKCE checks. (PR #938) @@ -23,6 +23,42 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Removed - `enableCodeExchangeProof` flag (PR #938) +- Support for PHP 7.0 (PR #1014) + +## [7.4.0] - released 2019-05-05 + +### Changed +- RefreshTokenRepository can now return null, allowing refresh tokens to be optional. (PR #649) + +## [7.3.3] - released 2019-03-29 + +### Added +- Added `error_description` to the error payload to improve standards compliance. The contents of this are copied from the existing `message` value. (PR #1006) + +### Deprecated +- Error payload will not issue `message` value in the next major release (PR #1006) + +## [7.3.2] - released 2018-11-21 + +### Fixed +- Revert setting keys on response type to be inside `getResponseType()` function instead of AuthorizationServer constructor (PR #969) + +## [7.3.1] - released 2018-11-15 + +### Fixed +- Fix issue with previous release where interface had changed for the AuthorizationServer. Reverted to the previous interface while maintaining functionality changes (PR #970) + +## [7.3.0] - released 2018-11-13 + +### Changed +- Moved the `finalizeScopes()` call from `validateAuthorizationRequest` method to the `completeAuthorizationRequest` method so it is called just before the access token is issued (PR #923) + +### Added +- Added a ScopeTrait to provide an implementation for jsonSerialize (PR #952) +- Ability to nest exceptions (PR #965) + +### Fixed +- Fix issue where AuthorizationServer is not stateless as ResponseType could store state of a previous request (PR #960) ## [7.2.0] - released 2018-06-23 @@ -424,7 +460,12 @@ Version 5 is a complete code rewrite. - First major release -[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/7.0.0...HEAD +[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/7.4.0...HEAD +[7.4.0]: https://github.com/thephpleague/oauth2-server/compare/7.3.3...7.4.0 +[7.3.3]: https://github.com/thephpleague/oauth2-server/compare/7.3.2...7.3.3 +[7.3.2]: https://github.com/thephpleague/oauth2-server/compare/7.3.1...7.3.2 +[7.3.1]: https://github.com/thephpleague/oauth2-server/compare/7.3.0...7.3.1 +[7.3.0]: https://github.com/thephpleague/oauth2-server/compare/7.2.0...7.3.0 [7.1.1]: https://github.com/thephpleague/oauth2-server/compare/7.1.0...7.1.1 [7.1.0]: https://github.com/thephpleague/oauth2-server/compare/7.0.0...7.1.0 [7.0.0]: https://github.com/thephpleague/oauth2-server/compare/6.1.1...7.0.0 diff --git a/README.md b/README.md index 4d5fd215..48656296 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](ht The following versions of PHP are supported: -* PHP 7.0 * PHP 7.1 * PHP 7.2 +* PHP 7.3 -The `openssl` extension is also required. +The `openssl` and `json` extensions are also required. All HTTP messages passed to the server should be [PSR-7 compliant](https://www.php-fig.org/psr/psr-7/). This ensures interoperability with other packages and frameworks. @@ -68,6 +68,8 @@ We use [Travis CI](https://travis-ci.org/), [Scrutinizer](https://scrutinizer-ci * [Drupal](https://www.drupal.org/project/simple_oauth) * [Laravel Passport](https://github.com/laravel/passport) * [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server) +* [OAuth 2 Server for Expressive](https://github.com/zendframework/zend-expressive-authentication-oauth2) +* [Trikoder OAuth 2 Bundle (Symfony)](https://github.com/trikoder/oauth2-bundle) ## Changelog @@ -83,13 +85,9 @@ Bugs and feature request are tracked on [GitHub](https://github.com/thephpleague If you have any questions about OAuth _please_ open a ticket here; please **don't** email the address below. -## Commercial Support - -If you would like help implementing this library into your existing platform, or would be interested in OAuth advice or training for you and your team please get in touch with [Glynde Labs](https://glyndelabs.com). - ## Security -If you discover any security related issues, please email `hello@alexbilbie.com` instead of using the issue tracker. +If you discover any security related issues, please email `andrew@noexceptions.io` instead of using the issue tracker. ## License diff --git a/composer.json b/composer.json index 48a95701..8f7f2dca 100644 --- a/composer.json +++ b/composer.json @@ -4,19 +4,21 @@ "homepage": "https://oauth2.thephpleague.com/", "license": "MIT", "require": { - "php": ">=7.0.0", + "php": ">=7.1.0", "ext-openssl": "*", "league/event": "^2.1", "lcobucci/jwt": "^3.2.2", "psr/http-message": "^1.0.1", - "defuse/php-encryption": "^2.1" + "defuse/php-encryption": "^2.1", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^6.3 || ^7.0", "zendframework/zend-diactoros": "^1.3.2", "phpstan/phpstan": "^0.9.2", "phpstan/phpstan-phpunit": "^0.9.4", - "phpstan/phpstan-strict-rules": "^0.9.0" + "phpstan/phpstan-strict-rules": "^0.9.0", + "roave/security-advisories": "dev-master" }, "repositories": [ { @@ -46,6 +48,12 @@ "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" } ], "replace": { diff --git a/examples/composer.json b/examples/composer.json index ec7387cf..a3ef9b7a 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -1,14 +1,13 @@ { "require": { - "slim/slim": "3.0.*" + "slim/slim": "^3.0.0" }, "require-dev": { "league/event": "^2.1", - "lcobucci/jwt": "^3.1", - "paragonie/random_compat": "^2.0", + "lcobucci/jwt": "^3.2", "psr/http-message": "^1.0", - "defuse/php-encryption": "^2.1", - "zendframework/zend-diactoros": "^1.0" + "defuse/php-encryption": "^2.2", + "zendframework/zend-diactoros": "^2.0.0" }, "autoload": { "psr-4": { diff --git a/examples/composer.lock b/examples/composer.lock index 8603438a..1611f94b 100644 --- a/examples/composer.lock +++ b/examples/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6701e0eaa09f74e1ebb19c3d61f39068", + "content-hash": "97f2878428e37d1d8e5418cc85cbfa3d", "packages": [ { "name": "container-interop/container-interop", @@ -39,21 +39,24 @@ }, { "name": "nikic/fast-route", - "version": "v0.6.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/nikic/FastRoute.git", - "reference": "31fa86924556b80735f98b294a7ffdfb26789f22" + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/FastRoute/zipball/31fa86924556b80735f98b294a7ffdfb26789f22", - "reference": "31fa86924556b80735f98b294a7ffdfb26789f22", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", "shasum": "" }, "require": { "php": ">=5.4.0" }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, "type": "library", "autoload": { "psr-4": { @@ -78,7 +81,7 @@ "router", "routing" ], - "time": "2015-06-18T19:15:47+00:00" + "time": "2018-02-13T20:26:39+00:00" }, { "name": "pimple/pimple", @@ -231,27 +234,32 @@ }, { "name": "slim/slim", - "version": "3.0.0", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/slimphp/Slim.git", - "reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e" + "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/3b06f0f2d84dabbe81b6cea46ace46a3e883253e", - "reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a", + "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a", "shasum": "" }, "require": { - "container-interop/container-interop": "^1.1", - "nikic/fast-route": "^0.6", + "container-interop/container-interop": "^1.2", + "nikic/fast-route": "^1.0", "php": ">=5.5.0", "pimple/pimple": "^3.0", + "psr/container": "^1.0", "psr/http-message": "^1.0" }, + "provide": { + "psr/http-message-implementation": "1.0" + }, "require-dev": { - "phpunit/phpunit": "^4.0" + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" }, "type": "library", "autoload": { @@ -286,14 +294,14 @@ } ], "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", - "homepage": "http://slimframework.com", + "homepage": "https://slimframework.com", "keywords": [ "api", "framework", "micro", "router" ], - "time": "2015-12-07T14:11:09+00:00" + "time": "2018-09-16T10:54:21+00:00" } ], "packages-dev": [ @@ -470,33 +478,29 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.17", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -515,41 +519,97 @@ "pseudorandom", "random" ], - "time": "2018-07-04T16:31:37+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { - "name": "zendframework/zend-diactoros", - "version": "1.8.4", + "name": "psr/http-factory", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba", - "reference": "736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/378bfe27931ecc54ff824a20d6f6bfc303bbd04c", + "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2018-07-30T21:54:04+00:00" + }, + { + "name": "zendframework/zend-diactoros", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "0bae78192e634774b5584f0210c1232da82cb1ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/0bae78192e634774b5584f0210c1232da82cb1ff", + "reference": "0bae78192e634774b5584f0210c1232da82cb1ff", + "shasum": "" + }, + "require": { + "php": "^7.1", + "psr/http-factory": "^1.0", "psr/http-message": "^1.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { "ext-dom": "*", "ext-libxml": "*", - "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7", - "zendframework/zend-coding-standard": "~1.0" + "http-interop/http-factory-tests": "^0.5.0", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev", - "dev-develop": "1.9.x-dev", - "dev-release-2.0": "2.0.x-dev" + "dev-master": "2.0.x-dev", + "dev-develop": "2.1.x-dev", + "dev-release-1.8": "1.8.x-dev" } }, "autoload": { @@ -569,16 +629,15 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "description": "PSR HTTP Message implementations", - "homepage": "https://github.com/zendframework/zend-diactoros", "keywords": [ "http", "psr", "psr-7" ], - "time": "2018-08-01T13:47:49+00:00" + "time": "2018-09-27T19:49:04+00:00" } ], "aliases": [], diff --git a/examples/src/Entities/ScopeEntity.php b/examples/src/Entities/ScopeEntity.php index ec83cf51..913c0592 100644 --- a/examples/src/Entities/ScopeEntity.php +++ b/examples/src/Entities/ScopeEntity.php @@ -11,13 +11,9 @@ namespace OAuth2ServerExamples\Entities; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\ScopeTrait; class ScopeEntity implements ScopeEntityInterface { - use EntityTrait; - - public function jsonSerialize() - { - return $this->getIdentifier(); - } + use EntityTrait, ScopeTrait; } diff --git a/examples/src/Repositories/RefreshTokenRepository.php b/examples/src/Repositories/RefreshTokenRepository.php index 39a0b8cd..007529cd 100644 --- a/examples/src/Repositories/RefreshTokenRepository.php +++ b/examples/src/Repositories/RefreshTokenRepository.php @@ -18,7 +18,7 @@ class RefreshTokenRepository implements RefreshTokenRepositoryInterface /** * {@inheritdoc} */ - public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntityInterface) + public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) { // Some logic to persist the refresh token in a database } diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 81973b24..8b0b2815 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -50,7 +50,7 @@ class AuthorizationServer implements EmitterAwareInterface protected $publicKey; /** - * @var null|ResponseTypeInterface + * @var ResponseTypeInterface */ protected $responseType; @@ -104,8 +104,16 @@ class AuthorizationServer implements EmitterAwareInterface if ($privateKey instanceof CryptKey === false) { $privateKey = new CryptKey($privateKey); } + $this->privateKey = $privateKey; $this->encryptionKey = $encryptionKey; + + if ($responseType === null) { + $responseType = new BearerTokenResponse(); + } else { + $responseType = clone $responseType; + } + $this->responseType = $responseType; } @@ -205,16 +213,15 @@ class AuthorizationServer implements EmitterAwareInterface */ protected function getResponseType() { - if ($this->responseType instanceof ResponseTypeInterface === false) { - $this->responseType = new BearerTokenResponse(); + $responseType = clone $this->responseType; + + if ($responseType instanceof AbstractResponseType) { + $responseType->setPrivateKey($this->privateKey); } - if ($this->responseType instanceof AbstractResponseType === true) { - $this->responseType->setPrivateKey($this->privateKey); - } - $this->responseType->setEncryptionKey($this->encryptionKey); + $responseType->setEncryptionKey($this->encryptionKey); - return $this->responseType; + return $responseType; } /** diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php index 2efa3c8e..b2035ccc 100644 --- a/src/AuthorizationValidators/BearerTokenValidator.php +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -9,6 +9,8 @@ namespace League\OAuth2\Server\AuthorizationValidators; +use BadMethodCallException; +use InvalidArgumentException; use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\ValidationData; @@ -17,6 +19,7 @@ use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; class BearerTokenValidator implements AuthorizationValidatorInterface { @@ -28,7 +31,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface private $accessTokenRepository; /** - * @var \League\OAuth2\Server\CryptKey + * @var CryptKey */ protected $publicKey; @@ -43,7 +46,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface /** * Set the public key * - * @param \League\OAuth2\Server\CryptKey $key + * @param CryptKey $key */ public function setPublicKey(CryptKey $key) { @@ -69,8 +72,8 @@ class BearerTokenValidator implements AuthorizationValidatorInterface if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) { throw OAuthServerException::accessDenied('Access token could not be verified'); } - } catch (\BadMethodCallException $exception) { - throw OAuthServerException::accessDenied('Access token is not signed'); + } catch (BadMethodCallException $exception) { + throw OAuthServerException::accessDenied('Access token is not signed', null, $exception); } // Ensure access token hasn't expired @@ -92,12 +95,12 @@ class BearerTokenValidator implements AuthorizationValidatorInterface ->withAttribute('oauth_client_id', $token->getClaim('aud')) ->withAttribute('oauth_user_id', $token->getClaim('sub')) ->withAttribute('oauth_scopes', $token->getClaim('scopes')); - } catch (\InvalidArgumentException $exception) { + } catch (InvalidArgumentException $exception) { // JWT couldn't be parsed so return the request as is - throw OAuthServerException::accessDenied($exception->getMessage()); - } catch (\RuntimeException $exception) { + throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception); + } catch (RuntimeException $exception) { //JWR couldn't be parsed so return the request as is - throw OAuthServerException::accessDenied('Error while decoding to JSON'); + throw OAuthServerException::accessDenied('Error while decoding to JSON', null, $exception); } } } diff --git a/src/CryptKey.php b/src/CryptKey.php index 98b53222..6fc4dff0 100644 --- a/src/CryptKey.php +++ b/src/CryptKey.php @@ -11,6 +11,9 @@ namespace League\OAuth2\Server; +use LogicException; +use RuntimeException; + class CryptKey { const RSA_KEY_PATTERN = @@ -42,7 +45,7 @@ class CryptKey } if (!file_exists($keyPath) || !is_readable($keyPath)) { - throw new \LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath)); + throw new LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath)); } if ($keyPermissionsCheck === true) { @@ -64,7 +67,7 @@ class CryptKey /** * @param string $key * - * @throws \RuntimeException + * @throws RuntimeException * * @return string */ @@ -79,19 +82,19 @@ class CryptKey if (!touch($keyPath)) { // @codeCoverageIgnoreStart - throw new \RuntimeException(sprintf('"%s" key file could not be created', $keyPath)); + throw new RuntimeException(sprintf('"%s" key file could not be created', $keyPath)); // @codeCoverageIgnoreEnd } if (file_put_contents($keyPath, $key) === false) { // @codeCoverageIgnoreStart - throw new \RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir)); + throw new RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir)); // @codeCoverageIgnoreEnd } if (chmod($keyPath, 0600) === false) { // @codeCoverageIgnoreStart - throw new \RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath)); + throw new RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath)); // @codeCoverageIgnoreEnd } diff --git a/src/CryptTrait.php b/src/CryptTrait.php index 672c7e2e..1196e9dc 100644 --- a/src/CryptTrait.php +++ b/src/CryptTrait.php @@ -13,6 +13,8 @@ namespace League\OAuth2\Server; use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; +use Exception; +use LogicException; trait CryptTrait { @@ -26,7 +28,7 @@ trait CryptTrait * * @param string $unencryptedData * - * @throws \LogicException + * @throws LogicException * * @return string */ @@ -38,8 +40,8 @@ trait CryptTrait } return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); - } catch (\Exception $e) { - throw new \LogicException($e->getMessage()); + } catch (Exception $e) { + throw new LogicException($e->getMessage(), null, $e); } } @@ -48,7 +50,7 @@ trait CryptTrait * * @param string $encryptedData * - * @throws \LogicException + * @throws LogicException * * @return string */ @@ -60,8 +62,8 @@ trait CryptTrait } return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); - } catch (\Exception $e) { - throw new \LogicException($e->getMessage()); + } catch (Exception $e) { + throw new LogicException($e->getMessage(), null, $e); } } diff --git a/src/Entities/ScopeEntityInterface.php b/src/Entities/ScopeEntityInterface.php index 34ef75f0..26748e0c 100644 --- a/src/Entities/ScopeEntityInterface.php +++ b/src/Entities/ScopeEntityInterface.php @@ -9,7 +9,9 @@ namespace League\OAuth2\Server\Entities; -interface ScopeEntityInterface extends \JsonSerializable +use JsonSerializable; + +interface ScopeEntityInterface extends JsonSerializable { /** * Get the scope's identifier. diff --git a/src/Entities/Traits/ScopeTrait.php b/src/Entities/Traits/ScopeTrait.php new file mode 100644 index 00000000..a132234f --- /dev/null +++ b/src/Entities/Traits/ScopeTrait.php @@ -0,0 +1,28 @@ + + * @copyright Copyright (c) Andrew Millington + * @license http://mit-license.org + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +trait ScopeTrait +{ + /** + * Serialize the object to the scopes string identifier when using json_encode(). + * + * @return string + */ + public function jsonSerialize() + { + return $this->getIdentifier(); + } + + /** + * @return string + */ + abstract public function getIdentifier(); +} diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index 264f0e87..3c3c7129 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -9,10 +9,12 @@ namespace League\OAuth2\Server\Exception; +use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Throwable; -class OAuthServerException extends \Exception +class OAuthServerException extends Exception { /** * @var int @@ -53,17 +55,18 @@ class OAuthServerException extends \Exception * @param int $httpStatusCode HTTP status code to send (default = 400) * @param null|string $hint A helper hint * @param null|string $redirectUri A HTTP URI to redirect the user back to + * @param Throwable $previous Previous exception */ - public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null) + public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null, Throwable $previous = null) { - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); $this->httpStatusCode = $httpStatusCode; $this->errorType = $errorType; $this->hint = $hint; $this->redirectUri = $redirectUri; $this->payload = [ - 'error' => $errorType, - 'message' => $message, + 'error' => $errorType, + 'error_description' => $message, ]; if ($hint !== null) { $this->payload['hint'] = $hint; @@ -77,7 +80,15 @@ class OAuthServerException extends \Exception */ public function getPayload() { - return $this->payload; + $payload = $this->payload; + + // The "message" property is deprecated and replaced by "error_description" + // TODO: remove "message" property + if (isset($payload['error_description']) && !isset($payload['message'])) { + $payload['message'] = $payload['error_description']; + } + + return $payload; } /** @@ -95,7 +106,7 @@ class OAuthServerException extends \Exception * * @param ServerRequestInterface $serverRequest */ - public function setServerRequest($serverRequest) + public function setServerRequest(ServerRequestInterface $serverRequest) { $this->serverRequest = $serverRequest; } @@ -118,16 +129,17 @@ class OAuthServerException extends \Exception * * @param string $parameter The invalid parameter * @param null|string $hint + * @param Throwable $previous Previous exception * * @return static */ - public static function invalidRequest($parameter, $hint = null) + public static function invalidRequest($parameter, $hint = null, Throwable $previous = null) { $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' . 'includes a parameter more than once, or is otherwise malformed.'; $hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint; - return new static($errorMessage, 3, 'invalid_request', 400, $hint); + return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous); } /** @@ -137,7 +149,7 @@ class OAuthServerException extends \Exception * * @return static */ - public static function invalidClient($serverRequest) + public static function invalidClient(ServerRequestInterface $serverRequest) { $exception = new static('Client authentication failed', 4, 'invalid_client', 401); @@ -183,20 +195,24 @@ class OAuthServerException extends \Exception /** * Server error. * - * @param string $hint + * @param string $hint + * @param Throwable $previous * * @return static * * @codeCoverageIgnore */ - public static function serverError($hint) + public static function serverError($hint, Throwable $previous = null) { return new static( 'The authorization server encountered an unexpected condition which prevented it from fulfilling' . ' the request: ' . $hint, 7, 'server_error', - 500 + 500, + null, + null, + $previous ); } @@ -204,12 +220,13 @@ class OAuthServerException extends \Exception * Invalid refresh token. * * @param null|string $hint + * @param Throwable $previous * * @return static */ - public static function invalidRefreshToken($hint = null) + public static function invalidRefreshToken($hint = null, Throwable $previous = null) { - return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint); + return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint, null, $previous); } /** @@ -217,10 +234,11 @@ class OAuthServerException extends \Exception * * @param null|string $hint * @param null|string $redirectUri + * @param Throwable $previous * * @return static */ - public static function accessDenied($hint = null, $redirectUri = null) + public static function accessDenied($hint = null, $redirectUri = null, Throwable $previous = null) { return new static( 'The resource owner or authorization server denied the request.', @@ -228,7 +246,8 @@ class OAuthServerException extends \Exception 'access_denied', 401, $hint, - $redirectUri + $redirectUri, + $previous ); } diff --git a/src/Exception/UniqueTokenIdentifierConstraintViolationException.php b/src/Exception/UniqueTokenIdentifierConstraintViolationException.php index a67855b2..175ab3a8 100644 --- a/src/Exception/UniqueTokenIdentifierConstraintViolationException.php +++ b/src/Exception/UniqueTokenIdentifierConstraintViolationException.php @@ -11,6 +11,9 @@ namespace League\OAuth2\Server\Exception; class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException { + /** + * @return UniqueTokenIdentifierConstraintViolationException + */ public static function create() { $errorMessage = 'Could not create unique access token identifier'; diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 4836e8dd..aed0e6c7 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -12,6 +12,8 @@ namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; +use Error; +use Exception; use League\Event\EmitterAwareTrait; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; @@ -30,7 +32,9 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use LogicException; use Psr\Http\Message\ServerRequestInterface; +use TypeError; /** * Abstract grant class. @@ -79,7 +83,7 @@ abstract class AbstractGrant implements GrantTypeInterface protected $refreshTokenTTL; /** - * @var \League\OAuth2\Server\CryptKey + * @var CryptKey */ protected $privateKey; @@ -147,7 +151,7 @@ abstract class AbstractGrant implements GrantTypeInterface /** * Set the private key * - * @param \League\OAuth2\Server\CryptKey $key + * @param CryptKey $key */ public function setPrivateKey(CryptKey $key) { @@ -258,13 +262,13 @@ abstract class AbstractGrant implements GrantTypeInterface ClientEntityInterface $client, ServerRequestInterface $request ) { - if (is_string($client->getRedirectUri()) + if (\is_string($client->getRedirectUri()) && (strcmp($client->getRedirectUri(), $redirectUri) !== 0) ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); - } elseif (is_array($client->getRedirectUri()) - && in_array($redirectUri, $client->getRedirectUri(), true) === false + } elseif (\is_array($client->getRedirectUri()) + && \in_array($redirectUri, $client->getRedirectUri(), true) === false ) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); @@ -274,8 +278,8 @@ abstract class AbstractGrant implements GrantTypeInterface /** * Validate scopes in the request. * - * @param string $scopes - * @param string $redirectUri + * @param string|array $scopes + * @param string $redirectUri * * @throws OAuthServerException * @@ -283,13 +287,13 @@ abstract class AbstractGrant implements GrantTypeInterface */ public function validateScopes($scopes, $redirectUri = null) { - $scopesList = array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) { - return !empty($scope); - }); + if (!\is_array($scopes)) { + $scopes = $this->convertScopesQueryStringToArray($scopes); + } $validScopes = []; - foreach ($scopesList as $scopeItem) { + foreach ($scopes as $scopeItem) { $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); if ($scope instanceof ScopeEntityInterface === false) { @@ -302,6 +306,20 @@ abstract class AbstractGrant implements GrantTypeInterface return $validScopes; } + /** + * Converts a scopes query string to an array to easily iterate for validation. + * + * @param string $scopes + * + * @return array + */ + private function convertScopesQueryStringToArray($scopes) + { + return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) { + return !empty($scope); + }); + } + /** * Retrieve request parameter. * @@ -315,7 +333,7 @@ abstract class AbstractGrant implements GrantTypeInterface { $requestParameters = (array) $request->getParsedBody(); - return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default; + return $requestParameters[$parameter] ?? $default; } /** @@ -488,16 +506,21 @@ abstract class AbstractGrant implements GrantTypeInterface * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException * - * @return RefreshTokenEntityInterface + * @return RefreshTokenEntityInterface|null */ protected function issueRefreshToken(AccessTokenEntityInterface $accessToken) { - $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; - $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); + + if ($refreshToken === null) { + return null; + } + $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL)); $refreshToken->setAccessToken($accessToken); + $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; + while ($maxGenerationAttempts-- > 0) { $refreshToken->setIdentifier($this->generateUniqueIdentifier()); try { @@ -526,13 +549,13 @@ abstract class AbstractGrant implements GrantTypeInterface try { return bin2hex(random_bytes($length)); // @codeCoverageIgnoreStart - } catch (\TypeError $e) { - throw OAuthServerException::serverError('An unexpected error has occurred'); - } catch (\Error $e) { - throw OAuthServerException::serverError('An unexpected error has occurred'); - } catch (\Exception $e) { + } catch (TypeError $e) { + throw OAuthServerException::serverError('An unexpected error has occurred', $e); + } catch (Error $e) { + throw OAuthServerException::serverError('An unexpected error has occurred', $e); + } catch (Exception $e) { // If you get this message, the CSPRNG failed hard. - throw OAuthServerException::serverError('Could not generate a random string'); + throw OAuthServerException::serverError('Could not generate a random string', $e); } // @codeCoverageIgnoreEnd } @@ -563,7 +586,7 @@ abstract class AbstractGrant implements GrantTypeInterface */ public function validateAuthorizationRequest(ServerRequestInterface $request) { - throw new \LogicException('This grant cannot validate an authorization request'); + throw new LogicException('This grant cannot validate an authorization request'); } /** @@ -571,6 +594,6 @@ abstract class AbstractGrant implements GrantTypeInterface */ public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { - throw new \LogicException('This grant cannot complete an authorization request'); + throw new LogicException('This grant cannot complete an authorization request'); } } diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index ea9cf37e..51c9e90e 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -11,10 +11,10 @@ namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; +use Exception; use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface; use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier; use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier; -use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; @@ -23,7 +23,9 @@ use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use LogicException; use Psr\Http\Message\ServerRequestInterface; +use stdClass; class AuthCodeGrant extends AbstractAuthorizeGrant { @@ -46,6 +48,8 @@ class AuthCodeGrant extends AbstractAuthorizeGrant * @param AuthCodeRepositoryInterface $authCodeRepository * @param RefreshTokenRepositoryInterface $refreshTokenRepository * @param DateInterval $authCodeTTL + * + * @throws Exception */ public function __construct( AuthCodeRepositoryInterface $authCodeRepository, @@ -57,14 +61,13 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $this->authCodeTTL = $authCodeTTL; $this->refreshTokenTTL = new DateInterval('P1M'); - // SHOULD ONLY DO THIS IS SHA256 is supported - $s256Verifier = new S256Verifier(); - $plainVerifier = new PlainVerifier(); + if (in_array('sha256', hash_algos(), true)) { + $s256Verifier = new S256Verifier(); + $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier; + } - $this->codeChallengeVerifiers = [ - $s256Verifier->getMethod() => $s256Verifier, - $plainVerifier->getMethod() => $plainVerifier, - ]; + $plainVerifier = new PlainVerifier(); + $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier; } /** @@ -106,56 +109,22 @@ class AuthCodeGrant extends AbstractAuthorizeGrant throw OAuthServerException::invalidRequest('code'); } - // Validate the authorization code try { $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); - if (time() > $authCodePayload->expire_time) { - throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); - } - if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { - throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); - } + $this->validateAuthorizationCode($authCodePayload, $client, $request); - if ($authCodePayload->client_id !== $client->getIdentifier()) { - throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); - } - // The redirect URI is required in this request - $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); - if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { - throw OAuthServerException::invalidRequest('redirect_uri'); - } - - if ($authCodePayload->redirect_uri !== $redirectUri) { - throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); - } - - $scopes = []; - - foreach ($authCodePayload->scopes as $scopeId) { - $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId); - - if ($scope instanceof ScopeEntityInterface === false) { - // @codeCoverageIgnoreStart - throw OAuthServerException::invalidScope($scopeId); - // @codeCoverageIgnoreEnd - } - - $scopes[] = $scope; - } - - // Finalize the requested scopes $scopes = $this->scopeRepository->finalizeScopes( - $scopes, + $this->validateScopes($authCodePayload->scopes), $this->getIdentifier(), $client, $authCodePayload->user_id ); - } catch (\LogicException $e) { - throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code'); + } catch (LogicException $e) { + throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e); } // Validate code challenge @@ -191,17 +160,18 @@ class AuthCodeGrant extends AbstractAuthorizeGrant } } - // Issue and persist access + refresh tokens + // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); + $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); + $responseType->setAccessToken($accessToken); + + // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); - // Send events to emitter - $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); - $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); - - // Inject tokens into response type - $responseType->setAccessToken($accessToken); - $responseType->setRefreshToken($refreshToken); + if ($refreshToken !== null) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); + $responseType->setRefreshToken($refreshToken); + } // Revoke used auth code $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); @@ -209,6 +179,41 @@ class AuthCodeGrant extends AbstractAuthorizeGrant return $responseType; } + /** + * Validate the authorization code. + * + * @param stdClass $authCodePayload + * @param ClientEntityInterface $client + * @param ServerRequestInterface $request + */ + private function validateAuthorizationCode( + $authCodePayload, + ClientEntityInterface $client, + ServerRequestInterface $request + ) { + if (time() > $authCodePayload->expire_time) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); + } + + if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); + } + + if ($authCodePayload->client_id !== $client->getIdentifier()) { + throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); + } + + // The redirect URI is required in this request + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { + throw OAuthServerException::invalidRequest('redirect_uri'); + } + + if ($authCodePayload->redirect_uri !== $redirectUri) { + throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); + } + } + /** * Return the grant identifier that can be used in matching up requests. * @@ -242,7 +247,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $this->getServerParameter('PHP_AUTH_USER', $request) ); - if (is_null($clientId)) { + if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } @@ -252,12 +257,12 @@ class AuthCodeGrant extends AbstractAuthorizeGrant if ($redirectUri !== null) { $this->validateRedirectUri($redirectUri, $client, $request); - } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1 - || empty($client->getRedirectUri())) { + } elseif (empty($client->getRedirectUri()) || + (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } else { - $redirectUri = is_array($client->getRedirectUri()) + $redirectUri = \is_array($client->getRedirectUri()) ? $client->getRedirectUri()[0] : $client->getRedirectUri(); } @@ -321,14 +326,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { - throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); + throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } - $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) - ? is_array($authorizationRequest->getClient()->getRedirectUri()) - ? $authorizationRequest->getClient()->getRedirectUri()[0] - : $authorizationRequest->getClient()->getRedirectUri() - : $authorizationRequest->getRedirectUri(); + $finalRedirectUri = $authorizationRequest->getRedirectUri() + ?? $this->getClientRedirectUri($authorizationRequest); // The user approved the client, redirect them back with an auth code if ($authorizationRequest->isAuthorizationApproved() === true) { @@ -376,4 +378,18 @@ class AuthCodeGrant extends AbstractAuthorizeGrant ) ); } + + /** + * Get the client redirect URI if not set in the request. + * + * @param AuthorizationRequest $authorizationRequest + * + * @return string + */ + private function getClientRedirectUri(AuthorizationRequest $authorizationRequest) + { + return \is_array($authorizationRequest->getClient()->getRedirectUri()) + ? $authorizationRequest->getClient()->getRedirectUri()[0] + : $authorizationRequest->getClient()->getRedirectUri(); + } } diff --git a/src/Grant/ImplicitGrant.php b/src/Grant/ImplicitGrant.php index 8de22c36..251ea6c2 100644 --- a/src/Grant/ImplicitGrant.php +++ b/src/Grant/ImplicitGrant.php @@ -17,6 +17,7 @@ use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use LogicException; use Psr\Http\Message\ServerRequestInterface; class ImplicitGrant extends AbstractAuthorizeGrant @@ -44,21 +45,21 @@ class ImplicitGrant extends AbstractAuthorizeGrant /** * @param DateInterval $refreshTokenTTL * - * @throw \LogicException + * @throw LogicException */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL) { - throw new \LogicException('The Implicit Grant does not return refresh tokens'); + throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** * @param RefreshTokenRepositoryInterface $refreshTokenRepository * - * @throw \LogicException + * @throw LogicException */ public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) { - throw new \LogicException('The Implicit Grant does not return refresh tokens'); + throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** @@ -93,7 +94,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { - throw new \LogicException('This grant does not used this method'); + throw new LogicException('This grant does not used this method'); } /** @@ -144,13 +145,6 @@ class ImplicitGrant extends AbstractAuthorizeGrant $redirectUri ); - // Finalize the requested scopes - $finalizedScopes = $this->scopeRepository->finalizeScopes( - $scopes, - $this->getIdentifier(), - $client - ); - $stateParameter = $this->getQueryStringParameter('state', $request); $authorizationRequest = new AuthorizationRequest(); @@ -162,7 +156,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant $authorizationRequest->setState($stateParameter); } - $authorizationRequest->setScopes($finalizedScopes); + $authorizationRequest->setScopes($scopes); return $authorizationRequest; } @@ -173,7 +167,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { - throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); + throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) @@ -184,11 +178,19 @@ class ImplicitGrant extends AbstractAuthorizeGrant // The user approved the client, redirect them back with an access token if ($authorizationRequest->isAuthorizationApproved() === true) { + // Finalize the requested scopes + $finalizedScopes = $this->scopeRepository->finalizeScopes( + $authorizationRequest->getScopes(), + $this->getIdentifier(), + $authorizationRequest->getClient(), + $authorizationRequest->getUser()->getIdentifier() + ); + $accessToken = $this->issueAccessToken( $this->accessTokenTTL, $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier(), - $authorizationRequest->getScopes() + $finalizedScopes ); $response = new RedirectResponse(); diff --git a/src/Grant/PasswordGrant.php b/src/Grant/PasswordGrant.php index 7375d436..0dc687b9 100644 --- a/src/Grant/PasswordGrant.php +++ b/src/Grant/PasswordGrant.php @@ -56,17 +56,18 @@ class PasswordGrant extends AbstractGrant // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); - // Issue and persist new tokens + // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes); + $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); + $responseType->setAccessToken($accessToken); + + // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); - // Send events to emitter - $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); - $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); - - // Inject tokens into response - $responseType->setAccessToken($accessToken); - $responseType->setRefreshToken($refreshToken); + if ($refreshToken !== null) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); + $responseType->setRefreshToken($refreshToken); + } return $responseType; } diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index cde4beee..d0794075 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -12,6 +12,7 @@ namespace League\OAuth2\Server\Grant; use DateInterval; +use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestEvent; @@ -62,17 +63,18 @@ class RefreshTokenGrant extends AbstractGrant $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); - // Issue and persist new tokens + // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); + $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); + $responseType->setAccessToken($accessToken); + + // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); - // Send events to emitter - $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); - $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); - - // Inject tokens into response - $responseType->setAccessToken($accessToken); - $responseType->setRefreshToken($refreshToken); + if ($refreshToken !== null) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); + $responseType->setRefreshToken($refreshToken); + } return $responseType; } @@ -95,8 +97,8 @@ class RefreshTokenGrant extends AbstractGrant // Validate refresh token try { $refreshToken = $this->decrypt($encryptedRefreshToken); - } catch (\Exception $e) { - throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token'); + } catch (Exception $e) { + throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); } $refreshTokenData = json_decode($refreshToken, true); diff --git a/src/Middleware/AuthorizationServerMiddleware.php b/src/Middleware/AuthorizationServerMiddleware.php index a5f102fc..9b78b458 100644 --- a/src/Middleware/AuthorizationServerMiddleware.php +++ b/src/Middleware/AuthorizationServerMiddleware.php @@ -9,6 +9,7 @@ namespace League\OAuth2\Server\Middleware; +use Exception; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ResponseInterface; @@ -43,7 +44,7 @@ class AuthorizationServerMiddleware } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); // @codeCoverageIgnoreStart - } catch (\Exception $exception) { + } catch (Exception $exception) { return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) ->generateHttpResponse($response); // @codeCoverageIgnoreEnd diff --git a/src/Middleware/ResourceServerMiddleware.php b/src/Middleware/ResourceServerMiddleware.php index 56d28aee..e152a999 100644 --- a/src/Middleware/ResourceServerMiddleware.php +++ b/src/Middleware/ResourceServerMiddleware.php @@ -9,6 +9,7 @@ namespace League\OAuth2\Server\Middleware; +use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\ResourceServer; use Psr\Http\Message\ResponseInterface; @@ -34,7 +35,7 @@ class ResourceServerMiddleware * @param ResponseInterface $response * @param callable $next * - * @return \Psr\Http\Message\ResponseInterface + * @return ResponseInterface */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { @@ -43,7 +44,7 @@ class ResourceServerMiddleware } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); // @codeCoverageIgnoreStart - } catch (\Exception $exception) { + } catch (Exception $exception) { return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) ->generateHttpResponse($response); // @codeCoverageIgnoreEnd diff --git a/src/Repositories/RefreshTokenRepositoryInterface.php b/src/Repositories/RefreshTokenRepositoryInterface.php index 0c0697bf..a769cf6d 100644 --- a/src/Repositories/RefreshTokenRepositoryInterface.php +++ b/src/Repositories/RefreshTokenRepositoryInterface.php @@ -20,7 +20,7 @@ interface RefreshTokenRepositoryInterface extends RepositoryInterface /** * Creates a new refresh token * - * @return RefreshTokenEntityInterface + * @return RefreshTokenEntityInterface|null */ public function getNewRefreshToken(); diff --git a/src/ResponseTypes/AbstractResponseType.php b/src/ResponseTypes/AbstractResponseType.php index d013bab0..192f52aa 100644 --- a/src/ResponseTypes/AbstractResponseType.php +++ b/src/ResponseTypes/AbstractResponseType.php @@ -54,7 +54,7 @@ abstract class AbstractResponseType implements ResponseTypeInterface /** * Set the private key * - * @param \League\OAuth2\Server\CryptKey $key + * @param CryptKey $key */ public function setPrivateKey(CryptKey $key) { diff --git a/tests/AuthorizationServerTest.php b/tests/AuthorizationServerTest.php index 9fe1a020..f88cd733 100644 --- a/tests/AuthorizationServerTest.php +++ b/tests/AuthorizationServerTest.php @@ -4,6 +4,7 @@ namespace LeagueTests; use DateInterval; use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; @@ -110,6 +111,95 @@ class AuthorizationServerTest extends TestCase $this->assertInstanceOf(BearerTokenResponse::class, $method->invoke($server)); } + public function testGetResponseTypeExtended() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; + $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; + + $server = new class($clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), $privateKey, $encryptionKey) extends AuthorizationServer { + protected function getResponseType() + { + $this->responseType = new class extends BearerTokenResponse { + /* @return null|CryptKey */ + public function getPrivateKey() + { + return $this->privateKey; + } + + public function getEncryptionKey() + { + return $this->encryptionKey; + } + }; + + return parent::getResponseType(); + } + }; + + $abstractGrantReflection = new \ReflectionClass($server); + $method = $abstractGrantReflection->getMethod('getResponseType'); + $method->setAccessible(true); + $responseType = $method->invoke($server); + + $this->assertInstanceOf(BearerTokenResponse::class, $responseType); + // generated instances should have keys setup + $this->assertSame($privateKey, $responseType->getPrivateKey()->getKeyPath()); + $this->assertSame($encryptionKey, $responseType->getEncryptionKey()); + } + + public function testMultipleRequestsGetDifferentResponseTypeInstances() + { + $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; + $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; + + $responseTypePrototype = new class extends BearerTokenResponse { + /* @return null|CryptKey */ + public function getPrivateKey() + { + return $this->privateKey; + } + + public function getEncryptionKey() + { + return $this->encryptionKey; + } + }; + + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + + $server = new AuthorizationServer( + $clientRepository, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + $privateKey, + $encryptionKey, + $responseTypePrototype + ); + + $abstractGrantReflection = new \ReflectionClass($server); + $method = $abstractGrantReflection->getMethod('getResponseType'); + $method->setAccessible(true); + + $responseTypeA = $method->invoke($server); + $responseTypeB = $method->invoke($server); + + // prototype should not get changed + $this->assertNull($responseTypePrototype->getPrivateKey()); + $this->assertNull($responseTypePrototype->getEncryptionKey()); + + // generated instances should have keys setup + $this->assertSame($privateKey, $responseTypeA->getPrivateKey()->getKeyPath()); + $this->assertSame($encryptionKey, $responseTypeA->getEncryptionKey()); + + // all instances should be different but based on the same prototype + $this->assertSame(get_class($responseTypePrototype), get_class($responseTypeA)); + $this->assertSame(get_class($responseTypePrototype), get_class($responseTypeB)); + $this->assertNotSame($responseTypePrototype, $responseTypeA); + $this->assertNotSame($responseTypePrototype, $responseTypeB); + $this->assertNotSame($responseTypeA, $responseTypeB); + } + public function testCompleteAuthorizationRequest() { $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); diff --git a/tests/Exception/OAuthServerExceptionTest.php b/tests/Exception/OAuthServerExceptionTest.php index 8efce51f..82ecef02 100644 --- a/tests/Exception/OAuthServerExceptionTest.php +++ b/tests/Exception/OAuthServerExceptionTest.php @@ -2,6 +2,7 @@ namespace LeagueTests\Exception; +use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AbstractGrant; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; @@ -78,4 +79,19 @@ class OAuthServerExceptionTest extends TestCase $this->assertFalse($exceptionWithoutRedirect->hasRedirect()); } + + public function testHasPrevious() + { + $previous = new Exception('This is the previous'); + $exceptionWithPrevious = OAuthServerException::accessDenied(null, null, $previous); + + $this->assertSame('This is the previous', $exceptionWithPrevious->getPrevious()->getMessage()); + } + + public function testDoesNotHavePrevious() + { + $exceptionWithoutPrevious = OAuthServerException::accessDenied(); + + $this->assertNull($exceptionWithoutPrevious->getPrevious()); + } } diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php index 33103130..ec8d3f47 100644 --- a/tests/Grant/AbstractGrantTest.php +++ b/tests/Grant/AbstractGrantTest.php @@ -340,6 +340,27 @@ class AbstractGrantTest extends TestCase $this->assertEquals($accessToken, $refreshToken->getAccessToken()); } + public function testIssueNullRefreshToken() + { + $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepoMock + ->expects($this->once()) + ->method('getNewRefreshToken') + ->willReturn(null); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setRefreshTokenTTL(new \DateInterval('PT1M')); + $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); + $issueRefreshTokenMethod->setAccessible(true); + + $accessToken = new AccessTokenEntity(); + $this->assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken)); + } + public function testIssueAccessToken() { $accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php index 9b6f24d1..07b220c5 100644 --- a/tests/Grant/AuthCodeGrantTest.php +++ b/tests/Grant/AuthCodeGrantTest.php @@ -740,6 +740,74 @@ class AuthCodeGrantTest extends TestCase $this->assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } + public function testRespondToAccessTokenRequestNullRefreshToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $refreshTokenRepositoryMock, + new \DateInterval('PT10M') + ); + + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertInstanceOf(AccessTokenEntityInterface::class, $response->getAccessToken()); + $this->assertNull($response->getRefreshToken()); + } + public function testRespondToAccessTokenRequestCodeChallengePlain() { $client = new ClientEntity(); diff --git a/tests/Grant/ImplicitGrantTest.php b/tests/Grant/ImplicitGrantTest.php index 5e702e01..b63c3ffa 100644 --- a/tests/Grant/ImplicitGrantTest.php +++ b/tests/Grant/ImplicitGrantTest.php @@ -95,7 +95,6 @@ class ImplicitGrantTest extends TestCase $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); - $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); @@ -130,7 +129,6 @@ class ImplicitGrantTest extends TestCase $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); - $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); $grant->setClientRepository($clientRepositoryMock); @@ -293,9 +291,13 @@ class ImplicitGrantTest extends TestCase $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); - $grant = new ImplicitGrant(new DateInterval('PT10M')); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); $this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } @@ -316,9 +318,13 @@ class ImplicitGrantTest extends TestCase $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); - $grant = new ImplicitGrant(new DateInterval('PT10M')); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); $grant->completeAuthorizationRequest($authRequest); } @@ -343,9 +349,13 @@ class ImplicitGrantTest extends TestCase $accessTokenRepositoryMock->expects($this->at(0))->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); $accessTokenRepositoryMock->expects($this->at(1))->method('persistNewAccessToken')->willReturnSelf(); - $grant = new ImplicitGrant(new DateInterval('PT10M')); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); $this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } @@ -367,9 +377,13 @@ class ImplicitGrantTest extends TestCase $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(OAuthServerException::serverError('something bad happened')); - $grant = new ImplicitGrant(new DateInterval('PT10M')); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); $grant->completeAuthorizationRequest($authRequest); } @@ -391,9 +405,13 @@ class ImplicitGrantTest extends TestCase $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); - $grant = new ImplicitGrant(new DateInterval('PT10M')); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); $grant->completeAuthorizationRequest($authRequest); } diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php index 88592339..7fc99e83 100644 --- a/tests/Grant/PasswordGrantTest.php +++ b/tests/Grant/PasswordGrantTest.php @@ -81,6 +81,52 @@ class PasswordGrantTest extends TestCase $this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); } + public function testRespondToRequestNullRefreshToken() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + $userEntity = new UserEntity(); + $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity); + + $scope = new ScopeEntity(); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setDefaultScope(self::DEFAULT_SCOPE); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'username' => 'foo', + 'password' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken()); + $this->assertNull($responseType->getRefreshToken()); + } + /** * @expectedException \League\OAuth2\Server\Exception\OAuthServerException */ diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php index 6d1cbdf2..e895f16b 100644 --- a/tests/Grant/RefreshTokenGrantTest.php +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -95,6 +95,63 @@ class RefreshTokenGrantTest extends TestCase $this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); } + public function testRespondToRequestNullRefreshToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeEntity = new ScopeEntity(); + $scopeEntity->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->expects($this->once())->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); + $refreshTokenRepositoryMock->expects($this->never())->method('persistNewRefreshToken'); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + 'scopes' => ['foo'], + ]); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken()); + $this->assertNull($responseType->getRefreshToken()); + } + public function testRespondToReducedScopes() { $client = new ClientEntity(); diff --git a/tests/Middleware/AuthorizationServerMiddlewareTest.php b/tests/Middleware/AuthorizationServerMiddlewareTest.php index a7ef803d..c8ed7d1a 100644 --- a/tests/Middleware/AuthorizationServerMiddlewareTest.php +++ b/tests/Middleware/AuthorizationServerMiddlewareTest.php @@ -105,7 +105,7 @@ class AuthorizationServerMiddlewareTest extends TestCase $response = $exception->generateHttpResponse(new Response()); $this->assertEquals(302, $response->getStatusCode()); - $this->assertEquals('http://foo/bar?error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', + $this->assertEquals('http://foo/bar?error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed', $response->getHeader('location')[0]); } @@ -115,7 +115,7 @@ class AuthorizationServerMiddlewareTest extends TestCase $response = $exception->generateHttpResponse(new Response(), true); $this->assertEquals(302, $response->getStatusCode()); - $this->assertEquals('http://foo/bar#error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', + $this->assertEquals('http://foo/bar#error=invalid_scope&error_description=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed', $response->getHeader('location')[0]); } }