diff --git a/.github/workflows/backwards-compatibility.yml b/.github/workflows/backwards-compatibility.yml
new file mode 100644
index 000000000..e15e132b5
--- /dev/null
+++ b/.github/workflows/backwards-compatibility.yml
@@ -0,0 +1,21 @@
+name: "Backwards compatibility check"
+
+on:
+ pull_request:
+
+jobs:
+ bc-check:
+ name: "Backwards compatibility check"
+
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+ with:
+ fetch-depth: 0
+
+ - name: "Backwards Compatibility Check"
+ uses: docker://nyholm/roave-bc-check-ga
+ with:
+ args: --from=${{ github.event.pull_request.base.sha }}
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e9f2343bb..14df99517 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
- fail-fast: true
+ fail-fast: false
matrix:
- php: [7.2, 7.3, 7.4]
+ php: [7.3, 7.4, 8.0]
stability: [prefer-lowest, prefer-stable]
name: PHP ${{ matrix.php }} - ${{ matrix.stability }}
@@ -32,9 +32,6 @@ jobs:
- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
- - name: Run PHPStan
- run: vendor/bin/phpstan analyse -l 7 -c phpstan.neon src tests
-
- name: Execute tests
run: vendor/bin/phpunit --verbose --coverage-clover=coverage.clover
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbd2cbb69..f49eba4b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [8.2.0] - released 2020-11-25
### Added
- Add a `getRedirectUri` function to the `OAuthServerException` class (PR #1123)
+- Support for PHP 8.0 (PR #1146)
+
+### Removed
+- Removed support for PHP 7.2 (PR #1146)
### Fixed
- Fix typo in parameter hint. `code_challenged` changed to `code_challenge`. Thrown by Auth Code Grant when the code challenge does not match the regex. (PR #1130)
@@ -505,7 +511,8 @@ Version 5 is a complete code rewrite.
- First major release
-[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.1.1...HEAD
+[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.2.0...HEAD
+[8.2.0]: https://github.com/thephpleague/oauth2-server/compare/8.1.1...8.2.0
[8.1.1]: https://github.com/thephpleague/oauth2-server/compare/8.1.0...8.1.1
[8.1.0]: https://github.com/thephpleague/oauth2-server/compare/8.0.0...8.1.0
[8.0.0]: https://github.com/thephpleague/oauth2-server/compare/7.4.0...8.0.0
diff --git a/README.md b/README.md
index c76cf45ef..0c8869c52 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,6 @@
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server)
[![Total Downloads](https://img.shields.io/packagist/dt/league/oauth2-server.svg?style=flat-square)](https://packagist.org/packages/league/oauth2-server)
-[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat-square)](https://github.com/phpstan/phpstan)
`league/oauth2-server` is a standards compliant implementation of an [OAuth 2.0](https://tools.ietf.org/html/rfc6749) authorization server written in PHP which makes working with OAuth 2.0 trivial. You can easily configure an OAuth 2.0 server to protect your API with access tokens, or allow clients to request new access tokens and refresh them.
@@ -29,11 +28,11 @@ This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](ht
## Requirements
-The following versions of PHP are supported:
+The latest version of this package supports the following versions of PHP:
-* PHP 7.2
* PHP 7.3
* PHP 7.4
+* PHP 8.0
The `openssl` and `json` extensions are also required.
@@ -52,11 +51,10 @@ You can contribute to the documentation in the [gh-pages branch](https://github.
## Testing
-The library uses [PHPUnit](https://phpunit.de/) for unit tests and [PHPStan](https://github.com/phpstan/phpstan) for static analysis of the code.
+The library uses [PHPUnit](https://phpunit.de/) for unit tests.
```
vendor/bin/phpunit
-vendor/bin/phpstan analyse -l 7 -c phpstan.neon src tests
```
## Continuous Integration
diff --git a/composer.json b/composer.json
index 658cc6553..e0617eb66 100644
--- a/composer.json
+++ b/composer.json
@@ -4,19 +4,19 @@
"homepage": "https://oauth2.thephpleague.com/",
"license": "MIT",
"require": {
- "php": ">=7.2.0",
+ "php": "^7.3 || ^8.0",
"ext-openssl": "*",
"league/event": "^2.2",
- "lcobucci/jwt": "^3.3.1",
+ "lcobucci/jwt": "^3.4 || ^4.0",
"psr/http-message": "^1.0.1",
"defuse/php-encryption": "^2.2.1",
"ext-json": "*"
},
"require-dev": {
- "phpunit/phpunit": "^8.5.4 || ^9.1.3",
- "laminas/laminas-diactoros": "^2.3.0",
- "phpstan/phpstan": "^0.11.19",
- "phpstan/phpstan-phpunit": "^0.11.2",
+ "phpunit/phpunit": "^9.4.3",
+ "laminas/laminas-diactoros": "^2.5.0",
+ "phpstan/phpstan": "^0.12.57",
+ "phpstan/phpstan-phpunit": "^0.12.16",
"roave/security-advisories": "dev-master"
},
"repositories": [
diff --git a/examples/composer.json b/examples/composer.json
index e1c44efc8..d265472e6 100644
--- a/examples/composer.json
+++ b/examples/composer.json
@@ -1,13 +1,13 @@
{
"require": {
- "slim/slim": "^3.0.0"
+ "slim/slim": "^3.12.3"
},
"require-dev": {
"league/event": "^2.2",
- "lcobucci/jwt": "^3.3",
- "psr/http-message": "^1.0",
- "defuse/php-encryption": "^2.2",
- "laminas/laminas-diactoros": "^2.1.2"
+ "lcobucci/jwt": "^3.4 || ^4.0",
+ "psr/http-message": "^1.0.1",
+ "defuse/php-encryption": "^2.2.1",
+ "laminas/laminas-diactoros": "^2.5.0"
},
"autoload": {
"psr-4": {
diff --git a/examples/composer.lock b/examples/composer.lock
index 6de13b69f..f7affe0e9 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": "5bcbefe6cdff10a268399b1d138647ea",
+ "content-hash": "1f38bc4bb33ddc5527b3097d1118b227",
"packages": [
{
"name": "nikic/fast-route",
@@ -54,29 +54,29 @@
},
{
"name": "pimple/pimple",
- "version": "v3.2.3",
+ "version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/silexphp/Pimple.git",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32"
+ "reference": "21e45061c3429b1e06233475cc0e1f6fc774d5b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32",
- "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32",
+ "url": "https://api.github.com/repos/silexphp/Pimple/zipball/21e45061c3429b1e06233475cc0e1f6fc774d5b0",
+ "reference": "21e45061c3429b1e06233475cc0e1f6fc774d5b0",
"shasum": ""
},
"require": {
- "php": ">=5.3.0",
+ "php": ">=7.2.5",
"psr/container": "^1.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "^3.2"
+ "symfony/phpunit-bridge": "^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.2.x-dev"
+ "dev-master": "3.3.x-dev"
}
},
"autoload": {
@@ -95,12 +95,12 @@
}
],
"description": "Pimple, a simple Dependency Injection Container",
- "homepage": "http://pimple.sensiolabs.org",
+ "homepage": "https://pimple.symfony.com",
"keywords": [
"container",
"dependency injection"
],
- "time": "2018-01-21T07:42:36+00:00"
+ "time": "2020-11-24T20:35:42+00:00"
},
{
"name": "psr/container",
@@ -341,46 +341,50 @@
},
{
"name": "laminas/laminas-diactoros",
- "version": "2.2.1",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-diactoros.git",
- "reference": "064f0b20e832bb232d0311f915c7422fef1b1857"
+ "reference": "4ff7400c1c12e404144992ef43c8b733fd9ad516"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/064f0b20e832bb232d0311f915c7422fef1b1857",
- "reference": "064f0b20e832bb232d0311f915c7422fef1b1857",
+ "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/4ff7400c1c12e404144992ef43c8b733fd9ad516",
+ "reference": "4ff7400c1c12e404144992ef43c8b733fd9ad516",
"shasum": ""
},
"require": {
"laminas/laminas-zendframework-bridge": "^1.0",
- "php": "^7.1",
+ "php": "^7.3 || ~8.0.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0"
},
+ "conflict": {
+ "phpspec/prophecy": "<1.9.0"
+ },
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"replace": {
- "zendframework/zend-diactoros": "self.version"
+ "zendframework/zend-diactoros": "^2.2.1"
},
"require-dev": {
"ext-curl": "*",
"ext-dom": "*",
+ "ext-gd": "*",
"ext-libxml": "*",
- "http-interop/http-factory-tests": "^0.5.0",
+ "http-interop/http-factory-tests": "^0.8.0",
"laminas/laminas-coding-standard": "~1.0.0",
- "php-http/psr7-integration-tests": "dev-master",
- "phpunit/phpunit": "^7.0.2"
+ "php-http/psr7-integration-tests": "^1.1",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.1"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "2.1.x-dev",
- "dev-develop": "2.2.x-dev",
- "dev-release-1.8": "1.8.x-dev"
+ "laminas": {
+ "config-provider": "Laminas\\Diactoros\\ConfigProvider",
+ "module": "Laminas\\Diactoros"
}
},
"autoload": {
@@ -416,37 +420,34 @@
"http",
"laminas",
"psr",
+ "psr-17",
"psr-7"
],
- "time": "2019-12-31T16:41:56+00:00"
+ "time": "2020-11-18T18:39:28+00:00"
},
{
"name": "laminas/laminas-zendframework-bridge",
- "version": "1.0.0",
+ "version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-zendframework-bridge.git",
- "reference": "32d7095e436a31b8d98e485a5c63d70df74915a8"
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/32d7095e436a31b8d98e485a5c63d70df74915a8",
- "reference": "32d7095e436a31b8d98e485a5c63d70df74915a8",
+ "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1",
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev",
- "dev-develop": "1.1.x-dev"
- },
"laminas": {
"module": "Laminas\\ZendFrameworkBridge"
}
@@ -470,38 +471,92 @@
"laminas",
"zf"
],
- "time": "2019-12-31T15:24:03+00:00"
+ "time": "2020-09-14T14:23:00+00:00"
+ },
+ {
+ "name": "lcobucci/clock",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lcobucci/clock.git",
+ "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+ "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "infection/infection": "^0.17",
+ "lcobucci/coding-standard": "^6.0",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-deprecation-rules": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpstan/phpstan-strict-rules": "^0.12",
+ "phpunit/php-code-coverage": "9.1.4",
+ "phpunit/phpunit": "9.3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Lcobucci\\Clock\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Luís Cobucci",
+ "email": "lcobucci@gmail.com"
+ }
+ ],
+ "description": "Yet another clock abstraction",
+ "time": "2020-08-27T18:56:02+00:00"
},
{
"name": "lcobucci/jwt",
- "version": "3.3.1",
+ "version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
- "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
+ "reference": "6d8665ccd924dc076a9b65d1ea8abe21d68f6958"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
- "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/6d8665ccd924dc076a9b65d1ea8abe21d68f6958",
+ "reference": "6d8665ccd924dc076a9b65d1ea8abe21d68f6958",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-openssl": "*",
- "php": "^5.6 || ^7.0"
+ "lcobucci/clock": "^2.0",
+ "php": "^7.4 || ^8.0"
},
"require-dev": {
- "mikey179/vfsstream": "~1.5",
- "phpmd/phpmd": "~2.2",
- "phpunit/php-invoker": "~1.1",
- "phpunit/phpunit": "^5.7 || ^7.3",
- "squizlabs/php_codesniffer": "~2.3"
+ "infection/infection": "^0.20",
+ "lcobucci/coding-standard": "^6.0",
+ "mikey179/vfsstream": "^1.6",
+ "phpbench/phpbench": "^0.17",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-deprecation-rules": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpstan/phpstan-strict-rules": "^0.12",
+ "phpunit/php-invoker": "^3.1",
+ "phpunit/phpunit": "^9.4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -515,7 +570,7 @@
],
"authors": [
{
- "name": "Luís Otávio Cobucci Oblonczyk",
+ "name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
@@ -525,7 +580,7 @@
"JWS",
"jwt"
],
- "time": "2019-05-24T18:30:49+00:00"
+ "time": "2020-11-25T02:06:12+00:00"
},
{
"name": "league/event",
@@ -579,20 +634,20 @@
},
{
"name": "paragonie/random_compat",
- "version": "v9.99.99",
+ "version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
- "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
- "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
- "php": "^7"
+ "php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
@@ -620,7 +675,7 @@
"pseudorandom",
"random"
],
- "time": "2018-07-02T15:55:56+00:00"
+ "time": "2020-10-15T08:29:30+00:00"
},
{
"name": "psr/http-factory",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 92564086b..a3e34084d 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,19 +6,9 @@
./tests/
-
-
+
+
src
-
- src/ResponseTypes/DefaultTemplates
- src/TemplateRenderer
-
-
-
-
-
-
-
+
+
diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php
index 213ecf893..9a279de4f 100644
--- a/src/AuthorizationValidators/BearerTokenValidator.php
+++ b/src/AuthorizationValidators/BearerTokenValidator.php
@@ -9,17 +9,23 @@
namespace League\OAuth2\Server\AuthorizationValidators;
-use BadMethodCallException;
-use InvalidArgumentException;
-use Lcobucci\JWT\Parser;
+use DateTimeZone;
+use Lcobucci\Clock\SystemClock;
+use Lcobucci\JWT\Configuration;
+use Lcobucci\JWT\Encoding\CannotDecodeContent;
+use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Rsa\Sha256;
-use Lcobucci\JWT\ValidationData;
+use Lcobucci\JWT\Token\InvalidTokenStructure;
+use Lcobucci\JWT\Token\UnsupportedHeaderFound;
+use Lcobucci\JWT\Validation\Constraint\SignedWith;
+use Lcobucci\JWT\Validation\Constraint\ValidAt;
+use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use League\OAuth2\Server\CryptKey;
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
{
@@ -35,6 +41,11 @@ class BearerTokenValidator implements AuthorizationValidatorInterface
*/
protected $publicKey;
+ /**
+ * @var Configuration
+ */
+ private $jwtConfiguration;
+
/**
* @param AccessTokenRepositoryInterface $accessTokenRepository
*/
@@ -51,6 +62,24 @@ public function __construct(AccessTokenRepositoryInterface $accessTokenRepositor
public function setPublicKey(CryptKey $key)
{
$this->publicKey = $key;
+
+ $this->initJwtConfiguration();
+ }
+
+ /**
+ * Initialise the JWT configuration.
+ */
+ private function initJwtConfiguration()
+ {
+ $this->jwtConfiguration = Configuration::forSymmetricSigner(
+ new Sha256(),
+ InMemory::plainText('')
+ );
+
+ $this->jwtConfiguration->setValidationConstraints(
+ new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
+ new SignedWith(new Sha256(), LocalFileReference::file($this->publicKey->getKeyPath()))
+ );
}
/**
@@ -67,40 +96,43 @@ public function validateAuthorization(ServerRequestInterface $request)
try {
// Attempt to parse and validate the JWT
- $token = (new Parser())->parse($jwt);
- try {
- 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', null, $exception);
- }
+ $token = $this->jwtConfiguration->parser()->parse($jwt);
- // Ensure access token hasn't expired
- $data = new ValidationData();
- $data->setCurrentTime(\time());
+ $constraints = $this->jwtConfiguration->validationConstraints();
- if ($token->validate($data) === false) {
- throw OAuthServerException::accessDenied('Access token is invalid');
+ try {
+ $this->jwtConfiguration->validator()->assert($token, ...$constraints);
+ } catch (RequiredConstraintsViolated $exception) {
+ throw OAuthServerException::accessDenied('Access token could not be verified');
}
- } catch (InvalidArgumentException $exception) {
- // JWT couldn't be parsed so return the request as is
+ } catch (CannotDecodeContent | InvalidTokenStructure | UnsupportedHeaderFound $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
- } catch (RuntimeException $exception) {
- // JWT couldn't be parsed so return the request as is
- throw OAuthServerException::accessDenied('Error while decoding to JSON', null, $exception);
}
+ $claims = $token->claims();
+
// Check if token has been revoked
- if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
+ if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}
// Return the request with additional attributes
return $request
- ->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
- ->withAttribute('oauth_client_id', $token->getClaim('aud'))
- ->withAttribute('oauth_user_id', $token->getClaim('sub'))
- ->withAttribute('oauth_scopes', $token->getClaim('scopes'));
+ ->withAttribute('oauth_access_token_id', $claims->get('jti'))
+ ->withAttribute('oauth_client_id', $this->convertSingleRecordAudToString($claims->get('aud')))
+ ->withAttribute('oauth_user_id', $claims->get('sub'))
+ ->withAttribute('oauth_scopes', $claims->get('scopes'));
+ }
+
+ /**
+ * Convert single record arrays into strings to ensure backwards compatibility between v4 and v3.x of lcobucci/jwt
+ *
+ * @param mixed $aud
+ *
+ * @return array|string
+ */
+ private function convertSingleRecordAudToString($aud)
+ {
+ return \is_countable($aud) && \count($aud) === 1 ? $aud[0] : $aud;
}
}
diff --git a/src/Entities/Traits/AccessTokenTrait.php b/src/Entities/Traits/AccessTokenTrait.php
index 48e3d1ac4..26007188f 100644
--- a/src/Entities/Traits/AccessTokenTrait.php
+++ b/src/Entities/Traits/AccessTokenTrait.php
@@ -10,8 +10,9 @@
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
-use Lcobucci\JWT\Builder;
-use Lcobucci\JWT\Signer\Key;
+use Lcobucci\JWT\Configuration;
+use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
@@ -25,6 +26,11 @@ trait AccessTokenTrait
*/
private $privateKey;
+ /**
+ * @var Configuration
+ */
+ private $jwtConfiguration;
+
/**
* Set the private key used to encrypt this access token.
*/
@@ -33,24 +39,40 @@ public function setPrivateKey(CryptKey $privateKey)
$this->privateKey = $privateKey;
}
+ /**
+ * Initialise the JWT Configuration.
+ */
+ public function initJwtConfiguration()
+ {
+ $privateKeyPassPhrase = $this->privateKey->getPassPhrase();
+
+ $verificationKey = empty($privateKeyPassPhrase) ? InMemory::plainText('') : $privateKeyPassPhrase;
+
+ $this->jwtConfiguration = Configuration::forAsymmetricSigner(
+ new Sha256(),
+ LocalFileReference::file($this->privateKey->getKeyPath()),
+ $verificationKey
+ );
+ }
+
/**
* Generate a JWT from the access token
*
- * @param CryptKey $privateKey
- *
* @return Token
*/
- private function convertToJWT(CryptKey $privateKey)
+ private function convertToJWT()
{
- return (new Builder())
+ $this->initJwtConfiguration();
+
+ return $this->jwtConfiguration->builder()
->permittedFor($this->getClient()->getIdentifier())
->identifiedBy($this->getIdentifier())
- ->issuedAt(\time())
- ->canOnlyBeUsedAfter(\time())
- ->expiresAt($this->getExpiryDateTime()->getTimestamp())
+ ->issuedAt(new DateTimeImmutable())
+ ->canOnlyBeUsedAfter(new DateTimeImmutable())
+ ->expiresAt($this->getExpiryDateTime())
->relatedTo((string) $this->getUserIdentifier())
->withClaim('scopes', $this->getScopes())
- ->getToken(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()));
+ ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
}
/**
@@ -58,7 +80,7 @@ private function convertToJWT(CryptKey $privateKey)
*/
public function __toString()
{
- return (string) $this->convertToJWT($this->privateKey);
+ return $this->convertToJWT()->toString();
}
/**
diff --git a/tests/AuthorizationValidators/BearerTokenValidatorTest.php b/tests/AuthorizationValidators/BearerTokenValidatorTest.php
index c95c60531..705368e4c 100644
--- a/tests/AuthorizationValidators/BearerTokenValidatorTest.php
+++ b/tests/AuthorizationValidators/BearerTokenValidatorTest.php
@@ -2,33 +2,69 @@
namespace LeagueTests\AuthorizationValidators;
+use DateInterval;
+use DateTimeImmutable;
use Laminas\Diactoros\ServerRequest;
-use Lcobucci\JWT\Builder;
+use Lcobucci\JWT\Signer\Key\LocalFileReference;
+use Lcobucci\JWT\Signer\Rsa\Sha256;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use PHPUnit\Framework\TestCase;
+use ReflectionClass;
class BearerTokenValidatorTest extends TestCase
{
- public function testThrowExceptionWhenAccessTokenIsNotSigned()
+ public function testBearerTokenValidatorAcceptsValidToken()
{
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock);
$bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
- $unsignedJwt = (new Builder())
+ $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class);
+ $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration');
+ $jwtConfiguration->setAccessible(true);
+
+ $validJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder()
+ ->permittedFor('client-id')
+ ->identifiedBy('token-id')
+ ->issuedAt(new DateTimeImmutable())
+ ->canOnlyBeUsedAfter(new DateTimeImmutable())
+ ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H')))
+ ->relatedTo('user-id')
+ ->withClaim('scopes', 'scope1 scope2 scope3 scope4')
+ ->getToken(new Sha256(), LocalFileReference::file(__DIR__ . '/../Stubs/private.key'));
+
+ $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $validJwt->toString()));
+
+ $response = $bearerTokenValidator->validateAuthorization($request);
+
+ $this->assertArrayHasKey('authorization', $response->getHeaders());
+ }
+
+ public function testBearerTokenValidatorRejectsExpiredToken()
+ {
+ $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
+
+ $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock);
+ $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key'));
+
+ $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class);
+ $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration');
+ $jwtConfiguration->setAccessible(true);
+
+ $expiredJwt = $jwtConfiguration->getValue($bearerTokenValidator)->builder()
->permittedFor('client-id')
- ->identifiedBy('token-id', true)
- ->issuedAt(\time())
- ->canOnlyBeUsedAfter(\time())
- ->expiresAt(\time())
+ ->identifiedBy('token-id')
+ ->issuedAt(new DateTimeImmutable())
+ ->canOnlyBeUsedAfter(new DateTimeImmutable())
+ ->expiresAt((new DateTimeImmutable())->sub(new DateInterval('PT1H')))
->relatedTo('user-id')
->withClaim('scopes', 'scope1 scope2 scope3 scope4')
- ->getToken();
+ ->getToken(new Sha256(), LocalFileReference::file(__DIR__ . '/../Stubs/private.key'));
- $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $unsignedJwt));
+ $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $expiredJwt->toString()));
$this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class);
$this->expectExceptionCode(9);
diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php
index 28527aecf..a080da317 100644
--- a/tests/Grant/AuthCodeGrantTest.php
+++ b/tests/Grant/AuthCodeGrantTest.php
@@ -1715,8 +1715,16 @@ public function testAuthCodeRepositoryUniqueConstraintCheck()
$authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock();
$authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity());
- $authCodeRepository->expects($this->at(0))->method('persistNewAuthCode')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
- $authCodeRepository->expects($this->at(1))->method('persistNewAuthCode');
+ $matcher = $this->exactly(2);
+
+ $authCodeRepository
+ ->expects($matcher)
+ ->method('persistNewAuthCode')
+ ->willReturnCallback(function () use ($matcher) {
+ if ($matcher->getInvocationCount() === 1) {
+ throw UniqueTokenIdentifierConstraintViolationException::create();
+ }
+ });
$grant = new AuthCodeGrant(
$authCodeRepository,
@@ -1797,8 +1805,17 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck()
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity());
- $refreshTokenRepositoryMock->expects($this->at(0))->method('persistNewRefreshToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
- $refreshTokenRepositoryMock->expects($this->at(1))->method('persistNewRefreshToken');
+
+ $matcher = $this->exactly(2);
+
+ $refreshTokenRepositoryMock
+ ->expects($matcher)
+ ->method('persistNewRefreshToken')
+ ->willReturnCallback(function () use ($matcher) {
+ if ($matcher->getInvocationCount() === 1) {
+ throw UniqueTokenIdentifierConstraintViolationException::create();
+ }
+ });
$grant = new AuthCodeGrant(
$this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
diff --git a/tests/Grant/ImplicitGrantTest.php b/tests/Grant/ImplicitGrantTest.php
index 99fa3f57c..546450384 100644
--- a/tests/Grant/ImplicitGrantTest.php
+++ b/tests/Grant/ImplicitGrantTest.php
@@ -276,8 +276,17 @@ public function testAccessTokenRepositoryUniqueConstraintCheck()
/** @var AccessTokenRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject $accessTokenRepositoryMock */
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
- $accessTokenRepositoryMock->expects($this->at(0))->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create());
- $accessTokenRepositoryMock->expects($this->at(1))->method('persistNewAccessToken')->willReturnSelf();
+
+ $matcher = $this->exactly(2);
+
+ $accessTokenRepositoryMock
+ ->expects($matcher)
+ ->method('persistNewAccessToken')
+ ->willReturnCallback(function () use ($matcher) {
+ if ($matcher->getInvocationCount() === 1) {
+ throw UniqueTokenIdentifierConstraintViolationException::create();
+ }
+ });
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
diff --git a/tests/ResponseTypes/BearerResponseTypeTest.php b/tests/ResponseTypes/BearerResponseTypeTest.php
index 6dc24ff3b..a57820d00 100644
--- a/tests/ResponseTypes/BearerResponseTypeTest.php
+++ b/tests/ResponseTypes/BearerResponseTypeTest.php
@@ -281,7 +281,7 @@ public function testDetermineMissingBearerInHeader()
$authorizationValidator->validateAuthorization($request);
} catch (OAuthServerException $e) {
$this->assertEquals(
- 'Error while decoding to JSON',
+ 'Error while decoding from JSON',
$e->getHint()
);
}