diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php index faef53585..7723eda81 100644 --- a/src/AuthorizationValidators/BearerTokenValidator.php +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -14,8 +14,8 @@ use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; -use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; +use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\Constraint\ValidAt; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use League\OAuth2\Server\CryptKey; @@ -43,12 +43,19 @@ class BearerTokenValidator implements AuthorizationValidatorInterface */ private $jwtConfiguration; + /** + * @var \DateInterval|null + */ + private $jwtValidAtDateLeeway; + /** * @param AccessTokenRepositoryInterface $accessTokenRepository + * @param \DateInterval|null $jwtValidAtDateLeeway */ - public function __construct(AccessTokenRepositoryInterface $accessTokenRepository) + public function __construct(AccessTokenRepositoryInterface $accessTokenRepository, \DateInterval $jwtValidAtDateLeeway = null) { $this->accessTokenRepository = $accessTokenRepository; + $this->jwtValidAtDateLeeway = $jwtValidAtDateLeeway; } /** @@ -73,10 +80,11 @@ private function initJwtConfiguration() InMemory::plainText('empty', 'empty') ); + $clock = new SystemClock(new DateTimeZone(\date_default_timezone_get())); $this->jwtConfiguration->setValidationConstraints( \class_exists(LooseValidAt::class) - ? new LooseValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))) - : new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + ? new LooseValidAt($clock, $this->jwtValidAtDateLeeway) + : new ValidAt($clock, $this->jwtValidAtDateLeeway), new SignedWith( new Sha256(), InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') diff --git a/tests/AuthorizationValidators/BearerTokenValidatorTest.php b/tests/AuthorizationValidators/BearerTokenValidatorTest.php index 838d2bbae..536618b9a 100644 --- a/tests/AuthorizationValidators/BearerTokenValidatorTest.php +++ b/tests/AuthorizationValidators/BearerTokenValidatorTest.php @@ -71,4 +71,67 @@ public function testBearerTokenValidatorRejectsExpiredToken() $bearerTokenValidator->validateAuthorization($request); } + + public function testBearerTokenValidatorAcceptsExpiredTokenWithinLeeway() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + // We fake generating this token 10 seconds into the future, an extreme example of possible time drift between servers + $future = (new DateTimeImmutable())->add(new DateInterval('PT10S')); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new \DateInterval('PT10S')); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $jwtTokenFromFutureWithinLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt($future) + ->canOnlyBeUsedAfter($future) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $jwtTokenFromFutureWithinLeeway->toString())); + + $validRequest = $bearerTokenValidator->validateAuthorization($request); + + $this->assertArrayHasKey('authorization', $validRequest->getHeaders()); + } + + public function testBearerTokenValidatorRejectsExpiredTokenBeyondLeeway() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + // We fake generating this token 10 seconds into the future, an extreme example of possible time drift between servers + $future = (new DateTimeImmutable())->add(new DateInterval('PT20S')); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new \DateInterval('PT10S')); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $jwtTokenFromFutureBeyondLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt($future) + ->canOnlyBeUsedAfter($future) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $jwtTokenFromFutureBeyondLeeway->toString())); + + $this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class); + $this->expectExceptionCode(9); + + $bearerTokenValidator->validateAuthorization($request); + } }