├── 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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 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 | --------------------------------------------------------------------------------