diff --git a/DependencyInjection/Compiler/CollectPayloadEnrichmentsPass.php b/DependencyInjection/Compiler/CollectPayloadEnrichmentsPass.php
new file mode 100644
index 00000000..ffbbb50f
--- /dev/null
+++ b/DependencyInjection/Compiler/CollectPayloadEnrichmentsPass.php
@@ -0,0 +1,22 @@
+hasDefinition('lexik_jwt_authentication.payload_enrichment')) {
+ return;
+ }
+
+ $container->getDefinition('lexik_jwt_authentication.payload_enrichment')
+ ->replaceArgument(0, $this->findAndSortTaggedServices('lexik_jwt_authentication.payload_enrichment', $container));
+ }
+}
diff --git a/DependencyInjection/LexikJWTAuthenticationExtension.php b/DependencyInjection/LexikJWTAuthenticationExtension.php
index 65aed886..877f80ad 100644
--- a/DependencyInjection/LexikJWTAuthenticationExtension.php
+++ b/DependencyInjection/LexikJWTAuthenticationExtension.php
@@ -174,6 +174,9 @@ public function load(array $configs, ContainerBuilder $container): void
$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');
}
}
diff --git a/EventListener/AddClaimsToJWTListener.php b/EventListener/AddClaimsToJWTListener.php
deleted file mode 100644
index bd180b1c..00000000
--- a/EventListener/AddClaimsToJWTListener.php
+++ /dev/null
@@ -1,19 +0,0 @@
-getData();
-
- if (!isset($data['jti'])) {
- $data['jti'] = bin2hex(random_bytes(16));
-
- $event->setData($data);
- }
- }
-}
diff --git a/LexikJWTAuthenticationBundle.php b/LexikJWTAuthenticationBundle.php
index e1e133f7..cc61d3f2 100644
--- a/LexikJWTAuthenticationBundle.php
+++ b/LexikJWTAuthenticationBundle.php
@@ -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\DeprecateLegacyGuardAuthenticatorPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\RegisterLegacyGuardAuthenticatorPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass;
@@ -34,6 +35,7 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new WireGenerateTokenCommandPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new DeprecateLegacyGuardAuthenticatorPass(), 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');
diff --git a/Resources/config/blocklist_token.xml b/Resources/config/blocklist_token.xml
index 45652fe5..e313a30f 100644
--- a/Resources/config/blocklist_token.xml
+++ b/Resources/config/blocklist_token.xml
@@ -5,10 +5,6 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
-
-
-
-
@@ -27,7 +23,6 @@
-
diff --git a/Resources/config/jwt_manager.xml b/Resources/config/jwt_manager.xml
index faee1bb0..0dbdb963 100644
--- a/Resources/config/jwt_manager.xml
+++ b/Resources/config/jwt_manager.xml
@@ -9,6 +9,7 @@
%lexik_jwt_authentication.user_id_claim%
+
%lexik_jwt_authentication.user_identity_field%
false
@@ -16,5 +17,12 @@
+
+
+
+
+
+
+
diff --git a/Resources/doc/10-invalidate-token.rst b/Resources/doc/10-invalidate-token.rst
index 80588dc6..24f3c1be 100644
--- a/Resources/doc/10-invalidate-token.rst
+++ b/Resources/doc/10-invalidate-token.rst
@@ -22,14 +22,14 @@ To configure token blocklist, update your `lexik_jwt_authentication.yaml` file:
cache: cache.app
-Enabling ``blocklist_token`` causes the activation of listeners:
+Enabling ``blocklist_token``:
-* an event listener ``Lexik\Bundle\JWTAuthenticationBundle\EventListenerAddClaimsToJWTListener`` which adds a ``jti`` claim if not present when the token is created
+* 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`
-* an event listener ``Lexik\Bundle\JWTAuthenticationBundle\BlockJWTListener`` which blocks JWTs on logout (``Symfony\Component\Security\Http\Event\LogoutEvent``)
+* 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``)
-* an event listener ``Lexik\Bundle\JWTAuthenticationBundle\RejectBlockedTokenListener`` which rejects blocked tokens during authentication
+* 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
diff --git a/Services/JWTManager.php b/Services/JWTManager.php
index b2008bbd..3fcf7c09 100644
--- a/Services/JWTManager.php
+++ b/Services/JWTManager.php
@@ -9,6 +9,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
+use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\NullEnrichment;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
@@ -45,15 +46,21 @@ class JWTManager implements JWTManagerInterface, JWTTokenManagerInterface
*/
protected $userIdClaim;
+ /**
+ * @var PayloadEnrichmentInterface
+ */
+ private $payloadEnrichment;
+
/**
* @param string|null $userIdClaim
*/
- public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, $userIdClaim = null)
+ public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, $userIdClaim = null, PayloadEnrichmentInterface $payloadEnrichment = null)
{
$this->jwtEncoder = $encoder;
$this->dispatcher = $dispatcher;
$this->userIdentityField = 'username';
$this->userIdClaim = $userIdClaim;
+ $this->payloadEnrichment = $payloadEnrichment ?? new NullEnrichment();
}
/**
@@ -64,6 +71,8 @@ public function create(UserInterface $user): string
$payload = ['roles' => $user->getRoles()];
$this->addUserIdentityToPayload($user, $payload);
+ $this->payloadEnrichment->enrich($user, $payload);
+
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
@@ -75,6 +84,8 @@ public function createFromPayload(UserInterface $user, array $payload): string
$payload = array_merge(['roles' => $user->getRoles()], $payload);
$this->addUserIdentityToPayload($user, $payload);
+ $this->payloadEnrichment->enrich($user, $payload);
+
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
diff --git a/Services/PayloadEnrichment/ChainEnrichment.php b/Services/PayloadEnrichment/ChainEnrichment.php
new file mode 100644
index 00000000..5dcc4b5b
--- /dev/null
+++ b/Services/PayloadEnrichment/ChainEnrichment.php
@@ -0,0 +1,26 @@
+enrichments = $enrichments;
+ }
+
+ public function enrich(UserInterface $user, array &$payload): void
+ {
+ foreach ($this->enrichments as $enrichment) {
+ $enrichment->enrich($user, $payload);
+ }
+ }
+}
diff --git a/Services/PayloadEnrichment/NullEnrichment.php b/Services/PayloadEnrichment/NullEnrichment.php
new file mode 100644
index 00000000..f18669ff
--- /dev/null
+++ b/Services/PayloadEnrichment/NullEnrichment.php
@@ -0,0 +1,13 @@
+ 'bar'];
+
+ $enrichmentFoo = new class() implements PayloadEnrichmentInterface {
+ public function enrich(UserInterface $user, array &$payload): void
+ {
+ $payload['foo'] = 'baz';
+ }
+ };
+
+ $enrichmentBar = new class() implements PayloadEnrichmentInterface {
+ public function enrich(UserInterface $user, array &$payload): void
+ {
+ $payload['bar'] = 'qux';
+ }
+ };
+
+ $chainEnrichment = new ChainEnrichment([$enrichmentFoo, $enrichmentBar]);
+ $chainEnrichment->enrich($this->createMock(UserInterface::class), $payload);
+
+ $this->assertEquals(['foo' => 'baz', 'bar' => 'qux'], $payload);
+ }
+}
diff --git a/Tests/PayloadEnrichment/NullEnrichmentTest.php b/Tests/PayloadEnrichment/NullEnrichmentTest.php
new file mode 100644
index 00000000..cf491da4
--- /dev/null
+++ b/Tests/PayloadEnrichment/NullEnrichmentTest.php
@@ -0,0 +1,18 @@
+ 'bar'];
+ $enrichment = new NullEnrichment();
+ $enrichment->enrich($this->createMock(UserInterface::class), $payload);
+
+ $this->assertEquals(['foo' => 'bar'], $payload);
+ }
+}
diff --git a/Tests/PayloadEnrichment/RandomJtiEnrichmentTest.php b/Tests/PayloadEnrichment/RandomJtiEnrichmentTest.php
new file mode 100644
index 00000000..40911bae
--- /dev/null
+++ b/Tests/PayloadEnrichment/RandomJtiEnrichmentTest.php
@@ -0,0 +1,20 @@
+ 'bar'];
+ $enrichment = new RandomJtiEnrichment();
+ $enrichment->enrich($this->createMock(UserInterface::class), $payload);
+
+ $this->assertArrayHasKey('jti', $payload);
+ $this->assertIsString($payload['jti']);
+ $this->assertArrayHasKey('foo', $payload);
+ }
+}
diff --git a/Tests/Services/JWTManagerTest.php b/Tests/Services/JWTManagerTest.php
index 1954a43a..d9bab4ac 100644
--- a/Tests/Services/JWTManagerTest.php
+++ b/Tests/Services/JWTManagerTest.php
@@ -9,6 +9,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManager;
+use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Tests\Stubs\User as CustomUser;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\User\InMemoryUser;
@@ -48,6 +49,25 @@ public function testCreate()
$this->assertEquals('secrettoken', $manager->create($this->createUser('user', 'password')));
}
+ public function testCreateWithPayloadEnrichment()
+ {
+ $dispatcher = $this->getEventDispatcherMock();
+ $encoder = $this->getJWTEncoderMock();
+ $encoder
+ ->method('encode')
+ ->with($this->arrayHasKey('baz'))
+ ->willReturn('secrettoken');
+
+ $manager = new JWTManager($encoder, $dispatcher, 'username', new class() implements PayloadEnrichmentInterface {
+ public function enrich(UserInterface $user, array &$payload): void
+ {
+ $payload['baz'] = 'qux';
+ }
+ });
+
+ $this->assertEquals('secrettoken', $manager->create($this->createUser('user', 'password')));
+ }
+
/**
* test create.
*/
@@ -74,6 +94,26 @@ public function testCreateFromPayload()
$this->assertEquals('secrettoken', $manager->createFromPayload($this->createUser('user', 'password'), $payload));
}
+ public function testCreateFromPayloadWithPayloadEnrichment()
+ {
+ $dispatcher = $this->getEventDispatcherMock();
+
+ $encoder = $this->getJWTEncoderMock();
+ $encoder
+ ->method('encode')
+ ->with($this->arrayHasKey('baz'))
+ ->willReturn('secrettoken');
+
+ $manager = new JWTManager($encoder, $dispatcher, 'username', new class() implements PayloadEnrichmentInterface {
+ public function enrich(UserInterface $user, array &$payload): void
+ {
+ $payload['baz'] = 'qux';
+ }
+ });
+ $payload = ['foo' => 'bar'];
+ $this->assertEquals('secrettoken', $manager->createFromPayload($this->createUser('user', 'password'), $payload));
+ }
+
/**
* test decode.
*/