forked from lexik/LexikJWTAuthenticationBundle
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Based on lexik#1005
- Loading branch information
Showing
21 changed files
with
509 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener; | ||
|
||
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; | ||
|
||
class AddClaimsToJWTListener | ||
{ | ||
public function __invoke(JWTCreatedEvent $event): void | ||
{ | ||
$data = $event->getData(); | ||
|
||
if (!isset($data['jti'])) { | ||
$data['jti'] = bin2hex(random_bytes(16)); | ||
|
||
$event->setData($data); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener; | ||
|
||
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManager; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\Security\Core\Exception\DisabledException; | ||
use Symfony\Component\Security\Http\Event\LoginFailureEvent; | ||
use Symfony\Component\Security\Http\Event\LogoutEvent; | ||
|
||
class BlockJWTListener | ||
{ | ||
public function __construct( | ||
private BlockedTokenManager $tokenManager, | ||
private TokenExtractorInterface $tokenExtractor, | ||
private JWTTokenManagerInterface $jwtManager, | ||
) { | ||
} | ||
|
||
public function onLoginFailure(LoginFailureEvent $event): void | ||
{ | ||
if ($event->getException() instanceof DisabledException) { | ||
$this->blockTokenFromRequest($event->getRequest()); | ||
} | ||
} | ||
|
||
public function onLogout(LogoutEvent $event): void | ||
{ | ||
$this->blockTokenFromRequest($event->getRequest()); | ||
} | ||
|
||
private function blockTokenFromRequest(Request $request): void | ||
{ | ||
$token = $this->tokenExtractor->extract($request); | ||
|
||
if ($token === false) { | ||
// There's nothing to block if the token isn't in the request | ||
return; | ||
} | ||
|
||
try { | ||
$payload = $this->jwtManager->parse($token); | ||
} catch (JWTDecodeFailureException) { | ||
// Ignore decode failures, this would mean the token is invalid anyway | ||
return; | ||
} | ||
|
||
try { | ||
$this->tokenManager->add($payload); | ||
} catch (MissingClaimException) { | ||
// We can't block a token missing the claims our system requires, so silently ignore this one | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener; | ||
|
||
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManager; | ||
|
||
class RejectBlockedTokenListener | ||
{ | ||
public function __construct(private BlockedTokenManager $tokenManager) | ||
{ | ||
} | ||
|
||
/** | ||
* @throws InvalidTokenException if the JWT is blocked | ||
*/ | ||
public function __invoke(JWTAuthenticatedEvent $event): void | ||
{ | ||
try { | ||
if ($this->tokenManager->has($event->getPayload())) { | ||
throw new InvalidTokenException('JWT blocked'); | ||
} | ||
} catch (MissingClaimException) { | ||
// Do nothing if the required claims do not exist on the payload (older JWTs won't have the "jti" claim the manager requires) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception; | ||
|
||
use Throwable; | ||
|
||
class MissingClaimException extends JWTFailureException | ||
{ | ||
public function __construct( | ||
string $claim, | ||
Throwable $previous = null | ||
) { | ||
parent::__construct('missing_claim', sprintf('Missing required "%s" claim on JWT payload.', $claim), $previous); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?xml version="1.0" ?> | ||
|
||
<container xmlns="http://symfony.com/schema/dic/services" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
|
||
<services> | ||
<service id="lexik_jwt_authentication.event_listener.add_claims_to_jwt_listener" class="Lexik\Bundle\JWTAuthenticationBundle\EventListener\AddClaimsToJWTListener"> | ||
<tag name="kernel.event_listener" event="lexik_jwt_authentication.on_jwt_created" /> | ||
</service> | ||
|
||
<service id="lexik_jwt_authentication.event_listener.block_jwt_listener" class="Lexik\Bundle\JWTAuthenticationBundle\EventListener\BlockJWTListener"> | ||
<argument type="service" id="lexik_jwt_authentication.blocked_token_manager"/> | ||
<argument type="service" id="lexik_jwt_authentication.extractor.chain_extractor"/> | ||
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/> | ||
<tag name="kernel.event_listener" event="Symfony\Component\Security\Http\Event\LoginFailureEvent" method="onLoginFailure" dispatcher="event_dispatcher"/> | ||
<tag name="kernel.event_listener" event="Symfony\Component\Security\Http\Event\LogoutEvent" method="onLogout" dispatcher="event_dispatcher"/> | ||
</service> | ||
|
||
<service id="lexik_jwt_authentication.event_listener.reject_blocked_token_listener" class="Lexik\Bundle\JWTAuthenticationBundle\EventListener\RejectBlockedTokenListener"> | ||
<argument type="service" id="lexik_jwt_authentication.blocked_token_manager"/> | ||
<tag name="kernel.event_listener" event="lexik_jwt_authentication.on_jwt_authenticated"/> | ||
</service> | ||
|
||
<service id="lexik_jwt_authentication.blocked_token_manager" class="Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManager"> | ||
<argument type="service" id="lexik_jwt_authentication.blocklist_token.cache"/> | ||
</service> | ||
|
||
</services> | ||
|
||
</container> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,4 +40,4 @@ public function authenticate(Request $request): PassportInterface | |
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\Services; | ||
|
||
use DateTime; | ||
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException; | ||
use Psr\Cache\CacheItemPoolInterface; | ||
|
||
class BlockedTokenManager | ||
{ | ||
private $cacheJwt; | ||
|
||
public function __construct(CacheItemPoolInterface $cacheJwt) | ||
{ | ||
$this->cacheJwt = $cacheJwt; | ||
} | ||
|
||
/** | ||
* @throws MissingClaimException if required claims do not exist in the payload | ||
*/ | ||
public function add(array $payload): bool | ||
{ | ||
if (!isset($payload['exp'])) { | ||
throw new MissingClaimException('exp'); | ||
} | ||
|
||
$expiration = new DateTime('@' . $payload['exp'], new \DateTimeZone('UTC')); | ||
$now = new DateTime(timezone: new \DateTimeZone('UTC')); | ||
|
||
// If the token is already expired, there's no point in adding it to storage | ||
if ($expiration <= $now) { | ||
return false; | ||
} | ||
|
||
$cacheExpiration = (clone $expiration)->add(new \DateInterval('PT5M')); | ||
|
||
if (!isset($payload['jti'])) { | ||
throw new MissingClaimException('jti'); | ||
} | ||
|
||
$cacheItem = $this->cacheJwt->getItem($payload['jti']); | ||
$cacheItem->set([]); | ||
$cacheItem->expiresAt($cacheExpiration); | ||
$this->cacheJwt->save($cacheItem); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @throws MissingClaimException if required claims do not exist in the payload | ||
*/ | ||
public function has(array $payload): bool | ||
{ | ||
if (!isset($payload['jti'])) { | ||
throw new MissingClaimException('jti'); | ||
} | ||
|
||
return $this->cacheJwt->hasItem($payload['jti']); | ||
} | ||
|
||
/** | ||
* @throws MissingClaimException if required claims do not exist in the payload | ||
*/ | ||
public function remove(array $payload): void | ||
{ | ||
if (!isset($payload['jti'])) { | ||
throw new MissingClaimException('jti'); | ||
} | ||
|
||
$this->cacheJwt->deleteItem($payload['jti']); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<?php | ||
|
||
namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional; | ||
|
||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManager; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
class BlocklistTokenTest extends TestCase | ||
{ | ||
public function testShouldInvalidateTokenOnLogoutWhenBlockListTokenIsEnabled() | ||
{ | ||
static::$client = static::createClient(['test_case' => 'BlockListToken']); | ||
|
||
$token = static::getAuthenticatedToken(); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseIsSuccessful('Pre condition - a valid token should be able to contact the api'); | ||
|
||
static::$client->jsonRequest('GET', '/api/logout', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_FOUND); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED, 'Logout should invalidate token'); | ||
|
||
$responseBody = json_decode(static::$client->getResponse()->getContent(), true); | ||
$this->assertEquals('Invalid JWT Token', $responseBody['message']); | ||
} | ||
|
||
public function testShouldAddJtiWhenBlockListTokenIsEnabled() | ||
{ | ||
static::$client = static::createClient(['test_case' => 'BlockListToken']); | ||
|
||
$token = static::getAuthenticatedToken(); | ||
/** @var JWTManager $jwtManager */ | ||
$jwtManager = static::getContainer()->get('lexik_jwt_authentication.jwt_manager'); | ||
$payload = $jwtManager->parse($token); | ||
self::assertNotEmpty($payload['jti']); | ||
} | ||
|
||
public function testShouldInvalidateTokenOnLogoutWhenBlockListTokenIsEnabledAndWhenUsingCustomLogout() | ||
{ | ||
static::$client = static::createClient(['test_case' => 'BlockListToken']); | ||
|
||
$token = static::getAuthenticatedToken(); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseIsSuccessful('Pre condition - a valid token should be able to contact the api'); | ||
|
||
static::$client->jsonRequest('GET', '/api/logout_custom', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_OK); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED, 'Logout should invalidate token'); | ||
|
||
$responseBody = json_decode(static::$client->getResponse()->getContent(), true); | ||
$this->assertEquals('Invalid JWT Token', $responseBody['message']); | ||
} | ||
|
||
public function testShouldNotInvalidateTokenOnLogoutWhenBlockListTokenIsDisabled() | ||
{ | ||
static::$client = static::createClient(['test_case' => 'BlockListTokenDisabled']); | ||
$token = static::getAuthenticatedToken(); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseIsSuccessful('Pre condition - a valid token should be able to contact the api'); | ||
|
||
static::$client->jsonRequest('GET', '/api/logout', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_FOUND); | ||
|
||
static::$client->jsonRequest('GET', '/api/secured', [], ['HTTP_AUTHORIZATION' => "Bearer $token"]); | ||
static::assertResponseStatusCodeSame(Response::HTTP_OK, 'Logout should NOT invalidate token when block list config is not enabled'); | ||
} | ||
|
||
public function testShouldNotAddJtiWhenBlockListTokenIsDisabled() | ||
{ | ||
static::$client = static::createClient(['test_case' => 'BlockListTokenDisabled']); | ||
|
||
$token = static::getAuthenticatedToken(); | ||
/** @var JWTManager $jwtManager */ | ||
$jwtManager = static::getContainer()->get('lexik_jwt_authentication.jwt_manager'); | ||
$payload = $jwtManager->parse($token); | ||
self::assertArrayNotHasKey('jti', $payload); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
imports: | ||
- { resource: '../base_config.yml' } | ||
|
||
lexik_jwt_authentication: | ||
blocklist_token: | ||
enabled: true |
Oops, something went wrong.