Merge remote-tracking branch 'upstream/8.0.0' into protect-client-entity-gets

This commit is contained in:
sephster 2019-06-23 17:23:40 +01:00
commit e1324b88b2
No known key found for this signature in database
GPG Key ID: 077754CA23023F4F
35 changed files with 780 additions and 249 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ phpunit.xml
examples/public.key examples/public.key
examples/private.key examples/private.key
build build
*.orig

View File

@ -4,6 +4,7 @@ enabled:
- binary_operator_spaces - binary_operator_spaces
- blank_line_before_return - blank_line_before_return
- concat_with_spaces - concat_with_spaces
- fully_qualified_strict_types
- function_typehint_space - function_typehint_space
- hash_to_slash_comment - hash_to_slash_comment
- include - include
@ -40,7 +41,6 @@ enabled:
- print_to_echo - print_to_echo
- short_array_syntax - short_array_syntax
- short_scalar_cast - short_scalar_cast
- simplified_null_return
- single_quote - single_quote
- spaces_cast - spaces_cast
- standardize_not_equal - standardize_not_equal

View File

@ -12,9 +12,9 @@ env:
- DEPENDENCIES="--prefer-lowest --prefer-stable" - DEPENDENCIES="--prefer-lowest --prefer-stable"
php: php:
- 7.0
- 7.1 - 7.1
- 7.2 - 7.2
- 7.3
install: install:
- composer update --no-interaction --prefer-dist $DEPENDENCIES - composer update --no-interaction --prefer-dist $DEPENDENCIES

View File

@ -7,13 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased] ## [Unreleased]
### Added ### 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) - 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) - Function `validateClient()` added to validate clients which was previously performed by the `getClientEntity()` function (PR #938)
### Changed ### 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) - 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) - `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) - 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 ### Removed
- `enableCodeExchangeProof` flag (PR #938) - `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 ## [7.2.0] - released 2018-06-23
@ -424,7 +460,12 @@ Version 5 is a complete code rewrite.
- First major release - 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.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.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 [7.0.0]: https://github.com/thephpleague/oauth2-server/compare/6.1.1...7.0.0

View File

@ -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: The following versions of PHP are supported:
* PHP 7.0
* PHP 7.1 * PHP 7.1
* PHP 7.2 * 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. 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) * [Drupal](https://www.drupal.org/project/simple_oauth)
* [Laravel Passport](https://github.com/laravel/passport) * [Laravel Passport](https://github.com/laravel/passport)
* [OAuth 2 Server for CakePHP 3](https://github.com/uafrica/oauth-server) * [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 ## 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. 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 ## 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 ## License

View File

@ -4,19 +4,21 @@
"homepage": "https://oauth2.thephpleague.com/", "homepage": "https://oauth2.thephpleague.com/",
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": ">=7.0.0", "php": ">=7.1.0",
"ext-openssl": "*", "ext-openssl": "*",
"league/event": "^2.1", "league/event": "^2.1",
"lcobucci/jwt": "^3.2.2", "lcobucci/jwt": "^3.2.2",
"psr/http-message": "^1.0.1", "psr/http-message": "^1.0.1",
"defuse/php-encryption": "^2.1" "defuse/php-encryption": "^2.1",
"ext-json": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^6.3 || ^7.0", "phpunit/phpunit": "^6.3 || ^7.0",
"zendframework/zend-diactoros": "^1.3.2", "zendframework/zend-diactoros": "^1.3.2",
"phpstan/phpstan": "^0.9.2", "phpstan/phpstan": "^0.9.2",
"phpstan/phpstan-phpunit": "^0.9.4", "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": [ "repositories": [
{ {
@ -46,6 +48,12 @@
"email": "hello@alexbilbie.com", "email": "hello@alexbilbie.com",
"homepage": "http://www.alexbilbie.com", "homepage": "http://www.alexbilbie.com",
"role": "Developer" "role": "Developer"
},
{
"name": "Andy Millington",
"email": "andrew@noexceptions.io",
"homepage": "https://www.noexceptions.io",
"role": "Developer"
} }
], ],
"replace": { "replace": {

View File

@ -1,14 +1,13 @@
{ {
"require": { "require": {
"slim/slim": "3.0.*" "slim/slim": "^3.0.0"
}, },
"require-dev": { "require-dev": {
"league/event": "^2.1", "league/event": "^2.1",
"lcobucci/jwt": "^3.1", "lcobucci/jwt": "^3.2",
"paragonie/random_compat": "^2.0",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"defuse/php-encryption": "^2.1", "defuse/php-encryption": "^2.2",
"zendframework/zend-diactoros": "^1.0" "zendframework/zend-diactoros": "^2.0.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

143
examples/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6701e0eaa09f74e1ebb19c3d61f39068", "content-hash": "97f2878428e37d1d8e5418cc85cbfa3d",
"packages": [ "packages": [
{ {
"name": "container-interop/container-interop", "name": "container-interop/container-interop",
@ -39,21 +39,24 @@
}, },
{ {
"name": "nikic/fast-route", "name": "nikic/fast-route",
"version": "v0.6.0", "version": "v1.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/FastRoute.git", "url": "https://github.com/nikic/FastRoute.git",
"reference": "31fa86924556b80735f98b294a7ffdfb26789f22" "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/31fa86924556b80735f98b294a7ffdfb26789f22", "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "31fa86924556b80735f98b294a7ffdfb26789f22", "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.4.0" "php": ">=5.4.0"
}, },
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -78,7 +81,7 @@
"router", "router",
"routing" "routing"
], ],
"time": "2015-06-18T19:15:47+00:00" "time": "2018-02-13T20:26:39+00:00"
}, },
{ {
"name": "pimple/pimple", "name": "pimple/pimple",
@ -231,27 +234,32 @@
}, },
{ {
"name": "slim/slim", "name": "slim/slim",
"version": "3.0.0", "version": "3.11.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/slimphp/Slim.git", "url": "https://github.com/slimphp/Slim.git",
"reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e" "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/3b06f0f2d84dabbe81b6cea46ace46a3e883253e", "url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
"reference": "3b06f0f2d84dabbe81b6cea46ace46a3e883253e", "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"container-interop/container-interop": "^1.1", "container-interop/container-interop": "^1.2",
"nikic/fast-route": "^0.6", "nikic/fast-route": "^1.0",
"php": ">=5.5.0", "php": ">=5.5.0",
"pimple/pimple": "^3.0", "pimple/pimple": "^3.0",
"psr/container": "^1.0",
"psr/http-message": "^1.0" "psr/http-message": "^1.0"
}, },
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.0" "phpunit/phpunit": "^4.0",
"squizlabs/php_codesniffer": "^2.5"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -286,14 +294,14 @@
} }
], ],
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", "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": [ "keywords": [
"api", "api",
"framework", "framework",
"micro", "micro",
"router" "router"
], ],
"time": "2015-12-07T14:11:09+00:00" "time": "2018-09-16T10:54:21+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -470,33 +478,29 @@
}, },
{ {
"name": "paragonie/random_compat", "name": "paragonie/random_compat",
"version": "v2.0.17", "version": "v9.99.99",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paragonie/random_compat.git", "url": "https://github.com/paragonie/random_compat.git",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.2.0" "php": "^7"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "4.*|5.*" "phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
}, },
"suggest": { "suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
}, },
"type": "library", "type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"MIT" "MIT"
@ -515,41 +519,97 @@
"pseudorandom", "pseudorandom",
"random" "random"
], ],
"time": "2018-07-04T16:31:37+00:00" "time": "2018-07-02T15:55:56+00:00"
}, },
{ {
"name": "zendframework/zend-diactoros", "name": "psr/http-factory",
"version": "1.8.4", "version": "1.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/zendframework/zend-diactoros.git", "url": "https://github.com/php-fig/http-factory.git",
"reference": "736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba" "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba", "url": "https://api.github.com/repos/php-fig/http-factory/zipball/378bfe27931ecc54ff824a20d6f6bfc303bbd04c",
"reference": "736ffa7c2bfa4a60e8a10acb316fa2ac456c5fba", "reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c",
"shasum": "" "shasum": ""
}, },
"require": { "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" "psr/http-message": "^1.0"
}, },
"provide": { "provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0" "psr/http-message-implementation": "1.0"
}, },
"require-dev": { "require-dev": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7", "http-interop/http-factory-tests": "^0.5.0",
"zendframework/zend-coding-standard": "~1.0" "php-http/psr7-integration-tests": "dev-master",
"phpunit/phpunit": "^7.0.2",
"zendframework/zend-coding-standard": "~1.0.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.8.x-dev", "dev-master": "2.0.x-dev",
"dev-develop": "1.9.x-dev", "dev-develop": "2.1.x-dev",
"dev-release-2.0": "2.0.x-dev" "dev-release-1.8": "1.8.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -569,16 +629,15 @@
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"BSD-2-Clause" "BSD-3-Clause"
], ],
"description": "PSR HTTP Message implementations", "description": "PSR HTTP Message implementations",
"homepage": "https://github.com/zendframework/zend-diactoros",
"keywords": [ "keywords": [
"http", "http",
"psr", "psr",
"psr-7" "psr-7"
], ],
"time": "2018-08-01T13:47:49+00:00" "time": "2018-09-27T19:49:04+00:00"
} }
], ],
"aliases": [], "aliases": [],

View File

@ -11,13 +11,9 @@ namespace OAuth2ServerExamples\Entities;
use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
class ScopeEntity implements ScopeEntityInterface class ScopeEntity implements ScopeEntityInterface
{ {
use EntityTrait; use EntityTrait, ScopeTrait;
public function jsonSerialize()
{
return $this->getIdentifier();
}
} }

View File

@ -18,7 +18,7 @@ class RefreshTokenRepository implements RefreshTokenRepositoryInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntityInterface) public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
{ {
// Some logic to persist the refresh token in a database // Some logic to persist the refresh token in a database
} }

View File

@ -50,7 +50,7 @@ class AuthorizationServer implements EmitterAwareInterface
protected $publicKey; protected $publicKey;
/** /**
* @var null|ResponseTypeInterface * @var ResponseTypeInterface
*/ */
protected $responseType; protected $responseType;
@ -104,8 +104,16 @@ class AuthorizationServer implements EmitterAwareInterface
if ($privateKey instanceof CryptKey === false) { if ($privateKey instanceof CryptKey === false) {
$privateKey = new CryptKey($privateKey); $privateKey = new CryptKey($privateKey);
} }
$this->privateKey = $privateKey; $this->privateKey = $privateKey;
$this->encryptionKey = $encryptionKey; $this->encryptionKey = $encryptionKey;
if ($responseType === null) {
$responseType = new BearerTokenResponse();
} else {
$responseType = clone $responseType;
}
$this->responseType = $responseType; $this->responseType = $responseType;
} }
@ -205,16 +213,15 @@ class AuthorizationServer implements EmitterAwareInterface
*/ */
protected function getResponseType() protected function getResponseType()
{ {
if ($this->responseType instanceof ResponseTypeInterface === false) { $responseType = clone $this->responseType;
$this->responseType = new BearerTokenResponse();
if ($responseType instanceof AbstractResponseType) {
$responseType->setPrivateKey($this->privateKey);
} }
if ($this->responseType instanceof AbstractResponseType === true) { $responseType->setEncryptionKey($this->encryptionKey);
$this->responseType->setPrivateKey($this->privateKey);
}
$this->responseType->setEncryptionKey($this->encryptionKey);
return $this->responseType; return $responseType;
} }
/** /**

View File

@ -9,6 +9,8 @@
namespace League\OAuth2\Server\AuthorizationValidators; namespace League\OAuth2\Server\AuthorizationValidators;
use BadMethodCallException;
use InvalidArgumentException;
use Lcobucci\JWT\Parser; use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\ValidationData; use Lcobucci\JWT\ValidationData;
@ -17,6 +19,7 @@ use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
class BearerTokenValidator implements AuthorizationValidatorInterface class BearerTokenValidator implements AuthorizationValidatorInterface
{ {
@ -28,7 +31,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
private $accessTokenRepository; private $accessTokenRepository;
/** /**
* @var \League\OAuth2\Server\CryptKey * @var CryptKey
*/ */
protected $publicKey; protected $publicKey;
@ -43,7 +46,7 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
/** /**
* Set the public key * Set the public key
* *
* @param \League\OAuth2\Server\CryptKey $key * @param CryptKey $key
*/ */
public function setPublicKey(CryptKey $key) public function setPublicKey(CryptKey $key)
{ {
@ -69,8 +72,8 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) { if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified'); throw OAuthServerException::accessDenied('Access token could not be verified');
} }
} catch (\BadMethodCallException $exception) { } catch (BadMethodCallException $exception) {
throw OAuthServerException::accessDenied('Access token is not signed'); throw OAuthServerException::accessDenied('Access token is not signed', null, $exception);
} }
// Ensure access token hasn't expired // Ensure access token hasn't expired
@ -92,12 +95,12 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
->withAttribute('oauth_client_id', $token->getClaim('aud')) ->withAttribute('oauth_client_id', $token->getClaim('aud'))
->withAttribute('oauth_user_id', $token->getClaim('sub')) ->withAttribute('oauth_user_id', $token->getClaim('sub'))
->withAttribute('oauth_scopes', $token->getClaim('scopes')); ->withAttribute('oauth_scopes', $token->getClaim('scopes'));
} catch (\InvalidArgumentException $exception) { } catch (InvalidArgumentException $exception) {
// JWT couldn't be parsed so return the request as is // JWT couldn't be parsed so return the request as is
throw OAuthServerException::accessDenied($exception->getMessage()); throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
} catch (\RuntimeException $exception) { } catch (RuntimeException $exception) {
//JWR couldn't be parsed so return the request as is //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);
} }
} }
} }

View File

@ -11,6 +11,9 @@
namespace League\OAuth2\Server; namespace League\OAuth2\Server;
use LogicException;
use RuntimeException;
class CryptKey class CryptKey
{ {
const RSA_KEY_PATTERN = const RSA_KEY_PATTERN =
@ -42,7 +45,7 @@ class CryptKey
} }
if (!file_exists($keyPath) || !is_readable($keyPath)) { 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) { if ($keyPermissionsCheck === true) {
@ -64,7 +67,7 @@ class CryptKey
/** /**
* @param string $key * @param string $key
* *
* @throws \RuntimeException * @throws RuntimeException
* *
* @return string * @return string
*/ */
@ -79,19 +82,19 @@ class CryptKey
if (!touch($keyPath)) { if (!touch($keyPath)) {
// @codeCoverageIgnoreStart // @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 // @codeCoverageIgnoreEnd
} }
if (file_put_contents($keyPath, $key) === false) { if (file_put_contents($keyPath, $key) === false) {
// @codeCoverageIgnoreStart // @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 // @codeCoverageIgnoreEnd
} }
if (chmod($keyPath, 0600) === false) { if (chmod($keyPath, 0600) === false) {
// @codeCoverageIgnoreStart // @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 // @codeCoverageIgnoreEnd
} }

View File

@ -13,6 +13,8 @@ namespace League\OAuth2\Server;
use Defuse\Crypto\Crypto; use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key; use Defuse\Crypto\Key;
use Exception;
use LogicException;
trait CryptTrait trait CryptTrait
{ {
@ -26,7 +28,7 @@ trait CryptTrait
* *
* @param string $unencryptedData * @param string $unencryptedData
* *
* @throws \LogicException * @throws LogicException
* *
* @return string * @return string
*/ */
@ -38,8 +40,8 @@ trait CryptTrait
} }
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
} catch (\Exception $e) { } catch (Exception $e) {
throw new \LogicException($e->getMessage()); throw new LogicException($e->getMessage(), null, $e);
} }
} }
@ -48,7 +50,7 @@ trait CryptTrait
* *
* @param string $encryptedData * @param string $encryptedData
* *
* @throws \LogicException * @throws LogicException
* *
* @return string * @return string
*/ */
@ -60,8 +62,8 @@ trait CryptTrait
} }
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
} catch (\Exception $e) { } catch (Exception $e) {
throw new \LogicException($e->getMessage()); throw new LogicException($e->getMessage(), null, $e);
} }
} }

View File

@ -9,7 +9,9 @@
namespace League\OAuth2\Server\Entities; namespace League\OAuth2\Server\Entities;
interface ScopeEntityInterface extends \JsonSerializable use JsonSerializable;
interface ScopeEntityInterface extends JsonSerializable
{ {
/** /**
* Get the scope's identifier. * Get the scope's identifier.

View File

@ -0,0 +1,28 @@
<?php
/**
* @author Andrew Millington <andrew@noexceptions.io>
* @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();
}

View File

@ -9,10 +9,12 @@
namespace League\OAuth2\Server\Exception; namespace League\OAuth2\Server\Exception;
use Exception;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Throwable;
class OAuthServerException extends \Exception class OAuthServerException extends Exception
{ {
/** /**
* @var int * @var int
@ -53,17 +55,18 @@ class OAuthServerException extends \Exception
* @param int $httpStatusCode HTTP status code to send (default = 400) * @param int $httpStatusCode HTTP status code to send (default = 400)
* @param null|string $hint A helper hint * @param null|string $hint A helper hint
* @param null|string $redirectUri A HTTP URI to redirect the user back to * @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->httpStatusCode = $httpStatusCode;
$this->errorType = $errorType; $this->errorType = $errorType;
$this->hint = $hint; $this->hint = $hint;
$this->redirectUri = $redirectUri; $this->redirectUri = $redirectUri;
$this->payload = [ $this->payload = [
'error' => $errorType, 'error' => $errorType,
'message' => $message, 'error_description' => $message,
]; ];
if ($hint !== null) { if ($hint !== null) {
$this->payload['hint'] = $hint; $this->payload['hint'] = $hint;
@ -77,7 +80,15 @@ class OAuthServerException extends \Exception
*/ */
public function getPayload() 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 * @param ServerRequestInterface $serverRequest
*/ */
public function setServerRequest($serverRequest) public function setServerRequest(ServerRequestInterface $serverRequest)
{ {
$this->serverRequest = $serverRequest; $this->serverRequest = $serverRequest;
} }
@ -118,16 +129,17 @@ class OAuthServerException extends \Exception
* *
* @param string $parameter The invalid parameter * @param string $parameter The invalid parameter
* @param null|string $hint * @param null|string $hint
* @param Throwable $previous Previous exception
* *
* @return static * @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, ' . $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
'includes a parameter more than once, or is otherwise malformed.'; 'includes a parameter more than once, or is otherwise malformed.';
$hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint; $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 * @return static
*/ */
public static function invalidClient($serverRequest) public static function invalidClient(ServerRequestInterface $serverRequest)
{ {
$exception = new static('Client authentication failed', 4, 'invalid_client', 401); $exception = new static('Client authentication failed', 4, 'invalid_client', 401);
@ -184,19 +196,23 @@ class OAuthServerException extends \Exception
* Server error. * Server error.
* *
* @param string $hint * @param string $hint
* @param Throwable $previous
* *
* @return static * @return static
* *
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
public static function serverError($hint) public static function serverError($hint, Throwable $previous = null)
{ {
return new static( return new static(
'The authorization server encountered an unexpected condition which prevented it from fulfilling' 'The authorization server encountered an unexpected condition which prevented it from fulfilling'
. ' the request: ' . $hint, . ' the request: ' . $hint,
7, 7,
'server_error', 'server_error',
500 500,
null,
null,
$previous
); );
} }
@ -204,12 +220,13 @@ class OAuthServerException extends \Exception
* Invalid refresh token. * Invalid refresh token.
* *
* @param null|string $hint * @param null|string $hint
* @param Throwable $previous
* *
* @return static * @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 $hint
* @param null|string $redirectUri * @param null|string $redirectUri
* @param Throwable $previous
* *
* @return static * @return static
*/ */
public static function accessDenied($hint = null, $redirectUri = null) public static function accessDenied($hint = null, $redirectUri = null, Throwable $previous = null)
{ {
return new static( return new static(
'The resource owner or authorization server denied the request.', 'The resource owner or authorization server denied the request.',
@ -228,7 +246,8 @@ class OAuthServerException extends \Exception
'access_denied', 'access_denied',
401, 401,
$hint, $hint,
$redirectUri $redirectUri,
$previous
); );
} }

View File

@ -11,6 +11,9 @@ namespace League\OAuth2\Server\Exception;
class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException
{ {
/**
* @return UniqueTokenIdentifierConstraintViolationException
*/
public static function create() public static function create()
{ {
$errorMessage = 'Could not create unique access token identifier'; $errorMessage = 'Could not create unique access token identifier';

View File

@ -12,6 +12,8 @@ namespace League\OAuth2\Server\Grant;
use DateInterval; use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Error;
use Exception;
use League\Event\EmitterAwareTrait; use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait; 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\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use LogicException;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use TypeError;
/** /**
* Abstract grant class. * Abstract grant class.
@ -79,7 +83,7 @@ abstract class AbstractGrant implements GrantTypeInterface
protected $refreshTokenTTL; protected $refreshTokenTTL;
/** /**
* @var \League\OAuth2\Server\CryptKey * @var CryptKey
*/ */
protected $privateKey; protected $privateKey;
@ -147,7 +151,7 @@ abstract class AbstractGrant implements GrantTypeInterface
/** /**
* Set the private key * Set the private key
* *
* @param \League\OAuth2\Server\CryptKey $key * @param CryptKey $key
*/ */
public function setPrivateKey(CryptKey $key) public function setPrivateKey(CryptKey $key)
{ {
@ -258,13 +262,13 @@ abstract class AbstractGrant implements GrantTypeInterface
ClientEntityInterface $client, ClientEntityInterface $client,
ServerRequestInterface $request ServerRequestInterface $request
) { ) {
if (is_string($client->getRedirectUri()) if (\is_string($client->getRedirectUri())
&& (strcmp($client->getRedirectUri(), $redirectUri) !== 0) && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
) { ) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request); throw OAuthServerException::invalidClient($request);
} elseif (is_array($client->getRedirectUri()) } elseif (\is_array($client->getRedirectUri())
&& in_array($redirectUri, $client->getRedirectUri(), true) === false && \in_array($redirectUri, $client->getRedirectUri(), true) === false
) { ) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request); throw OAuthServerException::invalidClient($request);
@ -274,7 +278,7 @@ abstract class AbstractGrant implements GrantTypeInterface
/** /**
* Validate scopes in the request. * Validate scopes in the request.
* *
* @param string $scopes * @param string|array $scopes
* @param string $redirectUri * @param string $redirectUri
* *
* @throws OAuthServerException * @throws OAuthServerException
@ -283,13 +287,13 @@ abstract class AbstractGrant implements GrantTypeInterface
*/ */
public function validateScopes($scopes, $redirectUri = null) public function validateScopes($scopes, $redirectUri = null)
{ {
$scopesList = array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) { if (!\is_array($scopes)) {
return !empty($scope); $scopes = $this->convertScopesQueryStringToArray($scopes);
}); }
$validScopes = []; $validScopes = [];
foreach ($scopesList as $scopeItem) { foreach ($scopes as $scopeItem) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
if ($scope instanceof ScopeEntityInterface === false) { if ($scope instanceof ScopeEntityInterface === false) {
@ -302,6 +306,20 @@ abstract class AbstractGrant implements GrantTypeInterface
return $validScopes; 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. * Retrieve request parameter.
* *
@ -315,7 +333,7 @@ abstract class AbstractGrant implements GrantTypeInterface
{ {
$requestParameters = (array) $request->getParsedBody(); $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 OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException * @throws UniqueTokenIdentifierConstraintViolationException
* *
* @return RefreshTokenEntityInterface * @return RefreshTokenEntityInterface|null
*/ */
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken) protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
{ {
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
if ($refreshToken === null) {
return null;
}
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL)); $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
$refreshToken->setAccessToken($accessToken); $refreshToken->setAccessToken($accessToken);
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
while ($maxGenerationAttempts-- > 0) { while ($maxGenerationAttempts-- > 0) {
$refreshToken->setIdentifier($this->generateUniqueIdentifier()); $refreshToken->setIdentifier($this->generateUniqueIdentifier());
try { try {
@ -526,13 +549,13 @@ abstract class AbstractGrant implements GrantTypeInterface
try { try {
return bin2hex(random_bytes($length)); return bin2hex(random_bytes($length));
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (\TypeError $e) { } catch (TypeError $e) {
throw OAuthServerException::serverError('An unexpected error has occurred'); throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (\Error $e) { } catch (Error $e) {
throw OAuthServerException::serverError('An unexpected error has occurred'); throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (\Exception $e) { } catch (Exception $e) {
// If you get this message, the CSPRNG failed hard. // 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 // @codeCoverageIgnoreEnd
} }
@ -563,7 +586,7 @@ abstract class AbstractGrant implements GrantTypeInterface
*/ */
public function validateAuthorizationRequest(ServerRequestInterface $request) 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) 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');
} }
} }

View File

@ -11,10 +11,10 @@ namespace League\OAuth2\Server\Grant;
use DateInterval; use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Exception;
use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface; use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier; use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier; use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; 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\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use stdClass;
class AuthCodeGrant extends AbstractAuthorizeGrant class AuthCodeGrant extends AbstractAuthorizeGrant
{ {
@ -46,6 +48,8 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
* @param AuthCodeRepositoryInterface $authCodeRepository * @param AuthCodeRepositoryInterface $authCodeRepository
* @param RefreshTokenRepositoryInterface $refreshTokenRepository * @param RefreshTokenRepositoryInterface $refreshTokenRepository
* @param DateInterval $authCodeTTL * @param DateInterval $authCodeTTL
*
* @throws Exception
*/ */
public function __construct( public function __construct(
AuthCodeRepositoryInterface $authCodeRepository, AuthCodeRepositoryInterface $authCodeRepository,
@ -57,14 +61,13 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$this->authCodeTTL = $authCodeTTL; $this->authCodeTTL = $authCodeTTL;
$this->refreshTokenTTL = new DateInterval('P1M'); $this->refreshTokenTTL = new DateInterval('P1M');
// SHOULD ONLY DO THIS IS SHA256 is supported if (in_array('sha256', hash_algos(), true)) {
$s256Verifier = new S256Verifier(); $s256Verifier = new S256Verifier();
$plainVerifier = new PlainVerifier(); $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
}
$this->codeChallengeVerifiers = [ $plainVerifier = new PlainVerifier();
$s256Verifier->getMethod() => $s256Verifier, $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
$plainVerifier->getMethod() => $plainVerifier,
];
} }
/** /**
@ -106,56 +109,22 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
throw OAuthServerException::invalidRequest('code'); throw OAuthServerException::invalidRequest('code');
} }
// Validate the authorization code
try { try {
$authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); $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) { $this->validateAuthorizationCode($authCodePayload, $client, $request);
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');
}
$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->scopeRepository->finalizeScopes(
$scopes, $this->validateScopes($authCodePayload->scopes),
$this->getIdentifier(), $this->getIdentifier(),
$client, $client,
$authCodePayload->user_id $authCodePayload->user_id
); );
} catch (\LogicException $e) { } catch (LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code'); throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
} }
// Validate code challenge // 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); $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); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response type
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken); $responseType->setRefreshToken($refreshToken);
}
// Revoke used auth code // Revoke used auth code
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
@ -209,6 +179,41 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
return $responseType; 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. * 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) $this->getServerParameter('PHP_AUTH_USER', $request)
); );
if (is_null($clientId)) { if ($clientId === null) {
throw OAuthServerException::invalidRequest('client_id'); throw OAuthServerException::invalidRequest('client_id');
} }
@ -252,12 +257,12 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
if ($redirectUri !== null) { if ($redirectUri !== null) {
$this->validateRedirectUri($redirectUri, $client, $request); $this->validateRedirectUri($redirectUri, $client, $request);
} elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1 } elseif (empty($client->getRedirectUri()) ||
|| empty($client->getRedirectUri())) { (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request); throw OAuthServerException::invalidClient($request);
} else { } else {
$redirectUri = is_array($client->getRedirectUri()) $redirectUri = \is_array($client->getRedirectUri())
? $client->getRedirectUri()[0] ? $client->getRedirectUri()[0]
: $client->getRedirectUri(); : $client->getRedirectUri();
} }
@ -321,14 +326,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{ {
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { 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) $finalRedirectUri = $authorizationRequest->getRedirectUri()
? is_array($authorizationRequest->getClient()->getRedirectUri()) ?? $this->getClientRedirectUri($authorizationRequest);
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri()
: $authorizationRequest->getRedirectUri();
// The user approved the client, redirect them back with an auth code // The user approved the client, redirect them back with an auth code
if ($authorizationRequest->isAuthorizationApproved() === true) { 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();
}
} }

View File

@ -17,6 +17,7 @@ use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class ImplicitGrant extends AbstractAuthorizeGrant class ImplicitGrant extends AbstractAuthorizeGrant
@ -44,21 +45,21 @@ class ImplicitGrant extends AbstractAuthorizeGrant
/** /**
* @param DateInterval $refreshTokenTTL * @param DateInterval $refreshTokenTTL
* *
* @throw \LogicException * @throw LogicException
*/ */
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL) 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 * @param RefreshTokenRepositoryInterface $refreshTokenRepository
* *
* @throw \LogicException * @throw LogicException
*/ */
public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) 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, ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL 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 $redirectUri
); );
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$scopes,
$this->getIdentifier(),
$client
);
$stateParameter = $this->getQueryStringParameter('state', $request); $stateParameter = $this->getQueryStringParameter('state', $request);
$authorizationRequest = new AuthorizationRequest(); $authorizationRequest = new AuthorizationRequest();
@ -162,7 +156,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$authorizationRequest->setState($stateParameter); $authorizationRequest->setState($stateParameter);
} }
$authorizationRequest->setScopes($finalizedScopes); $authorizationRequest->setScopes($scopes);
return $authorizationRequest; return $authorizationRequest;
} }
@ -173,7 +167,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{ {
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { 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) $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
@ -184,11 +178,19 @@ class ImplicitGrant extends AbstractAuthorizeGrant
// The user approved the client, redirect them back with an access token // The user approved the client, redirect them back with an access token
if ($authorizationRequest->isAuthorizationApproved() === true) { if ($authorizationRequest->isAuthorizationApproved() === true) {
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$authorizationRequest->getScopes(),
$this->getIdentifier(),
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier()
);
$accessToken = $this->issueAccessToken( $accessToken = $this->issueAccessToken(
$this->accessTokenTTL, $this->accessTokenTTL,
$authorizationRequest->getClient(), $authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier(), $authorizationRequest->getUser()->getIdentifier(),
$authorizationRequest->getScopes() $finalizedScopes
); );
$response = new RedirectResponse(); $response = new RedirectResponse();

View File

@ -56,17 +56,18 @@ class PasswordGrant extends AbstractGrant
// Finalize the requested scopes // Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); $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); $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); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken); $responseType->setRefreshToken($refreshToken);
}
return $responseType; return $responseType;
} }

View File

@ -12,6 +12,7 @@
namespace League\OAuth2\Server\Grant; namespace League\OAuth2\Server\Grant;
use DateInterval; use DateInterval;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestEvent;
@ -62,17 +63,18 @@ class RefreshTokenGrant extends AbstractGrant
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_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); $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); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken); $responseType->setRefreshToken($refreshToken);
}
return $responseType; return $responseType;
} }
@ -95,8 +97,8 @@ class RefreshTokenGrant extends AbstractGrant
// Validate refresh token // Validate refresh token
try { try {
$refreshToken = $this->decrypt($encryptedRefreshToken); $refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (\Exception $e) { } catch (Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token'); throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
} }
$refreshTokenData = json_decode($refreshToken, true); $refreshTokenData = json_decode($refreshToken, true);

View File

@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Middleware; namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -43,7 +44,7 @@ class AuthorizationServerMiddleware
} catch (OAuthServerException $exception) { } catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response); return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (\Exception $exception) { } catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response); ->generateHttpResponse($response);
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd

View File

@ -9,6 +9,7 @@
namespace League\OAuth2\Server\Middleware; namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer; use League\OAuth2\Server\ResourceServer;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -34,7 +35,7 @@ class ResourceServerMiddleware
* @param ResponseInterface $response * @param ResponseInterface $response
* @param callable $next * @param callable $next
* *
* @return \Psr\Http\Message\ResponseInterface * @return ResponseInterface
*/ */
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{ {
@ -43,7 +44,7 @@ class ResourceServerMiddleware
} catch (OAuthServerException $exception) { } catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response); return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
} catch (\Exception $exception) { } catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response); ->generateHttpResponse($response);
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd

View File

@ -20,7 +20,7 @@ interface RefreshTokenRepositoryInterface extends RepositoryInterface
/** /**
* Creates a new refresh token * Creates a new refresh token
* *
* @return RefreshTokenEntityInterface * @return RefreshTokenEntityInterface|null
*/ */
public function getNewRefreshToken(); public function getNewRefreshToken();

View File

@ -54,7 +54,7 @@ abstract class AbstractResponseType implements ResponseTypeInterface
/** /**
* Set the private key * Set the private key
* *
* @param \League\OAuth2\Server\CryptKey $key * @param CryptKey $key
*/ */
public function setPrivateKey(CryptKey $key) public function setPrivateKey(CryptKey $key)
{ {

View File

@ -4,6 +4,7 @@ namespace LeagueTests;
use DateInterval; use DateInterval;
use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\ClientCredentialsGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant;
@ -110,6 +111,95 @@ class AuthorizationServerTest extends TestCase
$this->assertInstanceOf(BearerTokenResponse::class, $method->invoke($server)); $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() public function testCompleteAuthorizationRequest()
{ {
$clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();

View File

@ -2,6 +2,7 @@
namespace LeagueTests\Exception; namespace LeagueTests\Exception;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant; use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
@ -78,4 +79,19 @@ class OAuthServerExceptionTest extends TestCase
$this->assertFalse($exceptionWithoutRedirect->hasRedirect()); $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());
}
} }

View File

@ -340,6 +340,27 @@ class AbstractGrantTest extends TestCase
$this->assertEquals($accessToken, $refreshToken->getAccessToken()); $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() public function testIssueAccessToken()
{ {
$accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();

View File

@ -740,6 +740,74 @@ class AuthCodeGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); $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() public function testRespondToAccessTokenRequestCodeChallengePlain()
{ {
$client = new ClientEntity(); $client = new ClientEntity();

View File

@ -95,7 +95,6 @@ class ImplicitGrantTest extends TestCase
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeEntity = new ScopeEntity(); $scopeEntity = new ScopeEntity();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new DateInterval('PT10M')); $grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock); $grant->setClientRepository($clientRepositoryMock);
@ -130,7 +129,6 @@ class ImplicitGrantTest extends TestCase
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeEntity = new ScopeEntity(); $scopeEntity = new ScopeEntity();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$grant = new ImplicitGrant(new DateInterval('PT10M')); $grant = new ImplicitGrant(new DateInterval('PT10M'));
$grant->setClientRepository($clientRepositoryMock); $grant->setClientRepository($clientRepositoryMock);
@ -293,9 +291,13 @@ class ImplicitGrantTest extends TestCase
$accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $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->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); $this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest));
} }
@ -316,9 +318,13 @@ class ImplicitGrantTest extends TestCase
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $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->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->completeAuthorizationRequest($authRequest); $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(0))->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
$accessTokenRepositoryMock->expects($this->at(1))->method('persistNewAccessToken')->willReturnSelf(); $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->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); $this->assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest));
} }
@ -367,9 +377,13 @@ class ImplicitGrantTest extends TestCase
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(OAuthServerException::serverError('something bad happened')); $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->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->completeAuthorizationRequest($authRequest); $grant->completeAuthorizationRequest($authRequest);
} }
@ -391,9 +405,13 @@ class ImplicitGrantTest extends TestCase
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); $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->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));
$grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->completeAuthorizationRequest($authRequest); $grant->completeAuthorizationRequest($authRequest);
} }

View File

@ -81,6 +81,52 @@ class PasswordGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $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 * @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/ */

View File

@ -95,6 +95,63 @@ class RefreshTokenGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $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() public function testRespondToReducedScopes()
{ {
$client = new ClientEntity(); $client = new ClientEntity();

View File

@ -105,7 +105,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
$response = $exception->generateHttpResponse(new Response()); $response = $exception->generateHttpResponse(new Response());
$this->assertEquals(302, $response->getStatusCode()); $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]); $response->getHeader('location')[0]);
} }
@ -115,7 +115,7 @@ class AuthorizationServerMiddlewareTest extends TestCase
$response = $exception->generateHttpResponse(new Response(), true); $response = $exception->generateHttpResponse(new Response(), true);
$this->assertEquals(302, $response->getStatusCode()); $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]); $response->getHeader('location')[0]);
} }
} }