Skip to content

Commit

Permalink
Merge branch '2.x' into 3.x
Browse files Browse the repository at this point in the history
* 2.x:
  Update CHANGELOG for v2.21.0
  Invalidate a JWT token - Adding the jti claim by the JWTManager class instead of doing it via a listener
  feat: Invalidate a JWT token
  • Loading branch information
chalasr committed May 1, 2024
2 parents 0f384f0 + d57159d commit e84c4ed
Show file tree
Hide file tree
Showing 36 changed files with 943 additions and 99 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
CHANGELOG
=========

For a diff between two versions https://github.com/lexik/LexikJWTAuthenticationBundle/compare/v1.0.0...v2.20.3
For a diff between two versions https://github.com/lexik/LexikJWTAuthenticationBundle/compare/v1.0.0...v2.21.0

## [2.21.0](https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.21.0) (2024-04-27)

* feature [\#1218](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/1170) Invalidate a JWT token ([@ldaspt](https://github.com/ldaspt))
* feature [\#1170](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/1170) Invalidate a JWT token ([@ldaspt](https://github.com/ldaspt))
* feature [\#1207](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/1207) Web-Token Framework simplified ([@Spomky](https://github.com/Spomky))
* bug [\60770f1](https://github.com/lexik/LexikJWTAuthenticationBundle/commit/60770f1fb9ec0b8b0f27a0ee9e2bf441ca4a1db9) Fix CI & web-token encryption support ([@Spomky](https://github.com/Spomky))

## [2.20.3](https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.20.3) (2023-12-14)

Expand Down
22 changes: 22 additions & 0 deletions DependencyInjection/Compiler/CollectPayloadEnrichmentsPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CollectPayloadEnrichmentsPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('lexik_jwt_authentication.payload_enrichment')) {
return;
}

$container->getDefinition('lexik_jwt_authentication.payload_enrichment')
->replaceArgument(0, $this->findAndSortTaggedServices('lexik_jwt_authentication.payload_enrichment', $container));
}
}
10 changes: 10 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->end()
->arrayNode('blocklist_token')
->addDefaultsIfNotSet()
->canBeEnabled()
->children()
->scalarNode('cache')
->defaultValue('cache.app')
->info('Storage to track blocked tokens')
->end()
->end()
->end()
->end()
->end();

Expand Down
9 changes: 9 additions & 0 deletions DependencyInjection/LexikJWTAuthenticationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ public function load(array $configs, ContainerBuilder $container): void
}

$this->processWithWebTokenConfig($config, $container, $loader);

if ($this->isConfigEnabled($container, $config['blocklist_token'])) {
$loader->load('blocklist_token.xml');
$blockListTokenConfig = $config['blocklist_token'];
$container->setAlias('lexik_jwt_authentication.blocklist_token.cache', $blockListTokenConfig['cache']);
} else {
$container->getDefinition('lexik_jwt_authentication.payload_enrichment.random_jti_enrichment')
->clearTag('lexik_jwt_authentication.payload_enrichment');
}
}

private function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig): array
Expand Down
67 changes: 67 additions & 0 deletions EventListener/BlockJWTListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener;

use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\CacheItemPoolBlockedTokenManager;
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
{
private $blockedTokenManager;
private $tokenExtractor;
private $jwtManager;

public function __construct(
BlockedTokenManagerInterface $blockedTokenManager,
TokenExtractorInterface $tokenExtractor,
JWTTokenManagerInterface $jwtManager
) {
$this->blockedTokenManager = $blockedTokenManager;
$this->tokenExtractor = $tokenExtractor;
$this->jwtManager = $jwtManager;
}

public function onLoginFailure(LoginFailureEvent $event): void
{
$exception = $event->getException();
if (($exception instanceof DisabledException) || ($exception->getPrevious() 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 $e) {
// Ignore decode failures, this would mean the token is invalid anyway
return;
}

try {
$this->blockedTokenManager->add($payload);
} catch (MissingClaimException $e) {
// We can't block a token missing the claims our system requires, so silently ignore this one
}
}
}
32 changes: 32 additions & 0 deletions EventListener/RejectBlockedTokenListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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\BlockedTokenManagerInterface;

class RejectBlockedTokenListener
{
private $blockedTokenManager;

public function __construct(BlockedTokenManagerInterface $blockedTokenManager)
{
$this->blockedTokenManager = $blockedTokenManager;
}

/**
* @throws InvalidTokenException if the JWT is blocked
*/
public function __invoke(JWTAuthenticatedEvent $event): void
{
try {
if ($this->blockedTokenManager->has($event->getPayload())) {
throw new InvalidTokenException('JWT blocked');
}
} catch (MissingClaimException $e) {
// Do nothing if the required claims do not exist on the payload (older JWTs won't have the "jti" claim the manager requires)
}
}
}
15 changes: 15 additions & 0 deletions Exception/MissingClaimException.php
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);
}
}
2 changes: 2 additions & 0 deletions LexikJWTAuthenticationBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lexik\Bundle\JWTAuthenticationBundle;

use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\ApiPlatformOpenApiPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\CollectPayloadEnrichmentsPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTAuthenticatorFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTUserFactory;
Expand All @@ -27,6 +28,7 @@ public function build(ContainerBuilder $container): void

$container->addCompilerPass(new WireGenerateTokenCommandPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ApiPlatformOpenApiPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CollectPayloadEnrichmentsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);

/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The bulk of the documentation is stored in the [`Resources/doc`](Resources/doc/i
* [Creating JWT tokens programmatically](Resources/doc/7-manual-token-creation.rst)
* [A database-less user provider](Resources/doc/8-jwt-user-provider.rst)
* [Accessing the authenticated JWT token](Resources/doc/9-access-authenticated-jwt-token.rst)
* [Invalidate token on logout](Resources/doc/10-invalidate-token-on-logout.rst)

Community Support
-----------------
Expand Down
28 changes: 28 additions & 0 deletions Resources/config/blocklist_token.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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.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\BlockedToken\CacheItemPoolBlockedTokenManager">
<argument type="service" id="lexik_jwt_authentication.blocklist_token.cache"/>
</service>

<service id="Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface" alias="lexik_jwt_authentication.blocked_token_manager" />
</services>

</container>
8 changes: 8 additions & 0 deletions Resources/config/jwt_manager.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@
<argument type="service" id="lexik_jwt_authentication.encoder"/>
<argument type="service" id="event_dispatcher"/>
<argument>%lexik_jwt_authentication.user_id_claim%</argument>
<argument type="service" id="lexik_jwt_authentication.payload_enrichment"/>
</service>

<service id="Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface" alias="lexik_jwt_authentication.jwt_manager" />

<service id="lexik_jwt_authentication.payload_enrichment.random_jti_enrichment" class="Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\RandomJtiEnrichment">
<tag name="lexik_jwt_authentication.payload_enrichment" priority="0" />
</service>
<service id="lexik_jwt_authentication.payload_enrichment" class="Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\ChainEnrichment">
<argument type="collection"/>
</service>
</services>
</container>
5 changes: 5 additions & 0 deletions Resources/doc/1-configuration-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ Full default configuration
# remove the token from the response body when using cookies
remove_token_from_body_when_cookies_used: true
# invalidate the token on logout by storing it in the cache
blocklist_token:
enabled: true
cache: cache.app
Encoder configuration
~~~~~~~~~~~~~~~~~~~~~

Expand Down
89 changes: 89 additions & 0 deletions Resources/doc/10-invalidate-token.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
Invalidate token
================

The token blocklist relies on the ``jti`` claim, a standard claim designed for tracking and revoking JWTs. `"jti" (JWT ID) Claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7>`_

The blocklist storage utilizes a cache implementing ``Psr\Cache\CacheItemPoolInterface``. The cache stores the ``jti`` of the blocked token to the cache, and the cache item expires after the "exp" (expiration time) claim of the token

Configuration
~~~~~~~~~~~~~

To configure token blocklist, update your `lexik_jwt_authentication.yaml` file:

.. code-block:: yaml
# config/packages/lexik_jwt_authentication.yaml
# ...
lexik_jwt_authentication:
# ...
# invalidate the token on logout by storing it in the cache
blocklist_token:
enabled: true
cache: cache.app
Enabling ``blocklist_token``:

* Adds a ``jti`` claim to the payload via `Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\RandomJtiEnrichment` passed as an argument to the `Lexik\Bundle\JWTAuthenticationBundle\Services\JwtManager`

* activates the event listener ``Lexik\Bundle\JWTAuthenticationBundle\BlockJWTListener`` which blocks JWTs on logout (``Symfony\Component\Security\Http\Event\LogoutEvent``)
or on login failure due to the user not being enabled (``Symfony\Component\Security\Core\Exception\DisabledException``)

* activates an event listener ``Lexik\Bundle\JWTAuthenticationBundle\RejectBlockedTokenListener`` which rejects blocked tokens during authentication

To block JWTs on logout, you must either activate logout in the firewall configuration or do it programmatically

* by firewall configuration

.. code-block:: yaml
# config/packages/security.yaml
security:
enable_authenticator_manager: true
firewalls:
api:
...
jwt: ~
logout:
path: app_logout
* programmatically in a controller action

.. code-block:: php
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
//...
class SecurityController
{
//...
public function logout(Request $request, EventDispatcherInterface $eventDispatcher, TokenStorageInterface $tokenStorage)
{
$eventDispatcher->dispatch(new LogoutEvent($request, $tokenStorage->getToken()));
return new JsonResponse();
}
]
Refer to `Symfony logging out <https://symfony.com/doc/current/security.html#logging-out>`_ for more details.

Changing blocklist storage
~~~~~~~~~~~~~~~~~~~~~~~~~~

To change the blocklist storage, refer to `Configuring Cache with FrameworkBundle <https://symfony.com/doc/current/cache.html#configuring-cache-with-frameworkbundle>`_

.. code-block:: yaml
# config/packages/framework.yaml
framework:
# ...
cache:
default_redis_provider: 'redis://localhost'
pools:
block_list_token_cache_pool:
adapter: cache.adapter.redis
# ...
blocklist_token:
enabled: true
cache: block_list_token_cache_pool
Loading

0 comments on commit e84c4ed

Please sign in to comment.