├── Command
├── CheckConfigCommand.php
├── EnableEncryptionConfigCommand.php
├── GenerateKeyPairCommand.php
├── GenerateTokenCommand.php
└── MigrateConfigCommand.php
├── DependencyInjection
├── Compiler
│ ├── ApiPlatformOpenApiPass.php
│ ├── CollectPayloadEnrichmentsPass.php
│ └── WireGenerateTokenCommandPass.php
├── Configuration.php
├── LexikJWTAuthenticationExtension.php
└── Security
│ └── Factory
│ ├── JWTAuthenticatorFactory.php
│ └── JWTUserFactory.php
├── Encoder
├── HeaderAwareJWTEncoderInterface.php
├── JWTEncoderInterface.php
├── LcobucciJWTEncoder.php
└── WebTokenEncoder.php
├── Event
├── AuthenticationFailureEvent.php
├── AuthenticationSuccessEvent.php
├── BeforeJWEComputationEvent.php
├── JWTAuthenticatedEvent.php
├── JWTCreatedEvent.php
├── JWTDecodedEvent.php
├── JWTEncodedEvent.php
├── JWTExpiredEvent.php
├── JWTFailureEventInterface.php
├── JWTInvalidEvent.php
└── JWTNotFoundEvent.php
├── EventListener
├── BlockJWTListener.php
└── RejectBlockedTokenListener.php
├── Events.php
├── Exception
├── ExpiredTokenException.php
├── InvalidPayloadException.php
├── InvalidTokenException.php
├── JWTDecodeFailureException.php
├── JWTEncodeFailureException.php
├── JWTFailureException.php
├── MissingClaimException.php
├── MissingTokenException.php
└── UserNotFoundException.php
├── Helper
└── JWTSplitter.php
├── LICENSE
├── LexikJWTAuthenticationBundle.php
├── OpenApi
└── OpenApiFactory.php
├── Resources
└── config
│ ├── api_platform.xml
│ ├── blocklist_token.xml
│ ├── console.xml
│ ├── cookie.xml
│ ├── jwt_manager.xml
│ ├── key_loader.xml
│ ├── lcobucci.xml
│ ├── response_interceptor.xml
│ ├── token_authenticator.xml
│ ├── token_extractor.xml
│ ├── web_token.xml
│ ├── web_token_issuance.xml
│ └── web_token_verification.xml
├── Response
├── JWTAuthenticationFailureResponse.php
└── JWTAuthenticationSuccessResponse.php
├── Security
├── Authenticator
│ ├── JWTAuthenticator.php
│ └── Token
│ │ └── JWTPostAuthenticationToken.php
├── Http
│ ├── Authentication
│ │ ├── AuthenticationFailureHandler.php
│ │ └── AuthenticationSuccessHandler.php
│ └── Cookie
│ │ └── JWTCookieProvider.php
└── User
│ ├── JWTUser.php
│ ├── JWTUserInterface.php
│ ├── JWTUserProvider.php
│ └── PayloadAwareUserProviderInterface.php
├── Services
├── BlockedToken
│ └── CacheItemPoolBlockedTokenManager.php
├── BlockedTokenManagerInterface.php
├── JWSProvider
│ ├── JWSProviderInterface.php
│ └── LcobucciJWSProvider.php
├── JWTManager.php
├── JWTTokenManagerInterface.php
├── KeyLoader
│ ├── AbstractKeyLoader.php
│ ├── KeyDumperInterface.php
│ ├── KeyLoaderInterface.php
│ └── RawKeyLoader.php
├── PayloadEnrichment
│ ├── ChainEnrichment.php
│ ├── NullEnrichment.php
│ └── RandomJtiEnrichment.php
├── PayloadEnrichmentInterface.php
└── WebToken
│ ├── AccessTokenBuilder.php
│ └── AccessTokenLoader.php
├── Signature
├── CreatedJWS.php
└── LoadedJWS.php
├── Subscriber
└── AdditionalAccessTokenClaimsAndHeaderSubscriber.php
├── TokenExtractor
├── AuthorizationHeaderTokenExtractor.php
├── ChainTokenExtractor.php
├── CookieTokenExtractor.php
├── QueryParameterTokenExtractor.php
├── SplitCookieExtractor.php
└── TokenExtractorInterface.php
├── composer.json
├── ecs.php
└── rector.php
/Command/CheckConfigCommand.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | #[AsCommand(name: 'lexik:jwt:check-config', description: 'Checks that the bundle is properly configured.')]
15 | final class CheckConfigCommand extends Command
16 | {
17 | private KeyLoaderInterface $keyLoader;
18 |
19 | private string $signatureAlgorithm;
20 |
21 | public function __construct(KeyLoaderInterface $keyLoader, string $signatureAlgorithm)
22 | {
23 | $this->keyLoader = $keyLoader;
24 | $this->signatureAlgorithm = $signatureAlgorithm;
25 |
26 | parent::__construct();
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | protected function execute(InputInterface $input, OutputInterface $output): int
33 | {
34 | try {
35 | $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE);
36 | // No public key for HMAC
37 | if (!str_contains($this->signatureAlgorithm, 'HS')) {
38 | $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC);
39 | }
40 | } catch (\RuntimeException $e) {
41 | $output->writeln('' . $e->getMessage() . '');
42 |
43 | return Command::FAILURE;
44 | }
45 |
46 | $output->writeln('The configuration seems correct.');
47 |
48 | return Command::SUCCESS;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Command/EnableEncryptionConfigCommand.php:
--------------------------------------------------------------------------------
1 |
35 | */
36 | #[AsCommand(name: 'lexik:jwt:enable-encryption', description: 'Enable Web-Token encryption support.')]
37 | final class EnableEncryptionConfigCommand extends AbstractConfigCommand
38 | {
39 | /**
40 | * @deprecated
41 | */
42 | protected static $defaultName = 'lexik:jwt:enable-encryption';
43 |
44 | /**
45 | * @var ?AlgorithmManagerFactory
46 | */
47 | private $algorithmManagerFactory;
48 |
49 | public function __construct(
50 | ?AlgorithmManagerFactory $algorithmManagerFactory = null
51 | ) {
52 | parent::__construct();
53 |
54 | $this->algorithmManagerFactory = $algorithmManagerFactory;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | protected function configure(): void
61 | {
62 | $this
63 | ->setName(static::$defaultName)
64 | ->setDescription('Enable Web-Token encryption support.')
65 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the modification of the configuration, even if already set.')
66 | ;
67 | }
68 |
69 | public function isEnabled(): bool
70 | {
71 | return $this->algorithmManagerFactory !== null;
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | *
77 | * @return int
78 | */
79 | protected function execute(InputInterface $input, OutputInterface $output): int
80 | {
81 | $force = $input->getOption('force');
82 | $this->checkRequirements();
83 | $io = new SymfonyStyle($input, $output);
84 | $io->title('Web-Token Encryption support');
85 | $io->info('This tool will help you enabling the encryption support for Web-Token');
86 |
87 | $algorithms = $this->algorithmManagerFactory->all();
88 | $availableKeyEncryptionAlgorithms = array_map(
89 | static function (Algorithm $algorithm): string {
90 | return $algorithm->name();
91 | },
92 | array_filter($algorithms, static function (Algorithm $algorithm): bool {
93 | return ($algorithm instanceof KeyEncryptionAlgorithm && $algorithm->name() !== 'dir');
94 | })
95 | );
96 | $availableContentEncryptionAlgorithms = array_map(
97 | static function (Algorithm $algorithm): string {
98 | return $algorithm->name();
99 | },
100 | array_filter($algorithms, static function (Algorithm $algorithm): bool {
101 | return $algorithm instanceof ContentEncryptionAlgorithm;
102 | })
103 | );
104 |
105 | $keyEncryptionAlgorithmAlias = $io->choice('Key Encryption Algorithm', $availableKeyEncryptionAlgorithms);
106 | $contentEncryptionAlgorithmAlias = $io->choice('Content Encryption Algorithm', $availableContentEncryptionAlgorithms);
107 | $keyEncryptionAlgorithm = $algorithms[$keyEncryptionAlgorithmAlias];
108 | $contentEncryptionAlgorithm = $algorithms[$contentEncryptionAlgorithmAlias];
109 |
110 | $continueOnDecryptionFailure = 'yes' === $io->choice('Continue decryption on failure', ['yes', 'no'], 'no');
111 |
112 | $extension = $this->findExtension('lexik_jwt_authentication');
113 | $config = $this->getConfiguration($extension);
114 | if (!isset($config['encoder']['service']) || $config['encoder']['service'] !== 'lexik_jwt_authentication.encoder.web_token') {
115 | $io->error('Please migrate to WebToken first.');
116 | return self::FAILURE;
117 | }
118 | if (!$force && ($config['access_token_issuance']['encryption']['enabled'] || $config['access_token_verification']['encryption']['enabled'])) {
119 | $io->error('Encryption support is already enabled.');
120 | return self::FAILURE;
121 | }
122 |
123 | $key = $this->generatePrivateKey($keyEncryptionAlgorithm);
124 | $keyset = $this->generatePublicKeyset($key, $keyEncryptionAlgorithm->name());
125 |
126 | $config['access_token_issuance']['encryption'] = [
127 | 'enabled' => true,
128 | 'key_encryption_algorithm' => $keyEncryptionAlgorithm->name(),
129 | 'content_encryption_algorithm' => $contentEncryptionAlgorithm->name(),
130 | 'key' => json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
131 | ];
132 | $config['access_token_verification']['encryption'] = [
133 | 'enabled' => true,
134 | 'continue_on_decryption_failure' => $continueOnDecryptionFailure,
135 | 'header_checkers' => ['iat_with_clock_skew', 'nbf_with_clock_skew', 'exp_with_clock_skew'],
136 | 'allowed_key_encryption_algorithms' => [$keyEncryptionAlgorithm->name()],
137 | 'allowed_content_encryption_algorithms' => [$contentEncryptionAlgorithm->name()],
138 | 'keyset' => json_encode($keyset, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
139 | ];
140 |
141 | $io->comment('Please replace the current configuration with the following parameters.');
142 | $io->section('# config/packages/lexik_jwt_authentication.yaml');
143 | $io->writeln(Yaml::dump([$extension->getAlias() => $config], 10));
144 | $io->section('# End of file');
145 |
146 | return self::SUCCESS;
147 | }
148 |
149 | private function generatePublicKeyset(JWK $key, string $algorithm): JWKSet
150 | {
151 | $keyset = new JWKSet([$key->toPublic()]);
152 | switch ($key->get('kty')) {
153 | case 'oct':
154 | return $this->withOctKeys($keyset, $algorithm);
155 | case 'OKP':
156 | return $this->withOkpKeys($keyset, $algorithm, $key->get('crv'));
157 | case 'EC':
158 | return $this->withEcKeys($keyset, $algorithm, $key->get('crv'));
159 | case 'RSA':
160 | return $this->withRsaKeys($keyset, $algorithm);
161 | default:
162 | throw new \InvalidArgumentException('Unsupported key type.');
163 | }
164 | }
165 |
166 | private function withOctKeys(JWKSet $keyset, string $algorithm): JWKSet
167 | {
168 | $size = $this->getKeySize($algorithm);
169 |
170 | return $keyset
171 | ->with($this->createOctKey($size, $algorithm)->toPublic())
172 | ->with($this->createOctKey($size, $algorithm)->toPublic())
173 | ;
174 | }
175 |
176 | private function withRsaKeys(JWKSet $keyset, string $algorithm): JWKSet
177 | {
178 | return $keyset
179 | ->with($this->createRsaKey(2048, $algorithm)->toPublic())
180 | ->with($this->createRsaKey(2048, $algorithm)->toPublic())
181 | ;
182 | }
183 |
184 | private function withOkpKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
185 | {
186 | return $keyset
187 | ->with($this->createOkpKey($curve, $algorithm)->toPublic())
188 | ->with($this->createOkpKey($curve, $algorithm)->toPublic())
189 | ;
190 | }
191 |
192 | private function withEcKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
193 | {
194 | return $keyset
195 | ->with($this->createEcKey($curve, $algorithm)->toPublic())
196 | ->with($this->createEcKey($curve, $algorithm)->toPublic())
197 | ;
198 | }
199 |
200 | private function generatePrivateKey(KeyEncryptionAlgorithm $algorithm): JWK
201 | {
202 | $keyType = current($algorithm->allowedKeyTypes());
203 | switch ($keyType) {
204 | case 'oct':
205 | return $this->createOctKey($this->getKeySize($algorithm->name()), $algorithm->name());
206 | case 'OKP':
207 | return $this->createOkpKey('X25519', $algorithm->name());
208 | case 'EC':
209 | return $this->createEcKey('P-256', $algorithm->name());
210 | case 'RSA':
211 | return $this->createRsaKey($this->getKeySize($algorithm->name()), $algorithm->name());
212 | default:
213 | throw new \InvalidArgumentException('Unsupported key type.');
214 | }
215 | }
216 |
217 | private function checkRequirements(): void
218 | {
219 | $requirements = [
220 | JoseFrameworkBundle::class => 'web-token/jwt-bundle',
221 | JWKFactory::class => 'web-token/jwt-key-mgmt',
222 | ClaimCheckerManager::class => 'web-token/jwt-checker',
223 | JWEBuilder::class => 'web-token/jwt-encryption',
224 | ];
225 | if ($this->algorithmManagerFactory === null) {
226 | throw new \RuntimeException('The package "web-token/jwt-bundle" is missing. Please install it for using this migration tool.');
227 | }
228 | foreach (array_keys($requirements) as $requirement) {
229 | if (!class_exists($requirement)) {
230 | throw new \RuntimeException(sprintf('The package "%s" is missing. Please install it for using this migration tool.', $requirement));
231 | }
232 | }
233 | }
234 | private function getConfiguration(ExtensionInterface $extension): array
235 | {
236 | $container = $this->compileContainer();
237 |
238 | $config = $this->getConfig($extension, $container);
239 | $uselessParameters = ['secret_key', 'public_key', 'pass_phrase', 'private_key_path', 'public_key_path', 'additional_public_keys'];
240 | foreach ($uselessParameters as $parameter) {
241 | unset($config[$parameter]);
242 | }
243 |
244 | return $config;
245 | }
246 |
247 | private function createOctKey(int $size, string $algorithm): JWK
248 | {
249 | return JWKFactory::createOctKey($size, $this->getOptions($algorithm));
250 | }
251 |
252 | private function createRsaKey(int $size, string $algorithm): JWK
253 | {
254 | return JWKFactory::createRSAKey($size, $this->getOptions($algorithm));
255 | }
256 |
257 | private function createOkpKey(string $curve, string $algorithm): JWK
258 | {
259 | return JWKFactory::createOKPKey($curve, $this->getOptions($algorithm));
260 | }
261 |
262 | private function createEcKey(string $curve, string $algorithm): JWK
263 | {
264 | return JWKFactory::createECKey($curve, $this->getOptions($algorithm));
265 | }
266 |
267 | private function compileContainer(): ContainerBuilder
268 | {
269 | $kernel = clone $this->getApplication()->getKernel();
270 | $kernel->boot();
271 |
272 | $method = new \ReflectionMethod($kernel, 'buildContainer');
273 | $container = $method->invoke($kernel);
274 | $container->getCompiler()->compile($container);
275 |
276 | return $container;
277 | }
278 |
279 | private function getConfig(ExtensionInterface $extension, ContainerBuilder $container)
280 | {
281 | return $container->resolveEnvPlaceholders(
282 | $container->getParameterBag()->resolveValue(
283 | $this->getConfigForExtension($extension, $container)
284 | )
285 | );
286 | }
287 |
288 | private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array
289 | {
290 | $extensionAlias = $extension->getAlias();
291 |
292 | $extensionConfig = [];
293 | foreach ($container->getCompilerPassConfig()->getPasses() as $pass) {
294 | if ($pass instanceof ValidateEnvPlaceholdersPass) {
295 | $extensionConfig = $pass->getExtensionConfig();
296 | break;
297 | }
298 | }
299 |
300 | if (isset($extensionConfig[$extensionAlias])) {
301 | return $extensionConfig[$extensionAlias];
302 | }
303 |
304 | // Fall back to default config if the extension has one
305 |
306 | if (!$extension instanceof ConfigurationExtensionInterface) {
307 | throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias));
308 | }
309 |
310 | $configs = $container->getExtensionConfig($extensionAlias);
311 | $configuration = $extension->getConfiguration($configs, $container);
312 | $this->validateConfiguration($extension, $configuration);
313 |
314 | return (new Processor())->processConfiguration($configuration, $configs);
315 | }
316 |
317 | private function getKeySize(string $algorithm): int
318 | {
319 | switch ($algorithm) {
320 | case 'RSA1_5':
321 | case 'RSA-OAEP':
322 | case 'RSA-OAEP-256':
323 | return 4096;
324 | case 'A128KW':
325 | case 'A128GCMKW':
326 | case 'PBES2-HS256+A128KW':
327 | return 128;
328 | case 'A192KW':
329 | case 'A192GCMKW':
330 | case 'PBES2-HS384+A192KW':
331 | return 192;
332 | case 'A256KW':
333 | case 'A256GCMKW':
334 | case 'PBES2-HS512+A256KW':
335 | return 256;
336 | default:
337 | throw new \LogicException('Unsupported algorithm');
338 | }
339 | }
340 |
341 | private function getOptions(string $algorithm): array
342 | {
343 | return [
344 | 'use' => 'enc',
345 | 'alg' => $algorithm,
346 | 'kid' => Base64UrlSafe::encodeUnpadded(random_bytes(16))
347 | ];
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/Command/GenerateKeyPairCommand.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | #[AsCommand(name: self::NAME, description: 'Generate public/private keys for use in your application.')]
18 | final class GenerateKeyPairCommand extends Command
19 | {
20 | private const NAME = 'lexik:jwt:generate-keypair';
21 |
22 | private const ACCEPTED_ALGORITHMS = [
23 | 'RS256',
24 | 'RS384',
25 | 'RS512',
26 | 'HS256',
27 | 'HS384',
28 | 'HS512',
29 | 'ES256',
30 | 'ES384',
31 | 'ES512',
32 | ];
33 |
34 | private Filesystem $filesystem;
35 |
36 | private ?string $secretKey;
37 |
38 | private ?string $publicKey;
39 |
40 | private ?string $passphrase;
41 |
42 | private string $algorithm;
43 |
44 | public function __construct(Filesystem $filesystem, ?string $secretKey, ?string $publicKey, ?string $passphrase, string $algorithm)
45 | {
46 | $this->filesystem = $filesystem;
47 | $this->secretKey = $secretKey;
48 | $this->publicKey = $publicKey;
49 | $this->passphrase = $passphrase;
50 | $this->algorithm = $algorithm;
51 |
52 | parent::__construct();
53 | }
54 |
55 | protected function configure(): void
56 | {
57 | $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not update key files.');
58 | $this->addOption('skip-if-exists', null, InputOption::VALUE_NONE, 'Do not update key files if they already exist.');
59 | $this->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite key files if they already exist.');
60 | }
61 |
62 | protected function execute(InputInterface $input, OutputInterface $output): int
63 | {
64 | $io = new SymfonyStyle($input, $output);
65 |
66 | if (!in_array($this->algorithm, self::ACCEPTED_ALGORITHMS, true)) {
67 | $io->error(sprintf('Cannot generate key pair with the provided algorithm `%s`.', $this->algorithm));
68 |
69 | return Command::FAILURE;
70 | }
71 |
72 | [$secretKey, $publicKey] = $this->generateKeyPair($this->passphrase);
73 |
74 | if (true === $input->getOption('dry-run')) {
75 | $io->success('Your keys have been generated!');
76 | $io->newLine();
77 | $io->writeln(sprintf('Update your private key in %s:', $this->secretKey));
78 | $io->writeln($secretKey);
79 | $io->newLine();
80 | $io->writeln(sprintf('Update your public key in %s:', $this->publicKey));
81 | $io->writeln($publicKey);
82 |
83 | return Command::SUCCESS;
84 | }
85 |
86 | if (null === $this->secretKey || null === $this->publicKey) {
87 | throw new LogicException(sprintf('The "lexik_jwt_authentication.secret_key" and "lexik_jwt_authentication.public_key" config options must not be empty for using the "%s" command.', self::NAME));
88 | }
89 |
90 | $alreadyExists = $this->filesystem->exists($this->secretKey) || $this->filesystem->exists($this->publicKey);
91 |
92 | if ($alreadyExists) {
93 | try {
94 | $this->handleExistingKeys($input);
95 | } catch (\RuntimeException $e) {
96 | if (0 === $e->getCode()) {
97 | $io->comment($e->getMessage());
98 |
99 | return Command::SUCCESS;
100 | }
101 |
102 | $io->error($e->getMessage());
103 |
104 | return Command::FAILURE;
105 | }
106 |
107 | if (!$io->confirm('You are about to replace your existing keys. Are you sure you wish to continue?')) {
108 | $io->comment('Your action was canceled.');
109 |
110 | return Command::SUCCESS;
111 | }
112 | }
113 |
114 | $this->filesystem->dumpFile($this->secretKey, $secretKey);
115 | $this->filesystem->dumpFile($this->publicKey, $publicKey);
116 |
117 | $io->success('Done!');
118 |
119 | return Command::SUCCESS;
120 | }
121 |
122 | private function handleExistingKeys(InputInterface $input): void
123 | {
124 | if (true === $input->getOption('skip-if-exists') && true === $input->getOption('overwrite')) {
125 | throw new \RuntimeException('Both options `--skip-if-exists` and `--overwrite` cannot be combined.', 1);
126 | }
127 |
128 | if (true === $input->getOption('skip-if-exists')) {
129 | throw new \RuntimeException('Your key files already exist, they won\'t be overriden.', 0);
130 | }
131 |
132 | if (false === $input->getOption('overwrite')) {
133 | throw new \RuntimeException('Your keys already exist. Use the `--overwrite` option to force regeneration.', 1);
134 | }
135 | }
136 |
137 | private function generateKeyPair(?string $passphrase): array
138 | {
139 | $config = $this->buildOpenSSLConfiguration();
140 |
141 | $resource = \openssl_pkey_new($config);
142 | if (false === $resource) {
143 | throw new \RuntimeException(\openssl_error_string());
144 | }
145 |
146 | $success = \openssl_pkey_export($resource, $privateKey, $passphrase);
147 |
148 | if (false === $success) {
149 | throw new \RuntimeException(\openssl_error_string());
150 | }
151 |
152 | $publicKeyData = \openssl_pkey_get_details($resource);
153 |
154 | if (false === $publicKeyData) {
155 | throw new \RuntimeException(\openssl_error_string());
156 | }
157 |
158 | $publicKey = $publicKeyData['key'];
159 |
160 | return [$privateKey, $publicKey];
161 | }
162 |
163 | private function buildOpenSSLConfiguration(): array
164 | {
165 | $digestAlgorithms = [
166 | 'RS256' => 'sha256',
167 | 'RS384' => 'sha384',
168 | 'RS512' => 'sha512',
169 | 'HS256' => 'sha256',
170 | 'HS384' => 'sha384',
171 | 'HS512' => 'sha512',
172 | 'ES256' => 'sha256',
173 | 'ES384' => 'sha384',
174 | 'ES512' => 'sha512',
175 | ];
176 | $privateKeyBits = [
177 | 'RS256' => 2048,
178 | 'RS384' => 2048,
179 | 'RS512' => 4096,
180 | 'HS256' => 512,
181 | 'HS384' => 512,
182 | 'HS512' => 512,
183 | 'ES256' => 384,
184 | 'ES384' => 512,
185 | 'ES512' => 1024,
186 | ];
187 | $privateKeyTypes = [
188 | 'RS256' => \OPENSSL_KEYTYPE_RSA,
189 | 'RS384' => \OPENSSL_KEYTYPE_RSA,
190 | 'RS512' => \OPENSSL_KEYTYPE_RSA,
191 | 'HS256' => \OPENSSL_KEYTYPE_DH,
192 | 'HS384' => \OPENSSL_KEYTYPE_DH,
193 | 'HS512' => \OPENSSL_KEYTYPE_DH,
194 | 'ES256' => \OPENSSL_KEYTYPE_EC,
195 | 'ES384' => \OPENSSL_KEYTYPE_EC,
196 | 'ES512' => \OPENSSL_KEYTYPE_EC,
197 | ];
198 |
199 | $curves = [
200 | 'ES256' => 'secp256k1',
201 | 'ES384' => 'secp384r1',
202 | 'ES512' => 'secp521r1',
203 | ];
204 |
205 | $config = [
206 | 'digest_alg' => $digestAlgorithms[$this->algorithm],
207 | 'private_key_type' => $privateKeyTypes[$this->algorithm],
208 | 'private_key_bits' => $privateKeyBits[$this->algorithm],
209 | ];
210 |
211 | if (isset($curves[$this->algorithm])) {
212 | $config['curve_name'] = $curves[$this->algorithm];
213 | }
214 |
215 | return $config;
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/Command/GenerateTokenCommand.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | #[AsCommand(name: 'lexik:jwt:generate-token', description: 'Generates a JWT token for a given user.')]
20 | class GenerateTokenCommand extends Command
21 | {
22 | private JWTTokenManagerInterface $tokenManager;
23 |
24 | /** @var \Traversable */
25 | private \Traversable $userProviders;
26 |
27 | public function __construct(JWTTokenManagerInterface $tokenManager, \Traversable $userProviders)
28 | {
29 | $this->tokenManager = $tokenManager;
30 | $this->userProviders = $userProviders;
31 |
32 | parent::__construct();
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | protected function configure(): void
39 | {
40 | $this
41 | ->addArgument('username', InputArgument::REQUIRED, 'Username of user to be retreived from user provider')
42 | ->addOption('ttl', 't', InputOption::VALUE_REQUIRED, 'Ttl in seconds to be added to current time. If not provided, the ttl configured in the bundle will be used. Use 0 to generate token without exp')
43 | ->addOption('user-class', 'c', InputOption::VALUE_REQUIRED, 'Userclass is used to determine which user provider to use')
44 | ;
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | protected function execute(InputInterface $input, OutputInterface $output): int
51 | {
52 | if ($this->userProviders instanceof \Countable && 0 === \count($this->userProviders)) {
53 | throw new \RuntimeException('You must have at least 1 configured user provider to generate a token.');
54 | }
55 |
56 | if (!$userClass = $input->getOption('user-class')) {
57 | if (1 < \count($userProviders = iterator_to_array($this->userProviders))) {
58 | throw new \RuntimeException('The "--user-class" option must be passed as there is more than 1 configured user provider.');
59 | }
60 |
61 | $userProvider = current($userProviders);
62 | } else {
63 | $userProvider = null;
64 |
65 | foreach ($this->userProviders as $provider) {
66 | if ($provider->supportsClass($userClass)) {
67 | $userProvider = $provider;
68 |
69 | break;
70 | }
71 | }
72 |
73 | if (null === $userProvider) {
74 | throw new \RuntimeException(sprintf('There is no configured user provider for class "%s".', $userClass));
75 | }
76 | }
77 |
78 | $user = $userProvider->loadUserByIdentifier($input->getArgument('username'));
79 |
80 | $payload = [];
81 | if (null !== $input->getOption('ttl') && ((int) $input->getOption('ttl')) == 0) {
82 | $payload['exp'] = 0;
83 | } elseif (null !== $input->getOption('ttl') && ((int) $input->getOption('ttl')) > 0) {
84 | $payload['exp'] = time() + $input->getOption('ttl');
85 | }
86 |
87 | $token = $this->tokenManager->createFromPayload($user, $payload);
88 |
89 | $output->writeln([
90 | '',
91 | '' . $token . '',
92 | '',
93 | ]);
94 |
95 | return Command::SUCCESS;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Command/MigrateConfigCommand.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | #[AsCommand(name: 'lexik:jwt:migrate-config', description: 'Migrate LexikJWTAuthenticationBundle configuration to the Web-Token one.')]
30 | final class MigrateConfigCommand extends AbstractConfigCommand
31 | {
32 | /**
33 | * @deprecated
34 | */
35 | protected static $defaultName = 'lexik:jwt:migrate-config';
36 |
37 | /**
38 | * @var KeyLoaderInterface
39 | */
40 | private $keyLoader;
41 |
42 | /**
43 | * @var string
44 | */
45 | private $signatureAlgorithm;
46 |
47 | /**
48 | * @var string
49 | */
50 | private $passphrase;
51 |
52 | public function __construct(
53 | KeyLoaderInterface $keyLoader,
54 | string $passphrase,
55 | string $signatureAlgorithm
56 | ) {
57 | parent::__construct();
58 | $this->keyLoader = $keyLoader;
59 | $this->passphrase = $passphrase === '' ? null : $passphrase;
60 | $this->signatureAlgorithm = $signatureAlgorithm;
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | protected function configure(): void
67 | {
68 | $this
69 | ->setName(static::$defaultName)
70 | ->setDescription('Migrate the configuration to Web-Token')
71 | ;
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | *
77 | * @return int
78 | */
79 | protected function execute(InputInterface $input, OutputInterface $output): int
80 | {
81 | $this->checkRequirements();
82 | $io = new SymfonyStyle($input, $output);
83 | $io->title('Web-Token Migration tool');
84 | $io->info('This tool will help you converting the current LexikJWTAuthenticationBundle configuration to support Web-Token');
85 |
86 | try {
87 | $key = $this->getKey();
88 | $keyset = $this->getKeyset($key, $this->signatureAlgorithm);
89 | } catch (\RuntimeException $e) {
90 | $io->error('An error occurred: ' . $e->getMessage());
91 |
92 | return self::FAILURE;
93 | }
94 |
95 | $extension = $this->findExtension('lexik_jwt_authentication');
96 | $config = $this->getConfiguration($extension);
97 |
98 | foreach ($config['set_cookies'] as $cookieConfig) {
99 | if ($cookieConfig['split'] !== []) {
100 | $io->error('Web-Token is not compatible with the cookie split feature. Please disable this option before using this migration tool.');
101 |
102 | return self::FAILURE;
103 | }
104 | }
105 |
106 | $config['encoder'] = ['service' => 'lexik_jwt_authentication.encoder.web_token'];
107 | $config['access_token_issuance'] = [
108 | 'enabled' => true,
109 | 'signature' => [
110 | 'signature_algorithm' => $this->signatureAlgorithm,
111 | 'key' => json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
112 | ]
113 | ];
114 | $config['access_token_verification'] = [
115 | 'enabled' => true,
116 | 'signature' => [
117 | 'allowed_signature_algorithms' => [$this->signatureAlgorithm],
118 | 'keyset' => json_encode($keyset, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
119 | ]
120 | ];
121 |
122 | $io->comment('Please replace the current configuration with the following parameters.');
123 | $io->section('# config/packages/lexik_jwt_authentication.yaml');
124 | $io->writeln(Yaml::dump([$extension->getAlias() => $config], 10));
125 | $io->section('# End of file');
126 |
127 | return self::SUCCESS;
128 | }
129 |
130 | private function getKeyset(JWK $key, string $algorithm): JWKSet
131 | {
132 | $keyset = new JWKSet([$key->toPublic()]);
133 | switch ($key->get('kty')) {
134 | case 'oct':
135 | return $this->withOctKeys($keyset, $algorithm);
136 | case 'OKP':
137 | return $this->withOkpKeys($keyset, $algorithm, $key->get('crv'));
138 | case 'EC':
139 | return $this->withEcKeys($keyset, $algorithm, $key->get('crv'));
140 | case 'RSA':
141 | return $this->withRsaKeys($keyset, $algorithm);
142 | default:
143 | throw new \InvalidArgumentException('Unsupported key type.');
144 | }
145 | }
146 |
147 | private function withOctKeys(JWKSet $keyset, string $algorithm): JWKSet
148 | {
149 | $size = $this->getKeySize($algorithm);
150 |
151 | return $keyset
152 | ->with($this->createOctKey($size, $algorithm)->toPublic())
153 | ->with($this->createOctKey($size, $algorithm)->toPublic())
154 | ;
155 | }
156 |
157 | private function withRsaKeys(JWKSet $keyset, string $algorithm): JWKSet
158 | {
159 | return $keyset
160 | ->with($this->createRsaKey(2048, $algorithm)->toPublic())
161 | ->with($this->createRsaKey(2048, $algorithm)->toPublic())
162 | ;
163 | }
164 |
165 | private function withOkpKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
166 | {
167 | return $keyset
168 | ->with($this->createOkpKey($curve, $algorithm)->toPublic())
169 | ->with($this->createOkpKey($curve, $algorithm)->toPublic())
170 | ;
171 | }
172 |
173 | private function withEcKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
174 | {
175 | return $keyset
176 | ->with($this->createEcKey($curve, $algorithm)->toPublic())
177 | ->with($this->createEcKey($curve, $algorithm)->toPublic())
178 | ;
179 | }
180 |
181 | private function getKey(): JWK
182 | {
183 | $additionalValues = [
184 | 'use' => 'sig',
185 | 'alg' => $this->signatureAlgorithm,
186 | ];
187 | // No public key for HMAC
188 | if (false !== strpos($this->signatureAlgorithm, 'HS')) {
189 | return JWKFactory::createFromSecret(
190 | $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC),
191 | $additionalValues
192 | );
193 | }
194 | return JWKFactory::createFromKey(
195 | $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE),
196 | $this->passphrase,
197 | $additionalValues
198 | );
199 | }
200 |
201 | private function checkRequirements(): void
202 | {
203 | $requirements = [
204 | JoseFrameworkBundle::class => 'web-token/jwt-bundle',
205 | JWKFactory::class => 'web-token/jwt-key-mgmt',
206 | ClaimCheckerManager::class => 'web-token/jwt-checker',
207 | JWSBuilder::class => 'web-token/jwt-signature',
208 | ];
209 |
210 | foreach (array_keys($requirements) as $requirement) {
211 | if (!class_exists($requirement)) {
212 | throw new \RuntimeException(sprintf('The package "%s" is missing. Please install it for using this migration tool.', $requirement));
213 | }
214 | }
215 | }
216 | private function getConfiguration(ExtensionInterface $extension): array
217 | {
218 | $container = $this->compileContainer();
219 |
220 | $config = $this->getConfig($extension, $container);
221 | $uselessParameters = ['secret_key', 'public_key', 'pass_phrase', 'private_key_path', 'public_key_path', 'additional_public_keys'];
222 | foreach ($uselessParameters as $parameter) {
223 | unset($config[$parameter]);
224 | }
225 |
226 | return $config;
227 | }
228 |
229 | private function createOctKey(int $size, string $algorithm): JWK
230 | {
231 | return JWKFactory::createOctKey($size, $this->getOptions($algorithm));
232 | }
233 |
234 | private function createRsaKey(int $size, string $algorithm): JWK
235 | {
236 | return JWKFactory::createRSAKey($size, $this->getOptions($algorithm));
237 | }
238 |
239 | private function createOkpKey(string $curve, string $algorithm): JWK
240 | {
241 | return JWKFactory::createOKPKey($curve, $this->getOptions($algorithm));
242 | }
243 |
244 | private function createEcKey(string $curve, string $algorithm): JWK
245 | {
246 | return JWKFactory::createECKey($curve, $this->getOptions($algorithm));
247 | }
248 |
249 | private function compileContainer(): ContainerBuilder
250 | {
251 | $kernel = clone $this->getApplication()->getKernel();
252 | $kernel->boot();
253 |
254 | $method = new \ReflectionMethod($kernel, 'buildContainer');
255 | $container = $method->invoke($kernel);
256 | $container->getCompiler()->compile($container);
257 |
258 | return $container;
259 | }
260 |
261 | private function getConfig(ExtensionInterface $extension, ContainerBuilder $container)
262 | {
263 | return $container->resolveEnvPlaceholders(
264 | $container->getParameterBag()->resolveValue(
265 | $this->getConfigForExtension($extension, $container)
266 | )
267 | );
268 | }
269 |
270 | private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array
271 | {
272 | $extensionAlias = $extension->getAlias();
273 |
274 | $extensionConfig = [];
275 | foreach ($container->getCompilerPassConfig()->getPasses() as $pass) {
276 | if ($pass instanceof ValidateEnvPlaceholdersPass) {
277 | $extensionConfig = $pass->getExtensionConfig();
278 | break;
279 | }
280 | }
281 |
282 | if (isset($extensionConfig[$extensionAlias])) {
283 | return $extensionConfig[$extensionAlias];
284 | }
285 |
286 | // Fall back to default config if the extension has one
287 |
288 | if (!$extension instanceof ConfigurationExtensionInterface) {
289 | throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias));
290 | }
291 |
292 | $configs = $container->getExtensionConfig($extensionAlias);
293 | $configuration = $extension->getConfiguration($configs, $container);
294 | $this->validateConfiguration($extension, $configuration);
295 |
296 | return (new Processor())->processConfiguration($configuration, $configs);
297 | }
298 |
299 | private function getKeySize(string $algorithm): int
300 | {
301 | switch ($algorithm) {
302 | case 'HS256':
303 | case 'HS256/64':
304 | return 256;
305 | case 'HS384':
306 | return 384;
307 | case 'HS512':
308 | return 512;
309 | default:
310 | throw new \LogicException('Unsupported algorithm');
311 | }
312 | }
313 |
314 | private function getOptions(string $algorithm): array
315 | {
316 | return [
317 | 'use' => 'sig',
318 | 'alg' => $algorithm,
319 | 'kid' => Base64UrlSafe::encodeUnpadded(random_bytes(16))
320 | ];
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/ApiPlatformOpenApiPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('lexik_jwt_authentication.api_platform.openapi.factory') || !$container->hasParameter('security.firewalls')) {
13 | return;
14 | }
15 |
16 | $checkPath = null;
17 | $usernamePath = null;
18 | $passwordPath = null;
19 | $firewalls = $container->getParameter('security.firewalls');
20 | foreach ($firewalls as $firewallName) {
21 | if ($container->hasDefinition('security.authenticator.json_login.' . $firewallName)) {
22 | $firewallOptions = $container->getDefinition('security.authenticator.json_login.' . $firewallName)->getArgument(4);
23 | $checkPath = $firewallOptions['check_path'];
24 | $usernamePath = $firewallOptions['username_path'];
25 | $passwordPath = $firewallOptions['password_path'];
26 |
27 | break;
28 | }
29 | }
30 |
31 | $openApiFactoryDefinition = $container->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory');
32 | $checkPathArg = $openApiFactoryDefinition->getArgument(1);
33 | $usernamePathArg = $openApiFactoryDefinition->getArgument(2);
34 | $passwordPathArg = $openApiFactoryDefinition->getArgument(3);
35 |
36 | if (!$checkPath && !$checkPathArg) {
37 | $container->removeDefinition('lexik_jwt_authentication.api_platform.openapi.factory');
38 |
39 | return;
40 | }
41 |
42 | if (!$checkPathArg) {
43 | $openApiFactoryDefinition->replaceArgument(1, $checkPath);
44 | }
45 | if (!$usernamePathArg) {
46 | $openApiFactoryDefinition->replaceArgument(2, $usernamePath ?? 'username');
47 | }
48 | if (!$passwordPathArg) {
49 | $openApiFactoryDefinition->replaceArgument(3, $passwordPath ?? 'password');
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/CollectPayloadEnrichmentsPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('lexik_jwt_authentication.payload_enrichment')) {
16 | return;
17 | }
18 |
19 | $container->getDefinition('lexik_jwt_authentication.payload_enrichment')
20 | ->replaceArgument(0, $this->findAndSortTaggedServices('lexik_jwt_authentication.payload_enrichment', $container));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/WireGenerateTokenCommandPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('lexik_jwt_authentication.generate_token_command') || !$container->hasDefinition('security.context_listener')) {
13 | return;
14 | }
15 |
16 | $container
17 | ->getDefinition('lexik_jwt_authentication.generate_token_command')
18 | ->replaceArgument(1, $container->getDefinition('security.context_listener')->getArgument(1))
19 | ;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode()
25 | ->addDefaultsIfNotSet()
26 | ->children()
27 | ->scalarNode('public_key')
28 | ->info('The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key.')
29 | ->defaultNull()
30 | ->end()
31 | ->arrayNode('additional_public_keys')
32 | ->info('Multiple public keys to try to verify token signature. If none is given, it will use the key provided in "public_key".')
33 | ->scalarPrototype()->end()
34 | ->end()
35 | ->scalarNode('secret_key')
36 | ->info('The key used to sign tokens. It can be a raw secret (for HMAC), a raw RSA/ECDSA key or the path to a file itself being plaintext or PEM.')
37 | ->defaultNull()
38 | ->end()
39 | ->scalarNode('pass_phrase')
40 | ->info('The key passphrase (useless for HMAC)')
41 | ->defaultValue('')
42 | ->end()
43 | ->scalarNode('token_ttl')
44 | ->defaultValue(3600)
45 | ->end()
46 | ->booleanNode('allow_no_expiration')
47 | ->info('Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare.')
48 | ->defaultFalse()
49 | ->end()
50 | ->scalarNode('clock_skew')
51 | ->defaultValue(0)
52 | ->end()
53 | ->arrayNode('encoder')
54 | ->addDefaultsIfNotSet()
55 | ->children()
56 | ->scalarNode('service')
57 | ->defaultValue('lexik_jwt_authentication.encoder.lcobucci')
58 | ->end()
59 | ->scalarNode('signature_algorithm')
60 | ->defaultValue('RS256')
61 | ->cannotBeEmpty()
62 | ->end()
63 | ->end()
64 | ->end()
65 | ->scalarNode('user_id_claim')
66 | ->defaultValue('username')
67 | ->cannotBeEmpty()
68 | ->end()
69 | ->append($this->getTokenExtractorsNode())
70 | ->scalarNode('remove_token_from_body_when_cookies_used')
71 | ->defaultTrue()
72 | ->end()
73 | ->arrayNode('set_cookies')
74 | ->fixXmlConfig('set_cookie')
75 | ->normalizeKeys(false)
76 | ->useAttributeAsKey('name')
77 | ->prototype('array')
78 | ->children()
79 | ->scalarNode('lifetime')
80 | ->defaultNull()
81 | ->info('The cookie lifetime. If null, the "token_ttl" option value will be used')
82 | ->end()
83 | ->enumNode('samesite')
84 | ->values([Cookie::SAMESITE_NONE, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT])
85 | ->defaultValue(Cookie::SAMESITE_LAX)
86 | ->end()
87 | ->scalarNode('path')->defaultValue('/')->cannotBeEmpty()->end()
88 | ->scalarNode('domain')->defaultNull()->end()
89 | ->scalarNode('secure')->defaultTrue()->end()
90 | ->scalarNode('httpOnly')->defaultTrue()->end()
91 | ->scalarNode('partitioned')->defaultFalse()->end()
92 | ->arrayNode('split')
93 | ->scalarPrototype()->end()
94 | ->end()
95 | ->end()
96 | ->end()
97 | ->end()
98 | ->arrayNode('api_platform')
99 | ->canBeEnabled()
100 | ->info('API Platform compatibility: add check_path in OpenAPI documentation.')
101 | ->children()
102 | ->scalarNode('check_path')
103 | ->defaultNull()
104 | ->info('The login check path to add in OpenAPI.')
105 | ->end()
106 | ->scalarNode('username_path')
107 | ->defaultNull()
108 | ->info('The path to the username in the JSON body.')
109 | ->end()
110 | ->scalarNode('password_path')
111 | ->defaultNull()
112 | ->info('The path to the password in the JSON body.')
113 | ->end()
114 | ->end()
115 | ->end()
116 | ->arrayNode('access_token_issuance')
117 | ->fixXmlConfig('access_token_issuance')
118 | ->canBeEnabled()
119 | ->children()
120 | ->arrayNode('signature')
121 | ->fixXmlConfig('signature')
122 | ->addDefaultsIfNotSet()
123 | ->children()
124 | ->scalarNode('algorithm')
125 | ->isRequired()
126 | ->info('The algorithm use to sign the access tokens.')
127 | ->end()
128 | ->scalarNode('key')
129 | ->isRequired()
130 | ->info('The signature key. It shall be JWK encoded.')
131 | ->end()
132 | ->end()
133 | ->end()
134 | ->arrayNode('encryption')
135 | ->fixXmlConfig('encryption')
136 | ->canBeEnabled()
137 | ->children()
138 | ->scalarNode('key_encryption_algorithm')
139 | ->isRequired()
140 | ->cannotBeEmpty()
141 | ->info('The key encryption algorithm is used to encrypt the token.')
142 | ->end()
143 | ->scalarNode('content_encryption_algorithm')
144 | ->isRequired()
145 | ->cannotBeEmpty()
146 | ->info('The key encryption algorithm is used to encrypt the token.')
147 | ->end()
148 | ->scalarNode('key')
149 | ->isRequired()
150 | ->info('The encryption key. It shall be JWK encoded.')
151 | ->end()
152 | ->end()
153 | ->end()
154 | ->end()
155 | ->end()
156 | ->arrayNode('access_token_verification')
157 | ->fixXmlConfig('access_token_verification')
158 | ->canBeEnabled()
159 | ->children()
160 | ->arrayNode('signature')
161 | ->fixXmlConfig('signature')
162 | ->addDefaultsIfNotSet()
163 | ->children()
164 | ->arrayNode('header_checkers')
165 | ->fixXmlConfig('header_checkers')
166 | ->scalarPrototype()->end()
167 | ->defaultValue([])
168 | ->info('The headers to be checked for validating the JWS.')
169 | ->end()
170 | ->arrayNode('claim_checkers')
171 | ->fixXmlConfig('claim_checkers')
172 | ->scalarPrototype()->end()
173 | ->defaultValue(['exp_with_clock_skew', 'iat_with_clock_skew', 'nbf_with_clock_skew'])
174 | ->info('The claims to be checked for validating the JWS.')
175 | ->end()
176 | ->arrayNode('mandatory_claims')
177 | ->fixXmlConfig('mandatory_claims')
178 | ->scalarPrototype()->end()
179 | ->defaultValue([])
180 | ->info('The list of claims that shall be present in the JWS.')
181 | ->end()
182 | ->arrayNode('allowed_algorithms')
183 | ->fixXmlConfig('allowed_algorithms')
184 | ->scalarPrototype()->end()
185 | ->requiresAtLeastOneElement()
186 | ->info('The algorithms allowed to be used for token verification.')
187 | ->end()
188 | ->scalarNode('keyset')
189 | ->isRequired()
190 | ->info('The signature keyset. It shall be JWKSet encoded.')
191 | ->end()
192 | ->end()
193 | ->end()
194 | ->arrayNode('encryption')
195 | ->fixXmlConfig('encryption')
196 | ->canBeEnabled()
197 | ->children()
198 | ->booleanNode('continue_on_decryption_failure')
199 | ->defaultFalse()
200 | ->info('If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted.')
201 | ->end()
202 | ->arrayNode('header_checkers')
203 | ->fixXmlConfig('header_checkers')
204 | ->scalarPrototype()->end()
205 | ->defaultValue(['iat_with_clock_skew', 'nbf_with_clock_skew', 'exp_with_clock_skew'])
206 | ->info('The headers to be checked for validating the JWE.')
207 | ->end()
208 | ->arrayNode('allowed_key_encryption_algorithms')
209 | ->fixXmlConfig('allowed_key_encryption_algorithms')
210 | ->scalarPrototype()->end()
211 | ->requiresAtLeastOneElement()
212 | ->info('The key encryption algorithm is used to encrypt the token.')
213 | ->end()
214 | ->arrayNode('allowed_content_encryption_algorithms')
215 | ->fixXmlConfig('allowed_content_encryption_algorithms')
216 | ->scalarPrototype()->end()
217 | ->requiresAtLeastOneElement()
218 | ->info('The key encryption algorithm is used to encrypt the token.')
219 | ->end()
220 | ->scalarNode('keyset')
221 | ->isRequired()
222 | ->info('The encryption keyset. It shall be JWKSet encoded.')
223 | ->end()
224 | ->end()
225 | ->end()
226 | ->end()
227 | ->end()
228 | ->arrayNode('blocklist_token')
229 | ->addDefaultsIfNotSet()
230 | ->canBeEnabled()
231 | ->children()
232 | ->scalarNode('cache')
233 | ->defaultValue('cache.app')
234 | ->info('Storage to track blocked tokens')
235 | ->end()
236 | ->end()
237 | ->end()
238 | ->end()
239 | ->end();
240 |
241 | return $treeBuilder;
242 | }
243 |
244 | private function getTokenExtractorsNode(): ArrayNodeDefinition
245 | {
246 | $builder = new TreeBuilder('token_extractors');
247 | $node = $builder->getRootNode();
248 | $node
249 | ->addDefaultsIfNotSet()
250 | ->children()
251 | ->arrayNode('authorization_header')
252 | ->addDefaultsIfNotSet()
253 | ->canBeDisabled()
254 | ->children()
255 | ->scalarNode('prefix')
256 | ->defaultValue('Bearer')
257 | ->end()
258 | ->scalarNode('name')
259 | ->defaultValue('Authorization')
260 | ->end()
261 | ->end()
262 | ->end()
263 | ->arrayNode('cookie')
264 | ->addDefaultsIfNotSet()
265 | ->canBeEnabled()
266 | ->children()
267 | ->scalarNode('name')
268 | ->defaultValue('BEARER')
269 | ->end()
270 | ->end()
271 | ->end()
272 | ->arrayNode('query_parameter')
273 | ->addDefaultsIfNotSet()
274 | ->canBeEnabled()
275 | ->children()
276 | ->scalarNode('name')
277 | ->defaultValue('bearer')
278 | ->end()
279 | ->end()
280 | ->end()
281 | ->arrayNode('split_cookie')
282 | ->canBeEnabled()
283 | ->children()
284 | ->arrayNode('cookies')
285 | ->scalarPrototype()->end()
286 | ->end()
287 | ->end()
288 | ->end()
289 | ->end()
290 | ;
291 |
292 | return $node;
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/DependencyInjection/LexikJWTAuthenticationExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
35 |
36 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
37 |
38 | $loader->load('jwt_manager.xml');
39 | $loader->load('key_loader.xml');
40 | $loader->load('lcobucci.xml');
41 | $loader->load('response_interceptor.xml');
42 | $loader->load('token_authenticator.xml');
43 | $loader->load('token_extractor.xml');
44 |
45 | if (empty($config['public_key']) && empty($config['secret_key'])) {
46 | $e = new InvalidConfigurationException('You must either configure a "public_key" or a "secret_key".');
47 | $e->setPath('lexik_jwt_authentication');
48 |
49 | throw $e;
50 | }
51 |
52 | $container->setParameter('lexik_jwt_authentication.pass_phrase', $config['pass_phrase']);
53 | $container->setParameter('lexik_jwt_authentication.token_ttl', $config['token_ttl']);
54 | $container->setParameter('lexik_jwt_authentication.clock_skew', $config['clock_skew']);
55 | $container->setParameter('lexik_jwt_authentication.allow_no_expiration', $config['allow_no_expiration']);
56 | $container->setParameter('lexik_jwt_authentication.user_id_claim', $config['user_id_claim']);
57 |
58 | $encoderConfig = $config['encoder'];
59 |
60 | $container->setAlias('lexik_jwt_authentication.encoder', new Alias($encoderConfig['service'], true));
61 | $container->setAlias(JWTEncoderInterface::class, 'lexik_jwt_authentication.encoder');
62 | $container->setAlias(
63 | 'lexik_jwt_authentication.key_loader',
64 | new Alias('lexik_jwt_authentication.key_loader.raw', true)
65 | );
66 |
67 | $container
68 | ->findDefinition('lexik_jwt_authentication.key_loader')
69 | ->replaceArgument(0, $config['secret_key'])
70 | ->replaceArgument(1, $config['public_key']);
71 |
72 | if (isset($config['additional_public_keys'])) {
73 | $container
74 | ->findDefinition('lexik_jwt_authentication.key_loader')
75 | ->replaceArgument(3, $config['additional_public_keys']);
76 | }
77 |
78 | $container->setParameter('lexik_jwt_authentication.encoder.signature_algorithm', $encoderConfig['signature_algorithm']);
79 |
80 | $tokenExtractors = $this->createTokenExtractors($container, $config['token_extractors']);
81 | $container
82 | ->getDefinition('lexik_jwt_authentication.extractor.chain_extractor')
83 | ->replaceArgument(0, $tokenExtractors);
84 |
85 | if (isset($config['remove_token_from_body_when_cookies_used'])) {
86 | $container
87 | ->getDefinition('lexik_jwt_authentication.handler.authentication_success')
88 | ->replaceArgument(3, $config['remove_token_from_body_when_cookies_used']);
89 | }
90 |
91 | if ($config['set_cookies']) {
92 | $loader->load('cookie.xml');
93 |
94 | $cookieProviders = [];
95 | foreach ($config['set_cookies'] as $name => $attributes) {
96 | if ($attributes['partitioned'] && Kernel::VERSION < '6.4') {
97 | throw new \LogicException(sprintf('The `partitioned` option for cookies is only available for Symfony 6.4 and above. You are currently on version %s', Kernel::VERSION));
98 | }
99 |
100 | $container
101 | ->setDefinition($id = "lexik_jwt_authentication.cookie_provider.$name", new ChildDefinition('lexik_jwt_authentication.cookie_provider'))
102 | ->replaceArgument(0, $name)
103 | ->replaceArgument(1, $attributes['lifetime'] ?? ($config['token_ttl'] ?: 0))
104 | ->replaceArgument(2, $attributes['samesite'])
105 | ->replaceArgument(3, $attributes['path'])
106 | ->replaceArgument(4, $attributes['domain'])
107 | ->replaceArgument(5, $attributes['secure'])
108 | ->replaceArgument(6, $attributes['httpOnly'])
109 | ->replaceArgument(7, $attributes['split'])
110 | ->replaceArgument(8, $attributes['partitioned']);
111 | $cookieProviders[] = new Reference($id);
112 | }
113 |
114 | $container
115 | ->getDefinition('lexik_jwt_authentication.handler.authentication_success')
116 | ->replaceArgument(2, new IteratorArgument($cookieProviders));
117 | }
118 |
119 | if (class_exists(Application::class)) {
120 | $loader->load('console.xml');
121 |
122 | $container
123 | ->getDefinition('lexik_jwt_authentication.generate_keypair_command')
124 | ->replaceArgument(1, $config['secret_key'])
125 | ->replaceArgument(2, $config['public_key'])
126 | ->replaceArgument(3, $config['pass_phrase'])
127 | ->replaceArgument(4, $encoderConfig['signature_algorithm']);
128 | if (!$container->hasParameter('kernel.debug') || !$container->getParameter('kernel.debug')) {
129 | $container->removeDefinition('lexik_jwt_authentication.migrate_config_command');
130 | }
131 | }
132 |
133 | if ($this->isConfigEnabled($container, $config['api_platform'])) {
134 | if (!class_exists(ApiPlatformBundle::class)) {
135 | throw new LogicException('API Platform cannot be detected. Try running "composer require api-platform/core".');
136 | }
137 |
138 | $loader->load('api_platform.xml');
139 |
140 | $container
141 | ->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory')
142 | ->replaceArgument(1, $config['api_platform']['check_path'] ?? null)
143 | ->replaceArgument(2, $config['api_platform']['username_path'] ?? null)
144 | ->replaceArgument(3, $config['api_platform']['password_path'] ?? null);
145 | }
146 |
147 | $this->processWithWebTokenConfig($config, $container, $loader);
148 |
149 | if ($this->isConfigEnabled($container, $config['blocklist_token'])) {
150 | $loader->load('blocklist_token.xml');
151 | $blockListTokenConfig = $config['blocklist_token'];
152 | $container->setAlias('lexik_jwt_authentication.blocklist_token.cache', $blockListTokenConfig['cache']);
153 | } else {
154 | $container->getDefinition('lexik_jwt_authentication.payload_enrichment.random_jti_enrichment')
155 | ->clearTag('lexik_jwt_authentication.payload_enrichment');
156 | }
157 | }
158 |
159 | private function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig): array
160 | {
161 | $map = [];
162 |
163 | if ($this->isConfigEnabled($container, $tokenExtractorsConfig['authorization_header'])) {
164 | $authorizationHeaderExtractorId = 'lexik_jwt_authentication.extractor.authorization_header_extractor';
165 | $container
166 | ->getDefinition($authorizationHeaderExtractorId)
167 | ->replaceArgument(0, $tokenExtractorsConfig['authorization_header']['prefix'])
168 | ->replaceArgument(1, $tokenExtractorsConfig['authorization_header']['name']);
169 |
170 | $map[] = new Reference($authorizationHeaderExtractorId);
171 | }
172 |
173 | if ($this->isConfigEnabled($container, $tokenExtractorsConfig['query_parameter'])) {
174 | $queryParameterExtractorId = 'lexik_jwt_authentication.extractor.query_parameter_extractor';
175 | $container
176 | ->getDefinition($queryParameterExtractorId)
177 | ->replaceArgument(0, $tokenExtractorsConfig['query_parameter']['name']);
178 |
179 | $map[] = new Reference($queryParameterExtractorId);
180 | }
181 |
182 | if ($this->isConfigEnabled($container, $tokenExtractorsConfig['cookie'])) {
183 | $cookieExtractorId = 'lexik_jwt_authentication.extractor.cookie_extractor';
184 | $container
185 | ->getDefinition($cookieExtractorId)
186 | ->replaceArgument(0, $tokenExtractorsConfig['cookie']['name']);
187 |
188 | $map[] = new Reference($cookieExtractorId);
189 | }
190 |
191 | if ($this->isConfigEnabled($container, $tokenExtractorsConfig['split_cookie'])) {
192 | $cookieExtractorId = 'lexik_jwt_authentication.extractor.split_cookie_extractor';
193 | $container
194 | ->getDefinition($cookieExtractorId)
195 | ->replaceArgument(0, $tokenExtractorsConfig['split_cookie']['cookies']);
196 |
197 | $map[] = new Reference($cookieExtractorId);
198 | }
199 |
200 | return $map;
201 | }
202 |
203 | private function processWithWebTokenConfig(array $config, ContainerBuilder $container, LoaderInterface $loader): void
204 | {
205 | if ($config['access_token_issuance']['enabled'] === false && $config['access_token_verification']['enabled'] === false) {
206 | return;
207 | }
208 | $loader->load('web_token.xml');
209 | if ($config['access_token_issuance']['enabled'] === true) {
210 | $loader->load('web_token_issuance.xml');
211 | $accessTokenBuilder = 'lexik_jwt_authentication.access_token_builder';
212 | $accessTokenBuilderDefinition = $container->getDefinition($accessTokenBuilder);
213 | $accessTokenBuilderDefinition
214 | ->replaceArgument(3, $config['access_token_issuance']['signature']['algorithm'])
215 | ->replaceArgument(4, $config['access_token_issuance']['signature']['key'])
216 | ;
217 | if ($config['access_token_issuance']['encryption']['enabled'] === true) {
218 | $accessTokenBuilderDefinition
219 | ->replaceArgument(5, $config['access_token_issuance']['encryption']['key_encryption_algorithm'])
220 | ->replaceArgument(6, $config['access_token_issuance']['encryption']['content_encryption_algorithm'])
221 | ->replaceArgument(7, $config['access_token_issuance']['encryption']['key'])
222 | ;
223 | }
224 | }
225 | if ($config['access_token_verification']['enabled'] === true) {
226 | $loader->load('web_token_verification.xml');
227 | $accessTokenLoader = 'lexik_jwt_authentication.access_token_loader';
228 | $accessTokenLoaderDefinition = $container->getDefinition($accessTokenLoader);
229 | $accessTokenLoaderDefinition
230 | ->replaceArgument(3, $config['access_token_verification']['signature']['claim_checkers'])
231 | ->replaceArgument(4, $config['access_token_verification']['signature']['header_checkers'])
232 | ->replaceArgument(5, $config['access_token_verification']['signature']['mandatory_claims'])
233 | ->replaceArgument(6, $config['access_token_verification']['signature']['allowed_algorithms'])
234 | ->replaceArgument(7, $config['access_token_verification']['signature']['keyset'])
235 | ;
236 | if ($config['access_token_verification']['encryption']['enabled'] === true) {
237 | $accessTokenLoaderDefinition
238 | ->replaceArgument(8, $config['access_token_verification']['encryption']['continue_on_decryption_failure'])
239 | ->replaceArgument(9, $config['access_token_verification']['encryption']['header_checkers'])
240 | ->replaceArgument(10, $config['access_token_verification']['encryption']['allowed_key_encryption_algorithms'])
241 | ->replaceArgument(11, $config['access_token_verification']['encryption']['allowed_content_encryption_algorithms'])
242 | ->replaceArgument(12, $config['access_token_verification']['encryption']['keyset'])
243 | ;
244 | }
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/DependencyInjection/Security/Factory/JWTAuthenticatorFactory.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class JWTAuthenticatorFactory implements AuthenticatorFactoryInterface
17 | {
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getPriority(): int
22 | {
23 | return -10;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getKey(): string
30 | {
31 | return 'jwt';
32 | }
33 |
34 | public function addConfiguration(NodeDefinition $node): void
35 | {
36 | $node
37 | ->children()
38 | ->scalarNode('provider')
39 | ->defaultNull()
40 | ->end()
41 | ->scalarNode('authenticator')
42 | ->defaultValue('lexik_jwt_authentication.security.jwt_authenticator')
43 | ->end()
44 | ->end()
45 | ;
46 | }
47 |
48 | public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
49 | {
50 | $authenticatorId = 'security.authenticator.jwt.' . $firewallName;
51 |
52 | $userProviderId = empty($config['provider']) ? $userProviderId : 'security.user.provider.concrete.' . $config['provider'];
53 |
54 | $container
55 | ->setDefinition($authenticatorId, new ChildDefinition($config['authenticator']))
56 | ->replaceArgument(3, new Reference($userProviderId))
57 | ;
58 |
59 | return $authenticatorId;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/DependencyInjection/Security/Factory/JWTUserFactory.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | final class JWTUserFactory implements UserProviderFactoryInterface
20 | {
21 | public function create(ContainerBuilder $container, string $id, array $config): void
22 | {
23 | $container->setDefinition($id, new ChildDefinition('lexik_jwt_authentication.security.jwt_user_provider'))
24 | ->replaceArgument(0, $config['class']);
25 | }
26 |
27 | public function getKey(): string
28 | {
29 | return 'lexik_jwt';
30 | }
31 |
32 | public function addConfiguration(NodeDefinition $node): void
33 | {
34 | $node
35 | ->children()
36 | ->scalarNode('class')
37 | ->cannotBeEmpty()
38 | ->defaultValue(JWTUser::class)
39 | ->validate()
40 | ->ifTrue(fn ($class) => !is_subclass_of($class, JWTUserInterface::class))
41 | ->thenInvalid('The %s class must implement ' . JWTUserInterface::class . ' for using the "lexik_jwt" user provider.')
42 | ->end()
43 | ->end()
44 | ->end()
45 | ;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Encoder/HeaderAwareJWTEncoderInterface.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | interface JWTEncoderInterface
14 | {
15 | /**
16 | * @return string the encoded token string
17 | *
18 | * @throws JWTEncodeFailureException If an error occurred while trying to create
19 | * the token (invalid crypto key, invalid payload...)
20 | */
21 | public function encode(array $data);
22 |
23 | /**
24 | * @param string $token
25 | *
26 | * @return array
27 | *
28 | * @throws JWTDecodeFailureException If an error occurred while trying to load the token
29 | * (invalid signature, invalid crypto key, expired token...)
30 | */
31 | public function decode($token);
32 | }
33 |
--------------------------------------------------------------------------------
/Encoder/LcobucciJWTEncoder.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class LcobucciJWTEncoder implements JWTEncoderInterface, HeaderAwareJWTEncoderInterface
15 | {
16 | protected JWSProviderInterface $jwsProvider;
17 |
18 | public function __construct(JWSProviderInterface $jwsProvider)
19 | {
20 | $this->jwsProvider = $jwsProvider;
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function encode(array $payload, array $header = [])
27 | {
28 | try {
29 | $jws = $this->jwsProvider->create($payload, $header);
30 | } catch (\InvalidArgumentException $e) {
31 | throw new JWTEncodeFailureException(JWTEncodeFailureException::INVALID_CONFIG, 'An error occurred while trying to encode the JWT token. Please verify your configuration (private key/passphrase)', $e, $payload);
32 | }
33 |
34 | if (!$jws->isSigned()) {
35 | throw new JWTEncodeFailureException(JWTEncodeFailureException::UNSIGNED_TOKEN, 'Unable to create a signed JWT from the given configuration.', null, $payload);
36 | }
37 |
38 | return $jws->getToken();
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function decode($token)
45 | {
46 | try {
47 | $jws = $this->jwsProvider->load($token);
48 | } catch (\Exception $e) {
49 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', $e);
50 | }
51 |
52 | if ($jws->isInvalid()) {
53 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', null, $jws->getPayload());
54 | }
55 |
56 | if ($jws->isExpired()) {
57 | throw new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token', null, $jws->getPayload());
58 | }
59 |
60 | if (!$jws->isVerified()) {
61 | throw new JWTDecodeFailureException(JWTDecodeFailureException::UNVERIFIED_TOKEN, 'Unable to verify the given JWT through the given configuration. If the "lexik_jwt_authentication.encoder" encryption options have been changed since your last authentication, please renew the token. If the problem persists, verify that the configured keys/passphrase are valid.', null, $jws->getPayload());
62 | }
63 |
64 | return $jws->getPayload();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Encoder/WebTokenEncoder.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | final class WebTokenEncoder implements HeaderAwareJWTEncoderInterface
17 | {
18 | /**
19 | * @var AccessTokenBuilder|null
20 | */
21 | private $accessTokenBuilder;
22 |
23 | /**
24 | * @var AccessTokenLoader|null
25 | */
26 | private $accessTokenLoader;
27 |
28 | public function __construct(?AccessTokenBuilder $accessTokenBuilder, ?AccessTokenLoader $accessTokenLoader)
29 | {
30 | $this->accessTokenBuilder = $accessTokenBuilder;
31 | $this->accessTokenLoader = $accessTokenLoader;
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function encode(array $payload, array $header = [])
38 | {
39 | if (!$this->accessTokenBuilder) {
40 | throw new \LogicException('The access token issuance features are not enabled.');
41 | }
42 |
43 | try {
44 | return $this->accessTokenBuilder->build($header, $payload);
45 | } catch (\InvalidArgumentException $e) {
46 | throw new JWTEncodeFailureException(JWTEncodeFailureException::INVALID_CONFIG, 'An error occurred while trying to encode the JWT token. Please verify your configuration (private key/passphrase)', $e, $payload);
47 | }
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | public function decode($token)
54 | {
55 | if (!$this->accessTokenLoader) {
56 | throw new \LogicException('The access token verification features are not enabled.');
57 | }
58 |
59 | try {
60 | return $this->accessTokenLoader->load($token);
61 | } catch (JWTFailureException $e) {
62 | throw $e;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Event/AuthenticationFailureEvent.php:
--------------------------------------------------------------------------------
1 |
14 | * @author Robin Chalas
15 | */
16 | class AuthenticationFailureEvent extends Event
17 | {
18 | protected AuthenticationException $exception;
19 | protected ?Response $response;
20 | protected ?Request $request;
21 |
22 | public function __construct(?AuthenticationException $exception, ?Response $response, ?Request $request = null)
23 | {
24 | $this->exception = $exception;
25 | $this->response = $response;
26 | $this->request = $request;
27 | }
28 |
29 | public function getException(): AuthenticationException
30 | {
31 | return $this->exception;
32 | }
33 |
34 | public function getResponse(): ?Response
35 | {
36 | return $this->response;
37 | }
38 |
39 | public function setResponse(Response $response): void
40 | {
41 | $this->response = $response;
42 | }
43 |
44 | public function getRequest(): ?Request
45 | {
46 | return $this->request;
47 | }
48 |
49 | public function setRequest(Request $request)
50 | {
51 | $this->request = $request;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Event/AuthenticationSuccessEvent.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class AuthenticationSuccessEvent extends Event
15 | {
16 | protected array $data;
17 | protected UserInterface $user;
18 | protected Response $response;
19 |
20 | public function __construct(array $data, UserInterface $user, Response $response)
21 | {
22 | $this->data = $data;
23 | $this->user = $user;
24 | $this->response = $response;
25 | }
26 |
27 | public function getData(): array
28 | {
29 | return $this->data;
30 | }
31 |
32 | public function setData(array $data): void
33 | {
34 | $this->data = $data;
35 | }
36 |
37 | public function getUser(): UserInterface
38 | {
39 | return $this->user;
40 | }
41 |
42 | public function getResponse(): Response
43 | {
44 | return $this->response;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Event/BeforeJWEComputationEvent.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class BeforeJWEComputationEvent
12 | {
13 | private $header;
14 |
15 | /**
16 | * @param array $header
17 | */
18 | public function __construct(array $header)
19 | {
20 | $this->header = $header;
21 | }
22 |
23 | public function setHeader(string $key, mixed $value): self
24 | {
25 | $this->header[$key] = $value;
26 |
27 | return $this;
28 | }
29 |
30 | public function removeHeader(string $key): self
31 | {
32 | unset($this->header[$key]);
33 |
34 | return $this;
35 | }
36 |
37 | /**
38 | * @return array
39 | */
40 | public function getHeader(): array
41 | {
42 | return $this->header;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Event/JWTAuthenticatedEvent.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
19 | $this->token = $token;
20 | }
21 |
22 | public function getPayload(): array
23 | {
24 | return $this->payload;
25 | }
26 |
27 | public function setPayload(array $payload)
28 | {
29 | $this->payload = $payload;
30 | }
31 |
32 | public function getToken(): TokenInterface
33 | {
34 | return $this->token;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Event/JWTCreatedEvent.php:
--------------------------------------------------------------------------------
1 | data = $data;
20 | $this->user = $user;
21 | $this->header = $header;
22 | }
23 |
24 | public function getHeader(): array
25 | {
26 | return $this->header;
27 | }
28 |
29 | public function setHeader(array $header)
30 | {
31 | $this->header = $header;
32 | }
33 |
34 | public function getData(): array
35 | {
36 | return $this->data;
37 | }
38 |
39 | public function setData(array $data)
40 | {
41 | $this->data = $data;
42 | }
43 |
44 | public function getUser(): UserInterface
45 | {
46 | return $this->user;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Event/JWTDecodedEvent.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class JWTDecodedEvent extends Event
13 | {
14 | protected array $payload;
15 | protected bool $isValid;
16 |
17 | public function __construct(array $payload)
18 | {
19 | $this->payload = $payload;
20 | $this->isValid = true;
21 | }
22 |
23 | public function getPayload(): array
24 | {
25 | return $this->payload;
26 | }
27 |
28 | public function setPayload(array $payload)
29 | {
30 | $this->payload = $payload;
31 | }
32 |
33 | /**
34 | * Mark payload as invalid.
35 | */
36 | public function markAsInvalid(): void
37 | {
38 | $this->isValid = false;
39 | $this->stopPropagation();
40 | }
41 |
42 | public function isValid(): bool
43 | {
44 | return $this->isValid;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Event/JWTEncodedEvent.php:
--------------------------------------------------------------------------------
1 | jwtString = $jwtString;
14 | }
15 |
16 | public function getJWTString(): string
17 | {
18 | return $this->jwtString;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Event/JWTExpiredEvent.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class JWTExpiredEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/Event/JWTFailureEventInterface.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | interface JWTFailureEventInterface
14 | {
15 | /**
16 | * Gets the response that will be returned after dispatching a
17 | * {@link JWTFailureEventInterface} implementation.
18 | *
19 | * @return Response
20 | */
21 | public function getResponse();
22 |
23 | /**
24 | * Gets the tied AuthenticationException object.
25 | *
26 | * @return AuthenticationException
27 | */
28 | public function getException();
29 |
30 | /**
31 | * Calling this allows to return a custom Response immediately after
32 | * the corresponding implementation of this event is dispatched.
33 | */
34 | public function setResponse(Response $response);
35 | }
36 |
--------------------------------------------------------------------------------
/Event/JWTInvalidEvent.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class JWTInvalidEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/Event/JWTNotFoundEvent.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class JWTNotFoundEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
16 | {
17 | public function __construct(?AuthenticationException $exception = null, ?Response $response = null, ?Request $request = null)
18 | {
19 | parent::__construct($exception, $response, $request);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/EventListener/BlockJWTListener.php:
--------------------------------------------------------------------------------
1 | blockedTokenManager = $blockedTokenManager;
28 | $this->tokenExtractor = $tokenExtractor;
29 | $this->jwtManager = $jwtManager;
30 | }
31 |
32 | public function onLoginFailure(LoginFailureEvent $event): void
33 | {
34 | $exception = $event->getException();
35 | if (($exception instanceof DisabledException) || ($exception->getPrevious() instanceof DisabledException)) {
36 | $this->blockTokenFromRequest($event->getRequest());
37 | }
38 | }
39 |
40 | public function onLogout(LogoutEvent $event): void
41 | {
42 | $this->blockTokenFromRequest($event->getRequest());
43 | }
44 |
45 | private function blockTokenFromRequest(Request $request): void
46 | {
47 | $token = $this->tokenExtractor->extract($request);
48 |
49 | if ($token === false) {
50 | // There's nothing to block if the token isn't in the request
51 | return;
52 | }
53 |
54 | try {
55 | $payload = $this->jwtManager->parse($token);
56 | } catch (JWTDecodeFailureException $e) {
57 | // Ignore decode failures, this would mean the token is invalid anyway
58 | return;
59 | }
60 |
61 | try {
62 | $this->blockedTokenManager->add($payload);
63 | } catch (MissingClaimException $e) {
64 | // We can't block a token missing the claims our system requires, so silently ignore this one
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/EventListener/RejectBlockedTokenListener.php:
--------------------------------------------------------------------------------
1 | blockedTokenManager = $blockedTokenManager;
17 | }
18 |
19 | /**
20 | * @throws InvalidTokenException If the JWT is blocked
21 | */
22 | public function __invoke(JWTAuthenticatedEvent $event): void
23 | {
24 | try {
25 | if ($this->blockedTokenManager->has($event->getPayload())) {
26 | throw new InvalidTokenException('JWT blocked');
27 | }
28 | } catch (MissingClaimException) {
29 | // Do nothing if the required claims do not exist on the payload (older JWTs won't have the "jti" claim the manager requires)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Events.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class Events
11 | {
12 | /**
13 | * Dispatched after the token generation to allow sending more data
14 | * on the authentication success response.
15 | *
16 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent")
17 | */
18 | public const AUTHENTICATION_SUCCESS = 'lexik_jwt_authentication.on_authentication_success';
19 |
20 | /**
21 | * Dispatched after an authentication failure.
22 | * Hook into this event to add a custom error message in the response body.
23 | *
24 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent")
25 | */
26 | public const AUTHENTICATION_FAILURE = 'lexik_jwt_authentication.on_authentication_failure';
27 |
28 | /**
29 | * Dispatched before the token payload is encoded by the configured encoder (JWTEncoder by default).
30 | * Hook into this event to add extra fields to the payload.
31 | *
32 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent")
33 | */
34 | public const JWT_CREATED = 'lexik_jwt_authentication.on_jwt_created';
35 |
36 | /**
37 | * Dispatched right after token string is created.
38 | * Hook into this event to get token representation itself.
39 | *
40 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent")
41 | */
42 | public const JWT_ENCODED = 'lexik_jwt_authentication.on_jwt_encoded';
43 |
44 | /**
45 | * Dispatched after the token payload has been decoded by the configured encoder (JWTEncoder by default).
46 | * Hook into this event to perform additional validation on the received payload.
47 | *
48 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent")
49 | */
50 | public const JWT_DECODED = 'lexik_jwt_authentication.on_jwt_decoded';
51 |
52 | /**
53 | * Dispatched after the token payload has been authenticated by the provider.
54 | * Hook into this event to perform additional modification to the authenticated token using the payload.
55 | *
56 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent")
57 | */
58 | public const JWT_AUTHENTICATED = 'lexik_jwt_authentication.on_jwt_authenticated';
59 |
60 | /**
61 | * Dispatched after the token has been invalidated by the provider.
62 | * Hook into this event to add a custom error message in the response body.
63 | *
64 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent")
65 | */
66 | public const JWT_INVALID = 'lexik_jwt_authentication.on_jwt_invalid';
67 |
68 | /**
69 | * Dispatched when no token can be found in a request.
70 | * Hook into this event to set a custom response.
71 | *
72 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent")
73 | */
74 | public const JWT_NOT_FOUND = 'lexik_jwt_authentication.on_jwt_not_found';
75 |
76 | /**
77 | * Dispatched when the token is expired.
78 | * The expired token's payload can be retrieved by hooking into this event, so you can set a different
79 | * response.
80 | *
81 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent")
82 | */
83 | public const JWT_EXPIRED = 'lexik_jwt_authentication.on_jwt_expired';
84 |
85 | /**
86 | * Dispatched before the JWE is computed.
87 | * This event allow the JWE header parameters to be changed.
88 | * It is only dispatched when using Web-Token
89 | *
90 | * @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\BeforeJWEComputationEvent")
91 | */
92 | public const BEFORE_JWE_COMPUTATION = 'lexik_jwt_authentication.before_jwe_computation';
93 | }
94 |
--------------------------------------------------------------------------------
/Exception/ExpiredTokenException.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class ExpiredTokenException extends AuthenticationException
14 | {
15 | /**
16 | * {@inheritdoc}
17 | */
18 | public function getMessageKey(): string
19 | {
20 | return 'Expired JWT Token';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Exception/InvalidPayloadException.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class InvalidPayloadException extends AuthenticationException
13 | {
14 | private string $invalidKey;
15 |
16 | /**
17 | * @param string $invalidKey The key that cannot be found in the payload
18 | */
19 | public function __construct(string $invalidKey)
20 | {
21 | $this->invalidKey = $invalidKey;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function getMessageKey(): string
28 | {
29 | return sprintf('Unable to find key "%s" in the token payload.', $this->invalidKey);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Exception/InvalidTokenException.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class InvalidTokenException extends AuthenticationException
13 | {
14 | /**
15 | * {@inheritdoc}
16 | */
17 | public function getMessageKey(): string
18 | {
19 | return 'Invalid JWT Token';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Exception/JWTDecodeFailureException.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class JWTDecodeFailureException extends JWTFailureException
11 | {
12 | public const INVALID_TOKEN = 'invalid_token';
13 |
14 | public const UNVERIFIED_TOKEN = 'unverified_token';
15 |
16 | public const EXPIRED_TOKEN = 'expired_token';
17 | }
18 |
--------------------------------------------------------------------------------
/Exception/JWTEncodeFailureException.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class JWTEncodeFailureException extends JWTFailureException
11 | {
12 | public const INVALID_CONFIG = 'invalid_config';
13 |
14 | public const UNSIGNED_TOKEN = 'unsigned_token';
15 | }
16 |
--------------------------------------------------------------------------------
/Exception/JWTFailureException.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class JWTFailureException extends \Exception
11 | {
12 | private string $reason;
13 | private ?array $payload;
14 |
15 | public function __construct(string $reason, string $message, ?\Throwable $previous = null, ?array $payload = null)
16 | {
17 | $this->reason = $reason;
18 | $this->payload = $payload;
19 |
20 | parent::__construct($message, 0, $previous);
21 | }
22 |
23 | public function getReason(): string
24 | {
25 | return $this->reason;
26 | }
27 |
28 | public function getPayload(): ?array
29 | {
30 | return $this->payload;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Exception/MissingClaimException.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class MissingTokenException extends AuthenticationException
13 | {
14 | /**
15 | * {@inheritdoc}
16 | */
17 | public function getMessageKey(): string
18 | {
19 | return 'JWT Token not found';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Exception/UserNotFoundException.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class UserNotFoundException extends AuthenticationException
13 | {
14 | private string $userIdentityField;
15 | private string $identity;
16 |
17 | public function __construct(string $userIdentityField, string $identity)
18 | {
19 | $this->userIdentityField = $userIdentityField;
20 | $this->identity = $identity;
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function getMessageKey(): string
27 | {
28 | return sprintf('Unable to load an user with property "%s" = "%s". If the user identity has changed, you must renew the token. Otherwise, verify that the "lexik_jwt_authentication.user_identity_field" config option is correctly set.', $this->userIdentityField, $this->identity);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Helper/JWTSplitter.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * @final
11 | */
12 | class JWTSplitter
13 | {
14 | private string $header;
15 | private string $payload;
16 | private string $signature;
17 |
18 | /**
19 | * @var string
20 | */
21 | private $jwt;
22 |
23 | public function __construct(string $jwt)
24 | {
25 | $this->jwt = $jwt;
26 | [$this->header, $this->payload, $this->signature] = explode('.', $jwt);
27 | }
28 |
29 | public function getParts(array $parts = []): string
30 | {
31 | if (!$parts) {
32 | return $this->jwt;
33 | }
34 |
35 | return implode('.', array_intersect_key(get_object_vars($this), array_flip($parts)));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2014-2020 Lexik , Robin Chalas
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/LexikJWTAuthenticationBundle.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class LexikJWTAuthenticationBundle extends Bundle
21 | {
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public function build(ContainerBuilder $container): void
26 | {
27 | parent::build($container);
28 |
29 | $container->addCompilerPass(new WireGenerateTokenCommandPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
30 | $container->addCompilerPass(new ApiPlatformOpenApiPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
31 | $container->addCompilerPass(new CollectPayloadEnrichmentsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
32 |
33 | /** @var SecurityExtension $extension */
34 | $extension = $container->getExtension('security');
35 |
36 | $extension->addUserProviderFactory(new JWTUserFactory());
37 |
38 | $extension->addAuthenticatorFactory(new JWTAuthenticatorFactory());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/OpenApi/OpenApiFactory.php:
--------------------------------------------------------------------------------
1 |
17 | *
18 | * @final
19 | */
20 | class OpenApiFactory implements OpenApiFactoryInterface
21 | {
22 | private OpenApiFactoryInterface $decorated;
23 | private string $checkPath;
24 | private string $usernamePath;
25 | private string $passwordPath;
26 |
27 | public function __construct(OpenApiFactoryInterface $decorated, string $checkPath, string $usernamePath, string $passwordPath)
28 | {
29 | $this->decorated = $decorated;
30 | $this->checkPath = $checkPath;
31 | $this->usernamePath = $usernamePath;
32 | $this->passwordPath = $passwordPath;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function __invoke(array $context = []): OpenApi
39 | {
40 | $openApi = ($this->decorated)($context);
41 |
42 | $openApi
43 | ->getComponents()->getSecuritySchemes()->offsetSet(
44 | 'JWT',
45 | new \ArrayObject(
46 | [
47 | 'type' => 'http',
48 | 'scheme' => 'bearer',
49 | 'bearerFormat' => 'JWT',
50 | ]
51 | )
52 | );
53 |
54 | $openApi
55 | ->getPaths()
56 | ->addPath($this->checkPath, (new PathItem())->withPost(
57 | (new Operation())
58 | ->withOperationId('login_check_post')
59 | ->withTags(['Login Check'])
60 | ->withResponses([
61 | Response::HTTP_OK => [
62 | 'description' => 'User token created',
63 | 'content' => [
64 | 'application/json' => [
65 | 'schema' => [
66 | 'type' => 'object',
67 | 'properties' => [
68 | 'token' => [
69 | 'readOnly' => true,
70 | 'type' => 'string',
71 | 'nullable' => false,
72 | ],
73 | ],
74 | 'required' => ['token'],
75 | ],
76 | ],
77 | ],
78 | ],
79 | ])
80 | ->withSummary('Creates a user token.')
81 | ->withDescription('Creates a user token.')
82 | ->withRequestBody(
83 | (new RequestBody())
84 | ->withDescription('The login data')
85 | ->withContent(new \ArrayObject([
86 | 'application/json' => new MediaType(new \ArrayObject(new \ArrayObject([
87 | 'type' => 'object',
88 | 'properties' => $properties = array_merge_recursive($this->getJsonSchemaFromPathParts(explode('.', $this->usernamePath)), $this->getJsonSchemaFromPathParts(explode('.', $this->passwordPath))),
89 | 'required' => array_keys($properties),
90 | ]))),
91 | ]))
92 | ->withRequired(true)
93 | )
94 | ));
95 |
96 | return $openApi;
97 | }
98 |
99 | private function getJsonSchemaFromPathParts(array $pathParts): array
100 | {
101 | $jsonSchema = [];
102 |
103 | if (count($pathParts) === 1) {
104 | $jsonSchema[array_shift($pathParts)] = [
105 | 'type' => 'string',
106 | 'nullable' => false,
107 | ];
108 |
109 | return $jsonSchema;
110 | }
111 |
112 | $pathPart = array_shift($pathParts);
113 | $properties = $this->getJsonSchemaFromPathParts($pathParts);
114 | $jsonSchema[$pathPart] = [
115 | 'type' => 'object',
116 | 'properties' => $properties,
117 | 'required' => array_keys($properties),
118 | ];
119 |
120 | return $jsonSchema;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Resources/config/api_platform.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Resources/config/blocklist_token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Resources/config/console.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 | %lexik_jwt_authentication.encoder.signature_algorithm%
11 |
12 |
13 |
14 |
15 |
16 | %lexik_jwt_authentication.pass_phrase%
17 | %lexik_jwt_authentication.encoder.signature_algorithm%
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Resources/config/cookie.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 | null
10 | null
11 |
12 |
13 | null
14 |
15 |
16 | null
17 | false
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Resources/config/jwt_manager.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 | %lexik_jwt_authentication.user_id_claim%
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Resources/config/key_loader.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 | %lexik_jwt_authentication.pass_phrase%
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Resources/config/lcobucci.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | %lexik_jwt_authentication.encoder.signature_algorithm%
16 | %lexik_jwt_authentication.token_ttl%
17 | %lexik_jwt_authentication.clock_skew%
18 | %lexik_jwt_authentication.allow_no_expiration%
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Resources/config/response_interceptor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | true
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Resources/config/token_authenticator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Resources/config/token_extractor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
11 |
15 |
18 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Resources/config/web_token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | %lexik_jwt_authentication.token_ttl%
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Resources/config/web_token_issuance.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | null
15 | null
16 | null
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Resources/config/web_token_verification.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | false
18 |
19 |
20 |
21 | null
22 |
23 |
24 | %lexik_jwt_authentication.clock_skew%
25 | true
26 |
27 |
28 |
29 |
30 | %lexik_jwt_authentication.clock_skew%
31 | true
32 |
33 |
34 |
35 |
36 | %lexik_jwt_authentication.clock_skew%
37 | true
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Response/JWTAuthenticationFailureResponse.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | final class JWTAuthenticationFailureResponse extends JsonResponse
16 | {
17 | private string $message;
18 |
19 | public function __construct(string $message = 'Bad credentials', int $statusCode = Response::HTTP_UNAUTHORIZED)
20 | {
21 | $this->message = $message;
22 |
23 | parent::__construct(null, $statusCode, ['WWW-Authenticate' => 'Bearer']);
24 | }
25 |
26 | /**
27 | * Sets the response data with the statusCode & message included.
28 | */
29 | public function setData(mixed $data = []): static
30 | {
31 | return parent::setData((array)$data + ["code" => $this->statusCode, "message" => $this->getMessage()]);
32 | }
33 |
34 | /**
35 | * Sets the failure message.
36 | */
37 | public function setMessage(string $message): JWTAuthenticationFailureResponse
38 | {
39 | $this->message = $message;
40 |
41 | $this->setData();
42 |
43 | return $this;
44 | }
45 |
46 | /**
47 | * Gets the failure message.
48 | */
49 | public function getMessage(): string
50 | {
51 | return $this->message;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Response/JWTAuthenticationSuccessResponse.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class JWTAuthenticationSuccessResponse extends JsonResponse
13 | {
14 | /**
15 | * @param string $token Json Web Token
16 | * @param array $data Extra data passed to the response
17 | */
18 | public function __construct(string $token, array $data = [], array $jwtCookies = [])
19 | {
20 | if (!$jwtCookies) {
21 | parent::__construct(['token' => $token] + $data);
22 |
23 | return;
24 | }
25 |
26 | parent::__construct($data);
27 |
28 | foreach ($jwtCookies as $cookie) {
29 | $this->headers->setCookie($cookie);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Security/Authenticator/JWTAuthenticator.php:
--------------------------------------------------------------------------------
1 | tokenExtractor = $tokenExtractor;
54 | $this->jwtManager = $jwtManager;
55 | $this->eventDispatcher = $eventDispatcher;
56 | $this->userProvider = $userProvider;
57 | $this->translator = $translator;
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function start(Request $request, ?AuthenticationException $authException = null): Response
64 | {
65 | $exception = new MissingTokenException('JWT Token not found', 0, $authException);
66 | $event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()), $request);
67 |
68 | $this->eventDispatcher->dispatch($event, Events::JWT_NOT_FOUND);
69 |
70 | return $event->getResponse();
71 | }
72 |
73 | public function supports(Request $request): ?bool
74 | {
75 | return false !== $this->getTokenExtractor()->extract($request);
76 | }
77 |
78 | public function authenticate(Request $request): Passport
79 | {
80 | $token = $this->getTokenExtractor()->extract($request);
81 | if ($token === false) {
82 | throw new \LogicException('Unable to extract a JWT token from the request. Also, make sure to call `supports()` before `authenticate()` to get a proper client error.');
83 | }
84 |
85 | try {
86 | if (!$payload = $this->jwtManager->parse($token)) {
87 | throw new InvalidTokenException('Invalid JWT Token');
88 | }
89 | } catch (JWTDecodeFailureException $e) {
90 | if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
91 | throw new ExpiredTokenException();
92 | }
93 |
94 | throw new InvalidTokenException('Invalid JWT Token', 0, $e);
95 | }
96 |
97 | $idClaim = $this->jwtManager->getUserIdClaim();
98 | if (!isset($payload[$idClaim])) {
99 | throw new InvalidPayloadException($idClaim);
100 | }
101 |
102 | $passport = new SelfValidatingPassport(
103 | new UserBadge(
104 | (string) $payload[$idClaim],
105 | fn ($userIdentifier) => $this->loadUser($payload, $userIdentifier)
106 | )
107 | );
108 |
109 | $passport->setAttribute('payload', $payload);
110 | $passport->setAttribute('token', $token);
111 |
112 | return $passport;
113 | }
114 |
115 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
116 | {
117 | return null;
118 | }
119 |
120 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
121 | {
122 | $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
123 | if (null !== $this->translator) {
124 | $errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
125 | }
126 | $response = new JWTAuthenticationFailureResponse($errorMessage);
127 |
128 | if ($exception instanceof ExpiredTokenException) {
129 | $event = new JWTExpiredEvent($exception, $response, $request);
130 | $eventName = Events::JWT_EXPIRED;
131 | } else {
132 | $event = new JWTInvalidEvent($exception, $response, $request);
133 | $eventName = Events::JWT_INVALID;
134 | }
135 |
136 | $this->eventDispatcher->dispatch($event, $eventName);
137 |
138 | return $event->getResponse();
139 | }
140 |
141 | /**
142 | * Gets the token extractor to be used for retrieving a JWT token in the
143 | * current request.
144 | *
145 | * Override this method for adding/removing extractors to the chain one or
146 | * returning a different {@link TokenExtractorInterface} implementation.
147 | */
148 | protected function getTokenExtractor(): TokenExtractorInterface
149 | {
150 | return $this->tokenExtractor;
151 | }
152 |
153 | /**
154 | * Gets the jwt manager.
155 | */
156 | protected function getJwtManager(): JWTTokenManagerInterface
157 | {
158 | return $this->jwtManager;
159 | }
160 |
161 | /**
162 | * Gets the event dispatcher.
163 | */
164 | protected function getEventDispatcher(): EventDispatcherInterface
165 | {
166 | return $this->eventDispatcher;
167 | }
168 |
169 | /**
170 | * Gets the user provider.
171 | */
172 | protected function getUserProvider(): UserProviderInterface
173 | {
174 | return $this->userProvider;
175 | }
176 |
177 | /**
178 | * Loads the user to authenticate.
179 | *
180 | * @param array $payload The token payload
181 | * @param string $identity The key from which to retrieve the user "identifier"
182 | */
183 | protected function loadUser(array $payload, string $identity): UserInterface
184 | {
185 | if ($this->userProvider instanceof PayloadAwareUserProviderInterface) {
186 | return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
187 | }
188 |
189 | if ($this->userProvider instanceof ChainUserProvider) {
190 | foreach ($this->userProvider->getProviders() as $provider) {
191 | try {
192 | if ($provider instanceof PayloadAwareUserProviderInterface) {
193 | return $provider->loadUserByIdentifierAndPayload($identity, $payload);
194 | }
195 |
196 | return $provider->loadUserByIdentifier($identity);
197 | } catch (AuthenticationException $e) {
198 | // try next one
199 | }
200 | }
201 |
202 | $ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $identity));
203 | $ex->setUserIdentifier($identity);
204 |
205 | throw $ex;
206 | }
207 |
208 | return $this->userProvider->loadUserByIdentifier($identity);
209 | }
210 |
211 | public function createToken(Passport $passport, string $firewallName): TokenInterface
212 | {
213 | $token = new JWTPostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles(), $passport->getAttribute('token'));
214 |
215 | $this->eventDispatcher->dispatch(new JWTAuthenticatedEvent($passport->getAttribute('payload'), $token), Events::JWT_AUTHENTICATED);
216 |
217 | return $token;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/Security/Authenticator/Token/JWTPostAuthenticationToken.php:
--------------------------------------------------------------------------------
1 | token = $token;
17 | }
18 |
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function getCredentials(): string
23 | {
24 | return $this->token;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Security/Http/Authentication/AuthenticationFailureHandler.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
21 | {
22 | protected EventDispatcherInterface $dispatcher;
23 | private ?TranslatorInterface $translator;
24 |
25 | public function __construct(EventDispatcherInterface $dispatcher, ?TranslatorInterface $translator = null)
26 | {
27 | $this->dispatcher = $dispatcher;
28 | $this->translator = $translator;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
35 | {
36 | $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
37 | $statusCode = self::mapExceptionCodeToStatusCode($exception->getCode());
38 | if ($this->translator) {
39 | $errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
40 | }
41 |
42 | $event = new AuthenticationFailureEvent(
43 | $exception,
44 | new JWTAuthenticationFailureResponse($errorMessage, $statusCode),
45 | $request
46 | );
47 |
48 | $this->dispatcher->dispatch($event, Events::AUTHENTICATION_FAILURE);
49 |
50 | return $event->getResponse();
51 | }
52 |
53 | /**
54 | * @param string|int $exceptionCode
55 | */
56 | private static function mapExceptionCodeToStatusCode($exceptionCode): int
57 | {
58 | $canMapToStatusCode = is_int($exceptionCode)
59 | && $exceptionCode >= 400
60 | && $exceptionCode < 500;
61 |
62 | return $canMapToStatusCode
63 | ? $exceptionCode
64 | : Response::HTTP_UNAUTHORIZED;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Security/Http/Authentication/AuthenticationSuccessHandler.php:
--------------------------------------------------------------------------------
1 |
21 | * @author Robin Chalas
22 | *
23 | * @final
24 | */
25 | class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
26 | {
27 | protected JWTTokenManagerInterface $jwtManager;
28 | protected EventDispatcherInterface $dispatcher;
29 | protected bool $removeTokenFromBodyWhenCookiesUsed;
30 | private iterable $cookieProviders;
31 |
32 | /**
33 | * @param iterable|JWTCookieProvider[] $cookieProviders
34 | */
35 | public function __construct(JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher, iterable $cookieProviders = [], bool $removeTokenFromBodyWhenCookiesUsed = true)
36 | {
37 | $this->jwtManager = $jwtManager;
38 | $this->dispatcher = $dispatcher;
39 | $this->cookieProviders = $cookieProviders;
40 | $this->removeTokenFromBodyWhenCookiesUsed = $removeTokenFromBodyWhenCookiesUsed;
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
47 | {
48 | return $this->handleAuthenticationSuccess($token->getUser());
49 | }
50 |
51 | public function handleAuthenticationSuccess(UserInterface $user, $jwt = null): Response
52 | {
53 | if (null === $jwt) {
54 | $jwt = $this->jwtManager->create($user);
55 | }
56 |
57 | $jwtCookies = [];
58 | foreach ($this->cookieProviders as $cookieProvider) {
59 | $jwtCookies[] = $cookieProvider->createCookie($jwt);
60 | }
61 |
62 | $response = new JWTAuthenticationSuccessResponse($jwt, [], $jwtCookies);
63 | $event = new AuthenticationSuccessEvent(['token' => $jwt], $user, $response);
64 |
65 | $this->dispatcher->dispatch($event, Events::AUTHENTICATION_SUCCESS);
66 | $responseData = $event->getData();
67 |
68 | if ($jwtCookies && $this->removeTokenFromBodyWhenCookiesUsed) {
69 | unset($responseData['token']);
70 | }
71 |
72 | if ($responseData) {
73 | $response->setData($responseData);
74 | } else {
75 | $response->setStatusCode(Response::HTTP_NO_CONTENT);
76 | }
77 |
78 | return $response;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Security/Http/Cookie/JWTCookieProvider.php:
--------------------------------------------------------------------------------
1 | defaultName = $defaultName;
27 | $this->defaultLifetime = $defaultLifetime;
28 | $this->defaultSameSite = $defaultSameSite;
29 | $this->defaultPath = $defaultPath;
30 | $this->defaultDomain = $defaultDomain;
31 | $this->defaultSecure = $defaultSecure;
32 | $this->defaultHttpOnly = $defaultHttpOnly;
33 | $this->defaultSplit = $defaultSplit;
34 | $this->defaultPartitioned = $defaultPartitioned;
35 |
36 | if ($defaultPartitioned && Kernel::VERSION < '6.4') {
37 | throw new \LogicException(sprintf('The `partitioned` option for cookies is only available for Symfony 6.4 and above. You are currently on version %s', Kernel::VERSION));
38 | }
39 | }
40 |
41 | /**
42 | * Creates a secure cookie containing the passed JWT.
43 | *
44 | * For each argument (all args except $jwt), if omitted or set to null then the
45 | * default value defined via the constructor will be used.
46 | */
47 | public function createCookie(string $jwt, ?string $name = null, $expiresAt = null, ?string $sameSite = null, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httpOnly = null, array $split = [], ?bool $partitioned = null): Cookie
48 | {
49 | if (!$name && !$this->defaultName) {
50 | throw new \LogicException(sprintf('The cookie name must be provided, either pass it as 2nd argument of %s or set a default name via the constructor.', __METHOD__));
51 | }
52 |
53 | if (!$expiresAt && null === $this->defaultLifetime) {
54 | throw new \LogicException(sprintf('The cookie expiration time must be provided, either pass it as 3rd argument of %s or set a default lifetime via the constructor.', __METHOD__));
55 | }
56 |
57 | if ($partitioned && Kernel::VERSION < '6.4') {
58 | throw new \LogicException(sprintf('The `partitioned` option for cookies is only available for Symfony 6.4 and above. You are currently on version %s', Kernel::VERSION));
59 | }
60 |
61 | $jwtParts = new JWTSplitter($jwt);
62 | $jwt = $jwtParts->getParts($split ?: $this->defaultSplit);
63 |
64 | if (null === $expiresAt) {
65 | $expiresAt = 0 === $this->defaultLifetime ? 0 : (time() + $this->defaultLifetime);
66 | }
67 |
68 | return Cookie::create(
69 | $name ?: $this->defaultName,
70 | $jwt,
71 | $expiresAt,
72 | $path ?: $this->defaultPath,
73 | $domain ?: $this->defaultDomain,
74 | $secure ?? $this->defaultSecure,
75 | $httpOnly ?? $this->defaultHttpOnly,
76 | false,
77 | $sameSite ?: $this->defaultSameSite,
78 | $partitioned ?? $this->defaultPartitioned
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Security/User/JWTUser.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class JWTUser implements JWTUserInterface
13 | {
14 | private string $userIdentifier;
15 | private array $roles;
16 |
17 | public function __construct(string $userIdentifier, array $roles = [])
18 | {
19 | $this->userIdentifier = $userIdentifier;
20 | $this->roles = $roles;
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public static function createFromPayload($username, array $payload): JWTUserInterface
27 | {
28 | if (isset($payload['roles'])) {
29 | return new static($username, (array) $payload['roles']);
30 | }
31 |
32 | return new static($username);
33 | }
34 |
35 | public function getUsername(): string
36 | {
37 | return $this->getUserIdentifier();
38 | }
39 |
40 | public function getUserIdentifier(): string
41 | {
42 | return $this->userIdentifier;
43 | }
44 |
45 | /**
46 | * {@inheritdoc}
47 | */
48 | public function getRoles(): array
49 | {
50 | return $this->roles;
51 | }
52 |
53 | public function getPassword(): ?string
54 | {
55 | return null;
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | */
61 | public function getSalt(): ?string
62 | {
63 | return null;
64 | }
65 |
66 | /**
67 | * {@inheritdoc}
68 | */
69 | public function eraseCredentials(): void
70 | {
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Security/User/JWTUserInterface.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class JWTUserProvider implements PayloadAwareUserProviderInterface
13 | {
14 | private string $class;
15 |
16 | private array $cache = [];
17 |
18 | /**
19 | * @param string $class The {@link JWTUserInterface} implementation FQCN for which to provide instances
20 | */
21 | public function __construct(string $class)
22 | {
23 | $this->class = $class;
24 | }
25 |
26 | /**
27 | * To be removed at the same time as symfony 5.4 support.
28 | */
29 | public function loadUserByUsername(string $username): UserInterface
30 | {
31 | // to be removed at the same time as symfony 5.4 support
32 | throw new \LogicException('This method is implemented for BC purpose and should never be called.');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | *
38 | * @param array $payload The JWT payload from which to create an instance
39 | */
40 | public function loadUserByIdentifier(string $identifier, array $payload = []): UserInterface
41 | {
42 | return $this->loadUserByIdentifierAndPayload($identifier, $payload);
43 | }
44 |
45 | public function loadUserByIdentifierAndPayload(string $identifier, array $payload): UserInterface
46 | {
47 | if (isset($this->cache[$identifier])) {
48 | return $this->cache[$identifier];
49 | }
50 |
51 | $class = $this->class;
52 |
53 | return $this->cache[$identifier] = $class::createFromPayload($identifier, $payload);
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function supportsClass($class): bool
60 | {
61 | return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
62 | }
63 |
64 | public function refreshUser(UserInterface $user): UserInterface
65 | {
66 | return $user; // noop
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Security/User/PayloadAwareUserProviderInterface.php:
--------------------------------------------------------------------------------
1 | cacheJwt = $cacheJwt;
19 | }
20 |
21 | public function add(array $payload): bool
22 | {
23 | if (!isset($payload['exp'])) {
24 | throw new MissingClaimException('exp');
25 | }
26 |
27 | $expiration = new DateTimeImmutable('@' . $payload['exp'], new DateTimeZone('UTC'));
28 | $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
29 |
30 | // If the token is already expired, there's no point in adding it to storage
31 | if ($expiration <= $now) {
32 | return false;
33 | }
34 |
35 | $cacheExpiration = $expiration->add(new DateInterval('PT5M'));
36 |
37 | if (!isset($payload['jti'])) {
38 | throw new MissingClaimException('jti');
39 | }
40 |
41 | $cacheItem = $this->cacheJwt->getItem($payload['jti']);
42 | $cacheItem->set([]);
43 | $cacheItem->expiresAt($cacheExpiration);
44 | $this->cacheJwt->save($cacheItem);
45 |
46 | return true;
47 | }
48 |
49 | public function has(array $payload): bool
50 | {
51 | if (!isset($payload['jti'])) {
52 | throw new MissingClaimException('jti');
53 | }
54 |
55 | return $this->cacheJwt->hasItem($payload['jti']);
56 | }
57 |
58 | public function remove(array $payload): void
59 | {
60 | if (!isset($payload['jti'])) {
61 | throw new MissingClaimException('jti');
62 | }
63 |
64 | $this->cacheJwt->deleteItem($payload['jti']);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Services/BlockedTokenManagerInterface.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | interface JWSProviderInterface
14 | {
15 | /**
16 | * Creates a new JWS signature from a given payload.
17 | */
18 | public function create(array $payload, array $header = []): CreatedJWS;
19 |
20 | /**
21 | * Loads an existing JWS signature from a given JWT token.
22 | */
23 | public function load(string $token): LoadedJWS;
24 | }
25 |
--------------------------------------------------------------------------------
/Services/JWSProvider/LcobucciJWSProvider.php:
--------------------------------------------------------------------------------
1 |
34 | */
35 | class LcobucciJWSProvider implements JWSProviderInterface
36 | {
37 | private KeyLoaderInterface $keyLoader;
38 | private Clock $clock;
39 | private Signer $signer;
40 | private ?int $ttl;
41 | private ?int $clockSkew;
42 | private bool $allowNoExpiration;
43 |
44 | /**
45 | * @throws \InvalidArgumentException If the given crypto engine is not supported
46 | */
47 | public function __construct(KeyLoaderInterface $keyLoader, string $signatureAlgorithm, ?int $ttl, ?int $clockSkew, bool $allowNoExpiration = false, ?Clock $clock = null)
48 | {
49 | if (null === $clock) {
50 | $clock = new SystemClock(new \DateTimeZone('UTC'));
51 | }
52 |
53 | $this->keyLoader = $keyLoader;
54 | $this->clock = $clock;
55 | $this->signer = $this->getSignerForAlgorithm($signatureAlgorithm);
56 | $this->ttl = $ttl;
57 | $this->clockSkew = $clockSkew;
58 | $this->allowNoExpiration = $allowNoExpiration;
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public function create(array $payload, array $header = []): CreatedJWS
65 | {
66 | $jws = new JWTBuilder(new JoseEncoder(), ChainedFormatter::default());
67 |
68 | foreach ($header as $k => $v) {
69 | $jws = $jws->withHeader($k, $v);
70 | }
71 |
72 | $now = time();
73 |
74 | $issuedAt = $payload['iat'] ?? $now;
75 | unset($payload['iat']);
76 |
77 | $jws = $jws->issuedAt(!$issuedAt instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$issuedAt}") : $issuedAt);
78 |
79 | $exp = null;
80 | if (null !== $this->ttl || isset($payload['exp'])) {
81 | $exp = $payload['exp'] ?? $now + $this->ttl;
82 | unset($payload['exp']);
83 | }
84 |
85 | if ($exp) {
86 | $jws = $jws->expiresAt(!$exp instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$exp}") : $exp);
87 | }
88 |
89 | if (isset($payload['sub'])) {
90 | $jws = $jws->relatedTo($payload['sub']);
91 | unset($payload['sub']);
92 | }
93 |
94 | $jws = $this->addStandardClaims($jws, $payload);
95 |
96 | foreach ($payload as $name => $value) {
97 | $jws = $jws->withClaim($name, $value);
98 | }
99 |
100 | $token = $this->getSignedToken($jws);
101 |
102 | return new CreatedJWS((string) $token, true);
103 | }
104 |
105 | /**
106 | * {@inheritdoc}
107 | */
108 | public function load(string $token): LoadedJWS
109 | {
110 | $jws = (new JWTParser(new JoseEncoder()))->parse($token);
111 |
112 | $payload = [];
113 | foreach ($jws->claims()->all() as $name => $value) {
114 | if ($value instanceof \DateTimeInterface) {
115 | $value = $value->getTimestamp();
116 | }
117 | $payload[$name] = $value;
118 | }
119 |
120 | return new LoadedJWS(
121 | $payload,
122 | $this->verify($jws),
123 | false == $this->allowNoExpiration,
124 | $jws->headers()->all(),
125 | $this->clockSkew
126 | );
127 | }
128 |
129 | private function getSignerForAlgorithm($signatureAlgorithm): Signer
130 | {
131 | $signerMap = [
132 | 'HS256' => Sha256::class,
133 | 'HS384' => Sha384::class,
134 | 'HS512' => Sha512::class,
135 | 'RS256' => Signer\Rsa\Sha256::class,
136 | 'RS384' => Signer\Rsa\Sha384::class,
137 | 'RS512' => Signer\Rsa\Sha512::class,
138 | 'ES256' => Signer\Ecdsa\Sha256::class,
139 | 'ES384' => Signer\Ecdsa\Sha384::class,
140 | 'ES512' => Signer\Ecdsa\Sha512::class,
141 | ];
142 |
143 | if (!isset($signerMap[$signatureAlgorithm])) {
144 | throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported by %s', $signatureAlgorithm, self::class));
145 | }
146 |
147 | $signerClass = $signerMap[$signatureAlgorithm];
148 |
149 | if (is_subclass_of($signerClass, Ecdsa::class) && method_exists($signerClass, 'create')) {
150 | return $signerClass::create();
151 | }
152 |
153 | return new $signerClass();
154 | }
155 |
156 | private function getSignedToken(Builder $jws): string
157 | {
158 | $key = InMemory::plainText($this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : (string) $this->keyLoader->getPassphrase());
159 |
160 | $token = $jws->getToken($this->signer, $key);
161 |
162 | return $token->toString();
163 | }
164 |
165 | private function verify(Token $jwt): bool
166 | {
167 | $key = InMemory::plainText($this->signer instanceof Hmac ? $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE) : $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC));
168 | $validator = new Validator();
169 |
170 | $isValid = $validator->validate(
171 | $jwt,
172 | new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
173 | new SignedWith($this->signer, $key)
174 | );
175 |
176 | $publicKeys = $this->keyLoader->getAdditionalPublicKeys();
177 | if ($isValid || $this->signer instanceof Hmac || empty($publicKeys)) {
178 | return $isValid;
179 | }
180 |
181 | // If the key used to verify the token is invalid, and it's not Hmac algorithm, try with additional public keys
182 | foreach ($publicKeys as $key) {
183 | $isValid = $validator->validate(
184 | $jwt,
185 | new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
186 | new SignedWith($this->signer, InMemory::plainText($key))
187 | );
188 |
189 | if ($isValid) {
190 | return true;
191 | }
192 | }
193 |
194 | return false;
195 | }
196 |
197 | private function addStandardClaims(Builder $builder, array &$payload): Builder
198 | {
199 | $mutatorMap = [
200 | RegisteredClaims::AUDIENCE => 'permittedFor',
201 | RegisteredClaims::ID => 'identifiedBy',
202 | RegisteredClaims::ISSUER => 'issuedBy',
203 | RegisteredClaims::NOT_BEFORE => 'canOnlyBeUsedAfter',
204 | ];
205 |
206 | foreach ($payload as $claim => $value) {
207 | if (!isset($mutatorMap[$claim])) {
208 | continue;
209 | }
210 |
211 | $mutator = $mutatorMap[$claim];
212 | unset($payload[$claim]);
213 |
214 | if (\is_array($value)) {
215 | $builder = \call_user_func_array([$builder, $mutator], $value);
216 | continue;
217 | }
218 |
219 | $builder = $builder->{$mutator}($value);
220 | }
221 |
222 | return $builder;
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/Services/JWTManager.php:
--------------------------------------------------------------------------------
1 |
24 | * @author Robin Chalas
25 | */
26 | class JWTManager implements JWTTokenManagerInterface
27 | {
28 | protected JWTEncoderInterface $jwtEncoder;
29 | protected EventDispatcherInterface $dispatcher;
30 | protected string $userIdClaim;
31 | private $payloadEnrichment;
32 |
33 | public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, string $userIdClaim, ?PayloadEnrichmentInterface $payloadEnrichment = null)
34 | {
35 | $this->jwtEncoder = $encoder;
36 | $this->dispatcher = $dispatcher;
37 | $this->userIdClaim = $userIdClaim;
38 | $this->payloadEnrichment = $payloadEnrichment ?? new NullEnrichment();
39 | }
40 |
41 | /**
42 | * @return string The JWT token
43 | *
44 | * @throws JWTEncodeFailureException
45 | */
46 | public function create(UserInterface $user): string
47 | {
48 | $payload = ['roles' => $user->getRoles()];
49 | $this->addUserIdentityToPayload($user, $payload);
50 |
51 | $this->payloadEnrichment->enrich($user, $payload);
52 |
53 | return $this->generateJwtStringAndDispatchEvents($user, $payload);
54 | }
55 |
56 | /**
57 | * @return string The JWT token
58 | *
59 | * @throws JWTEncodeFailureException
60 | */
61 | public function createFromPayload(UserInterface $user, array $payload = []): string
62 | {
63 | $payload = array_merge(['roles' => $user->getRoles()], $payload);
64 | $this->addUserIdentityToPayload($user, $payload);
65 |
66 | $this->payloadEnrichment->enrich($user, $payload);
67 |
68 | return $this->generateJwtStringAndDispatchEvents($user, $payload);
69 | }
70 |
71 | /**
72 | * @return string The JWT token
73 | *
74 | * @throws JWTEncodeFailureException
75 | */
76 | private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload): string
77 | {
78 | $jwtCreatedEvent = new JWTCreatedEvent($payload, $user);
79 | $this->dispatcher->dispatch($jwtCreatedEvent, Events::JWT_CREATED);
80 |
81 | if ($this->jwtEncoder instanceof HeaderAwareJWTEncoderInterface) {
82 | $jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData(), $jwtCreatedEvent->getHeader());
83 | } else {
84 | $jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData());
85 | }
86 |
87 | $jwtEncodedEvent = new JWTEncodedEvent($jwtString);
88 |
89 | $this->dispatcher->dispatch($jwtEncodedEvent, Events::JWT_ENCODED);
90 |
91 | return $jwtString;
92 | }
93 |
94 | /**
95 | * {@inheritdoc}
96 | *
97 | * @throws JWTDecodeFailureException
98 | */
99 | public function decode(TokenInterface $token): array|bool
100 | {
101 | if (!($payload = $this->jwtEncoder->decode($token->getCredentials()))) {
102 | return false;
103 | }
104 |
105 | $event = new JWTDecodedEvent($payload);
106 | $this->dispatcher->dispatch($event, Events::JWT_DECODED);
107 |
108 | if (!$event->isValid()) {
109 | return false;
110 | }
111 |
112 | return $event->getPayload();
113 | }
114 |
115 | /**
116 | * {@inheritdoc}
117 | *
118 | * @throws JWTDecodeFailureException
119 | */
120 | public function parse(string $token): array
121 | {
122 | $payload = $this->jwtEncoder->decode($token);
123 |
124 | $event = new JWTDecodedEvent($payload);
125 | $this->dispatcher->dispatch($event, Events::JWT_DECODED);
126 |
127 | if (!$event->isValid()) {
128 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'The token was marked as invalid by an event listener after successful decoding.');
129 | }
130 |
131 | return $event->getPayload();
132 | }
133 |
134 | /**
135 | * Add user identity to payload, username by default.
136 | * Override this if you need to identify it by another property.
137 | */
138 | protected function addUserIdentityToPayload(UserInterface $user, array &$payload): void
139 | {
140 | $accessor = PropertyAccess::createPropertyAccessor();
141 | $payload[$this->userIdClaim] = $accessor->getValue($user, $accessor->isReadable($user, $this->userIdClaim) ? $this->userIdClaim : 'user_identifier');
142 | }
143 |
144 | public function getUserIdClaim(): string
145 | {
146 | return $this->userIdClaim;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Services/JWTTokenManagerInterface.php:
--------------------------------------------------------------------------------
1 |
14 | * @author Eric Lannez
15 | */
16 | interface JWTTokenManagerInterface
17 | {
18 | /**
19 | * @return string The JWT token
20 | */
21 | public function create(UserInterface $user);
22 |
23 | public function createFromPayload(UserInterface $user, array $payload = []): string;
24 |
25 | /**
26 | * @return array|false The JWT token payload or false if an error occurs
27 | * @throws JWTDecodeFailureException
28 | */
29 | public function decode(TokenInterface $token): array|bool;
30 |
31 | /**
32 | * Parses a raw JWT token and returns its payload
33 | */
34 | public function parse(string $token): array;
35 |
36 | /**
37 | * Returns the claim used as identifier to load an user from a JWT payload.
38 | *
39 | * @return string
40 | */
41 | public function getUserIdClaim();
42 | }
43 |
--------------------------------------------------------------------------------
/Services/KeyLoader/AbstractKeyLoader.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * @internal
11 | */
12 | abstract class AbstractKeyLoader implements KeyLoaderInterface
13 | {
14 | private ?string $signingKey;
15 | private ?string $publicKey;
16 | private ?string $passphrase;
17 | private array $additionalPublicKeys;
18 |
19 | public function __construct(?string $signingKey = null, ?string $publicKey = null, ?string $passphrase = null, array $additionalPublicKeys = [])
20 | {
21 | $this->signingKey = $signingKey;
22 | $this->publicKey = $publicKey;
23 | $this->passphrase = $passphrase;
24 | $this->additionalPublicKeys = $additionalPublicKeys;
25 | }
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function getPassphrase(): ?string
31 | {
32 | return $this->passphrase;
33 | }
34 |
35 | public function getSigningKey(): ?string
36 | {
37 | return $this->signingKey && is_file($this->signingKey) ? $this->readKey(self::TYPE_PRIVATE) : $this->signingKey;
38 | }
39 |
40 | public function getPublicKey(): ?string
41 | {
42 | return $this->publicKey && is_file($this->publicKey) ? $this->readKey(self::TYPE_PUBLIC) : $this->publicKey;
43 | }
44 |
45 | public function getAdditionalPublicKeys(): array
46 | {
47 | $contents = [];
48 |
49 | foreach ($this->additionalPublicKeys as $key) {
50 | if (!$key || !is_file($key) || !is_readable($key)) {
51 | throw new \RuntimeException(sprintf('Additional public key "%s" does not exist or is not readable. Did you correctly set the "lexik_jwt_authentication.additional_public_keys" configuration key?', $key));
52 | }
53 |
54 | $rawKey = @file_get_contents($key);
55 |
56 | if (false === $rawKey) {
57 | // Try invalidating the realpath cache
58 | clearstatcache(true, $key);
59 | $rawKey = file_get_contents($key);
60 | }
61 | $contents[] = $rawKey;
62 | }
63 |
64 | return $contents;
65 | }
66 |
67 | private function readKey($type): ?string
68 | {
69 | $isPublic = self::TYPE_PUBLIC === $type;
70 | $key = $isPublic ? $this->publicKey : $this->signingKey;
71 |
72 | if (!$key || !is_file($key) || !is_readable($key)) {
73 | if ($isPublic) {
74 | return null;
75 | }
76 |
77 | throw new \RuntimeException(sprintf('Signature key "%s" does not exist or is not readable. Did you correctly set the "lexik_jwt_authentication.signature_key" configuration key?', $key));
78 | }
79 |
80 | $rawKey = @file_get_contents($key);
81 |
82 | if (false === $rawKey) {
83 | // Try invalidating the realpath cache
84 | clearstatcache(true, $key);
85 | $rawKey = file_get_contents($key);
86 | }
87 |
88 | return $rawKey;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Services/KeyLoader/KeyDumperInterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface KeyDumperInterface
9 | {
10 | /**
11 | * Dumps a key to be shared between parties.
12 | *
13 | * @return resource|string
14 | */
15 | public function dumpKey();
16 | }
17 |
--------------------------------------------------------------------------------
/Services/KeyLoader/KeyLoaderInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface KeyLoaderInterface
11 | {
12 | public const TYPE_PUBLIC = 'public';
13 | public const TYPE_PRIVATE = 'private';
14 |
15 | /**
16 | * Loads a key from a given type (public or private).
17 | *
18 | * @param resource|string|null $type
19 | *
20 | * @return resource|string|null
21 | */
22 | public function loadKey($type);
23 |
24 | public function getPassphrase(): ?string;
25 |
26 | public function getSigningKey(): ?string;
27 |
28 | public function getPublicKey(): ?string;
29 |
30 | public function getAdditionalPublicKeys(): array;
31 | }
32 |
--------------------------------------------------------------------------------
/Services/KeyLoader/RawKeyLoader.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class RawKeyLoader extends AbstractKeyLoader implements KeyDumperInterface
11 | {
12 | /**
13 | * @param string $type
14 | *
15 | * @return string
16 | *
17 | * @throws \RuntimeException If the key cannot be read
18 | */
19 | public function loadKey($type)
20 | {
21 | if (!in_array($type, [self::TYPE_PUBLIC, self::TYPE_PRIVATE])) {
22 | throw new \InvalidArgumentException(sprintf('The key type must be "public" or "private", "%s" given.', $type));
23 | }
24 |
25 | if (self::TYPE_PUBLIC === $type) {
26 | return $this->dumpKey();
27 | }
28 |
29 | return $this->getSigningKey();
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function dumpKey()
36 | {
37 | if ($publicKey = $this->getPublicKey()) {
38 | return $publicKey;
39 | }
40 |
41 | $signingKey = $this->getSigningKey();
42 |
43 | // no public key provided, compute it from signing key
44 | try {
45 | $publicKey = openssl_pkey_get_details(openssl_pkey_get_private($signingKey, $this->getPassphrase()))['key'];
46 | } catch (\Throwable $e) {
47 | throw new \RuntimeException('Secret key either does not exist, is not readable or is invalid. Did you correctly set the "lexik_jwt_authentication.secret_key" config option?');
48 | }
49 |
50 | return $publicKey;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Services/PayloadEnrichment/ChainEnrichment.php:
--------------------------------------------------------------------------------
1 | enrichments = $enrichments;
18 | }
19 |
20 | public function enrich(UserInterface $user, array &$payload): void
21 | {
22 | foreach ($this->enrichments as $enrichment) {
23 | $enrichment->enrich($user, $payload);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Services/PayloadEnrichment/NullEnrichment.php:
--------------------------------------------------------------------------------
1 | jwsBuilder = $jwsBuilderFactory->create([$signatureAlgorithm]);
69 | if ($jweBuilderFactory !== null && $keyEncryptionAlgorithm !== null && $contentEncryptionAlgorithm !== null) {
70 | $this->jweBuilder = $jweBuilderFactory->create([$keyEncryptionAlgorithm, $contentEncryptionAlgorithm]);
71 | }
72 | $this->signatureKey = JWK::createFromJson($signatureKey);
73 | $this->encryptionKey = $encryptionKey ? JWK::createFromJson($encryptionKey) : null;
74 | $this->signatureAlgorithm = $signatureAlgorithm;
75 | $this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm;
76 | $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
77 | $this->dispatcher = $dispatcher;
78 | }
79 |
80 | public function build(array $header, array $claims): string
81 | {
82 | $token = $this->buildJWS($header, $claims);
83 |
84 | if ($this->jweBuilder !== null) {
85 | $token = $this->buildJWE($claims, $token);
86 | }
87 |
88 | return $token;
89 | }
90 |
91 | /**
92 | * @param array $header
93 | * @param array $claims
94 | */
95 | private function buildJWS(array $header, array $claims): string
96 | {
97 | $header['alg'] = $this->signatureAlgorithm;
98 | if ($this->signatureKey->has('kid')) {
99 | $header['kid'] = $this->signatureKey->get('kid');
100 | }
101 | $jws = $this->jwsBuilder
102 | ->create()
103 | ->withPayload(json_encode($claims, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
104 | ->addSignature($this->signatureKey, $header)
105 | ->build()
106 | ;
107 | $token = (new JwsCompactSerializer())->serialize($jws);
108 |
109 | return $token;
110 | }
111 |
112 | /**
113 | * @param array $header
114 | */
115 | private function buildJWE(array $claims, string $payload): string
116 | {
117 | $header = [
118 | 'alg' => $this->keyEncryptionAlgorithm,
119 | 'enc' => $this->contentEncryptionAlgorithm,
120 | 'cty' => 'JWT',
121 | 'typ' => 'JWT',
122 | ];
123 | if ($this->encryptionKey->has('kid')) {
124 | $header['kid'] = $this->encryptionKey->get('kid');
125 | }
126 | foreach (['exp', 'iat', 'nbf'] as $claim) {
127 | if (array_key_exists($claim, $claims)) {
128 | $header[$claim] = $claims[$claim];
129 | }
130 | }
131 | $event = $this->dispatcher->dispatch(new BeforeJWEComputationEvent($header), Events::BEFORE_JWE_COMPUTATION);
132 | $jwe = $this->jweBuilder
133 | ->create()
134 | ->withPayload($payload)
135 | ->withSharedProtectedHeader($event->getHeader())
136 | ->addRecipient($this->encryptionKey)
137 | ->build()
138 | ;
139 | return (new JweCompactSerializer())->serialize($jwe);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Services/WebToken/AccessTokenLoader.php:
--------------------------------------------------------------------------------
1 | jwsLoader = $jwsLoaderFactory->create(['jws_compact'], $signatureAlgorithms, $jwsHeaderChecker);
49 | if ($jweLoaderFactory !== null && !empty($keyEncryptionAlgorithms) && !empty($contentEncryptionAlgorithms) && !empty($jweHeaderChecker)) {
50 | $this->jweLoader = $jweLoaderFactory->create(['jwe_compact'], array_merge($keyEncryptionAlgorithms, $contentEncryptionAlgorithms), null, null, $jweHeaderChecker);
51 | $this->continueOnDecryptionFailure = $continueOnDecryptionFailure;
52 | }
53 | $this->signatureKeyset = JWKSet::createFromJson($signatureKeyset);
54 | $this->encryptionKeyset = $encryptionKeyset ? JWKSet::createFromJson($encryptionKeyset) : null;
55 | $this->claimCheckerManager = $claimCheckerManagerFactory->create($claimChecker);
56 | $this->mandatoryClaims = $mandatoryClaims;
57 | }
58 |
59 | public function load(string $token): array
60 | {
61 | $token = $this->loadJWE($token);
62 | $data = $this->loadJWS($token);
63 | try {
64 | $this->claimCheckerManager->check($data, $this->mandatoryClaims);
65 | } catch (MissingMandatoryClaimException|InvalidClaimException $e) {
66 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, $e->getMessage(), $e, $data);
67 | } catch (\Throwable $e) {
68 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to load the token', $e, $data);
69 | }
70 |
71 | return $data;
72 | }
73 |
74 | /**
75 | * @return array
76 | */
77 | private function loadJWS(string $token): array
78 | {
79 | $payload = null;
80 | $data = null;
81 | $signature = null;
82 | try {
83 | $jws = $this->jwsLoader->loadAndVerifyWithKeySet($token, $this->signatureKeyset, $signature);
84 | } catch (\Throwable $e) {
85 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be loaded or the signature cannot be verified.');
86 | }
87 | if ($signature !== 0) {
88 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token shall contain only one signature.');
89 | }
90 |
91 | $payload = $jws->getPayload();
92 | if (!$payload) {
93 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
94 | }
95 |
96 | $data = json_decode($payload, true);
97 | if (!is_array($data)) {
98 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
99 | }
100 |
101 | return $data;
102 | }
103 |
104 | private function loadJWE(string $token): string
105 | {
106 | if (!$this->jweLoader) {
107 | return $token;
108 | }
109 |
110 | $recipient = null;
111 | try {
112 | $jwe = $this->jweLoader->loadAndDecryptWithKeySet($token, $this->encryptionKeyset, $recipient);
113 | } catch (\Throwable $e) {
114 | if ($this->continueOnDecryptionFailure === true) {
115 | return $token;
116 | }
117 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be decrypted.', $e);
118 | }
119 | $token = $jwe->getPayload();
120 | if (!$token || $recipient !== 0) {
121 | throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token has no valid content.');
122 | }
123 |
124 | return $token;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Signature/CreatedJWS.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class CreatedJWS
11 | {
12 | private string $token;
13 | private bool $signed;
14 |
15 | public function __construct(string $token, bool $isSigned)
16 | {
17 | $this->token = $token;
18 | $this->signed = $isSigned;
19 | }
20 |
21 | public function isSigned(): bool
22 | {
23 | return $this->signed;
24 | }
25 |
26 | public function getToken(): string
27 | {
28 | return $this->token;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Signature/LoadedJWS.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | final class LoadedJWS
12 | {
13 | public const VERIFIED = 'verified';
14 | public const EXPIRED = 'expired';
15 | public const INVALID = 'invalid';
16 |
17 | private array $header;
18 | private array $payload;
19 | private ?string $state = null;
20 | private int $clockSkew;
21 | private bool $shouldCheckExpiration;
22 |
23 | public function __construct(array $payload, bool $isVerified, bool $shouldCheckExpiration = true, array $header = [], int $clockSkew = 0)
24 | {
25 | $this->payload = $payload;
26 | $this->header = $header;
27 | $this->shouldCheckExpiration = $shouldCheckExpiration;
28 | $this->clockSkew = $clockSkew;
29 |
30 | if (true === $isVerified) {
31 | $this->state = self::VERIFIED;
32 | }
33 |
34 | $this->checkIssuedAt();
35 | $this->checkExpiration();
36 | }
37 |
38 | public function getHeader(): array
39 | {
40 | return $this->header;
41 | }
42 |
43 | public function getPayload(): array
44 | {
45 | return $this->payload;
46 | }
47 |
48 | public function isVerified(): bool
49 | {
50 | return self::VERIFIED === $this->state;
51 | }
52 |
53 | public function isExpired(): bool
54 | {
55 | $this->checkExpiration();
56 |
57 | return self::EXPIRED === $this->state;
58 | }
59 |
60 | public function isInvalid(): bool
61 | {
62 | return self::INVALID === $this->state;
63 | }
64 |
65 | private function checkExpiration(): void
66 | {
67 | if (!$this->shouldCheckExpiration) {
68 | return;
69 | }
70 |
71 | if (!isset($this->payload['exp']) || !is_numeric($this->payload['exp'])) {
72 | $this->state = self::INVALID;
73 |
74 | return;
75 | }
76 |
77 | if ($this->clockSkew <= time() - $this->payload['exp']) {
78 | $this->state = self::EXPIRED;
79 | }
80 | }
81 |
82 | /**
83 | * Ensures that the iat claim is not in the future.
84 | */
85 | private function checkIssuedAt(): void
86 | {
87 | if (isset($this->payload['iat']) && (int) $this->payload['iat'] - $this->clockSkew > time()) {
88 | $this->state = self::INVALID;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Subscriber/AdditionalAccessTokenClaimsAndHeaderSubscriber.php:
--------------------------------------------------------------------------------
1 | ttl = $ttl;
19 | }
20 |
21 | public static function getSubscribedEvents(): array
22 | {
23 | return [
24 | Events::JWT_CREATED => [
25 | ['addClaims'],
26 | ],
27 | ];
28 | }
29 |
30 | public function addClaims(JWTCreatedEvent $event): void
31 | {
32 | $claims = [
33 | 'jti' => uniqid('', true),
34 | 'iat' => time(),
35 | 'nbf' => time(),
36 | ];
37 | $data = $event->getData();
38 | if (!array_key_exists('exp', $data) && $this->ttl > 0) {
39 | $claims['exp'] = time() + $this->ttl;
40 | }
41 | $event->setData(array_merge($claims, $data));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/TokenExtractor/AuthorizationHeaderTokenExtractor.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class AuthorizationHeaderTokenExtractor implements TokenExtractorInterface
13 | {
14 | protected ?string $prefix;
15 |
16 | protected string $name;
17 |
18 | public function __construct(?string $prefix, string $name)
19 | {
20 | $this->prefix = $prefix;
21 | $this->name = $name;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function extract(Request $request)
28 | {
29 | if (!$request->headers->has($this->name)) {
30 | return false;
31 | }
32 |
33 | $authorizationHeader = $request->headers->get($this->name);
34 |
35 | if (empty($this->prefix)) {
36 | return $authorizationHeader;
37 | }
38 |
39 | $headerParts = explode(' ', (string) $authorizationHeader);
40 |
41 | if (!(2 === count($headerParts) && 0 === strcasecmp($headerParts[0], $this->prefix))) {
42 | return false;
43 | }
44 |
45 | return $headerParts[1];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/TokenExtractor/ChainTokenExtractor.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class ChainTokenExtractor implements \IteratorAggregate, TokenExtractorInterface
17 | {
18 | private array $map;
19 |
20 | public function __construct(array $map)
21 | {
22 | $this->map = $map;
23 | }
24 |
25 | /**
26 | * Adds a new token extractor to the map.
27 | */
28 | public function addExtractor(TokenExtractorInterface $extractor)
29 | {
30 | $this->map[] = $extractor;
31 | }
32 |
33 | /**
34 | * Removes a token extractor from the map.
35 | *
36 | * @param \Closure $filter A function taking an extractor as argument, used to find the extractor to remove.
37 | *
38 | * @return bool True in case of success, false otherwise
39 | */
40 | public function removeExtractor(\Closure $filter)
41 | {
42 | $filtered = array_filter($this->map, $filter);
43 |
44 | if (!$extractorToUnmap = current($filtered)) {
45 | return false;
46 | }
47 |
48 | $key = array_search($extractorToUnmap, $this->map);
49 | unset($this->map[$key]);
50 |
51 | return true;
52 | }
53 |
54 | /**
55 | * Clears the token extractor map.
56 | */
57 | public function clearMap()
58 | {
59 | $this->map = [];
60 | }
61 |
62 | /**
63 | * Iterates over the token extractors map calling {@see extract()}
64 | * until a token is found.
65 | *
66 | * {@inheritdoc}
67 | */
68 | public function extract(Request $request)
69 | {
70 | foreach ($this->getIterator() as $extractor) {
71 | if ($token = $extractor->extract($request)) {
72 | return $token;
73 | }
74 | }
75 |
76 | return false;
77 | }
78 |
79 | /**
80 | * Iterates over the mapped token extractors while generating them.
81 | *
82 | * @return \Traversable
83 | */
84 | #[\ReturnTypeWillChange]
85 | public function getIterator()
86 | {
87 | foreach ($this->map as $extractor) {
88 | if ($extractor instanceof TokenExtractorInterface) {
89 | yield $extractor;
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/TokenExtractor/CookieTokenExtractor.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class CookieTokenExtractor implements TokenExtractorInterface
13 | {
14 | protected string $name;
15 |
16 | public function __construct(string $name)
17 | {
18 | $this->name = $name;
19 | }
20 |
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function extract(Request $request)
25 | {
26 | return $request->cookies->get($this->name, false);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/TokenExtractor/QueryParameterTokenExtractor.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class QueryParameterTokenExtractor implements TokenExtractorInterface
13 | {
14 | protected string $parameterName;
15 |
16 | public function __construct(string $parameterName)
17 | {
18 | $this->parameterName = $parameterName;
19 | }
20 |
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function extract(Request $request)
25 | {
26 | return $request->query->get($this->parameterName, false);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/TokenExtractor/SplitCookieExtractor.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class SplitCookieExtractor implements TokenExtractorInterface
13 | {
14 | private array $cookies;
15 |
16 | public function __construct(array $cookies)
17 | {
18 | $this->cookies = $cookies;
19 | }
20 |
21 | /**
22 | * {@inheritDoc}
23 | */
24 | public function extract(Request $request)
25 | {
26 | $jwtCookies = [];
27 |
28 | foreach ($this->cookies as $cookie) {
29 | $jwtCookies[] = $request->cookies->get($cookie, false);
30 | }
31 |
32 | if (count($this->cookies) !== count(array_filter($jwtCookies))) {
33 | return false;
34 | }
35 |
36 | return implode('.', $jwtCookies);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/TokenExtractor/TokenExtractorInterface.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | interface TokenExtractorInterface
13 | {
14 | /**
15 | * @return string|false
16 | */
17 | public function extract(Request $request);
18 | }
19 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lexik/jwt-authentication-bundle",
3 | "type": "symfony-bundle",
4 | "description": "This bundle provides JWT authentication for your Symfony REST API",
5 | "keywords": ["Symfony", "bundle", "jwt", "jws", "authentication", "api", "rest"],
6 | "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Jeremy Barthe",
11 | "email": "j.barthe@lexik.fr",
12 | "homepage": "https://github.com/jeremyb"
13 | },
14 | {
15 | "name": "Nicolas Cabot",
16 | "email": "n.cabot@lexik.fr",
17 | "homepage": "https://github.com/slashfan"
18 | },
19 | {
20 | "name": "Cedric Girard",
21 | "email": "c.girard@lexik.fr",
22 | "homepage": "https://github.com/cedric-g"
23 | },
24 | {
25 | "name": "Dev Lexik",
26 | "email": "dev@lexik.fr",
27 | "homepage": "https://github.com/lexik"
28 | },
29 | {
30 | "name": "Robin Chalas",
31 | "email": "robin.chalas@gmail.com",
32 | "homepage": "https://github.com/chalasr"
33 | },
34 | {
35 | "name": "Lexik Community",
36 | "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
37 | }
38 | ],
39 | "require": {
40 | "php": ">=8.2",
41 | "ext-openssl": "*",
42 | "lcobucci/jwt": "^5.0",
43 | "lcobucci/clock": "^3.0",
44 | "symfony/config": "^6.4|^7.0",
45 | "symfony/dependency-injection": "^6.4|^7.0",
46 | "symfony/deprecation-contracts": "^2.4|^3.0",
47 | "symfony/event-dispatcher": "^6.4|^7.0",
48 | "symfony/http-foundation": "^6.4|^7.0",
49 | "symfony/http-kernel": "^6.4|^7.0",
50 | "symfony/property-access": "^6.4|^7.0",
51 | "symfony/security-bundle": "^6.4|^7.0",
52 | "symfony/translation-contracts": "^1.0|^2.0|^3.0"
53 | },
54 | "require-dev": {
55 | "api-platform/core": "^3.0|^4.0",
56 | "rector/rector": "^1.2",
57 | "symfony/browser-kit": "^6.4|^7.0",
58 | "symfony/console": "^6.4|^7.0",
59 | "symfony/dom-crawler": "^6.4|^7.0",
60 | "symfony/filesystem": "^6.4|^7.0",
61 | "symfony/framework-bundle": "^6.4|^7.0",
62 | "symfony/phpunit-bridge": "^6.4|^7.0",
63 | "symfony/var-dumper": "^6.4|^7.0",
64 | "symfony/yaml": "^6.4|^7.0"
65 | },
66 | "suggest": {
67 | "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
68 | "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
69 | },
70 | "autoload": {
71 | "psr-4": {
72 | "Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
73 | },
74 | "exclude-from-classmap": [
75 | "/Tests/"
76 | ]
77 | },
78 | "config": {
79 | "sort-packages": true
80 | },
81 | "scripts": {
82 | "test": [
83 | "vendor/bin/simple-phpunit --exclude-group web-token",
84 | "ENCODER=lcobucci vendor/bin/simple-phpunit --exclude-group web-token",
85 | "ENCODER=lcobucci ALGORITHM=HS256 vendor/bin/simple-phpunit --exclude-group web-token",
86 | "ENCODER=user_id_claim vendor/bin/simple-phpunit --exclude-group web-token",
87 | "PROVIDER=lexik_jwt vendor/bin/simple-phpunit --exclude-group web-token"
88 | ]
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | sets([SetList::PSR_12]);
13 | $config->rule(OrderedImportsFixer::class);
14 | $config->ruleWithConfiguration(ArraySyntaxFixer::class, [
15 | 'syntax' => 'short',
16 | ]);
17 |
18 | $config->parallel();
19 | $config->paths([__DIR__]);
20 | $config->skip([
21 | __DIR__ . '/.github',
22 | __DIR__ . '/vendor',
23 | PhpdocScalarFixer::class
24 | ]);
25 | };
26 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | sets([
14 | LevelSetList::UP_TO_PHP_71,
15 | SymfonySetList::SYMFONY_CODE_QUALITY,
16 | SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
17 | ]);
18 | $rectorConfig->phpVersion(\Rector\ValueObject\PhpVersion::PHP_71);
19 | $rectorConfig->importShortClasses(false);
20 | $rectorConfig->importNames();
21 | $rectorConfig->bootstrapFiles([
22 | __DIR__ . '/vendor/autoload.php',
23 | ]);
24 | $rectorConfig->parallel();
25 | $rectorConfig->paths([__DIR__]);
26 | $rectorConfig->skip([
27 | // Path
28 | __DIR__ . '/.github',
29 | __DIR__ . '/DependencyInjection/Configuration.php',
30 | __DIR__ . '/Tests/DependencyInjection/LexikJWTAuthenticationExtensionTest.php',
31 | __DIR__ . '/vendor',
32 |
33 | // Rules
34 | JsonThrowOnErrorRector::class,
35 | ReturnNeverTypeRector::class => [
36 | __DIR__ . '/Security/User/JWTUserProvider.php',
37 | ],
38 | ]);
39 | };
40 |
--------------------------------------------------------------------------------