The start of something beautiful

This commit is contained in:
2024-09-11 22:48:07 -06:00
parent 45acea47f3
commit f5997ee5ec
5614 changed files with 630696 additions and 0 deletions
@@ -0,0 +1,50 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Command;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
#[AsCommand(name: 'lexik:jwt:check-config', description: 'Checks that the bundle is properly configured.')]
final class CheckConfigCommand extends Command
{
private KeyLoaderInterface $keyLoader;
private string $signatureAlgorithm;
public function __construct(KeyLoaderInterface $keyLoader, string $signatureAlgorithm)
{
$this->keyLoader = $keyLoader;
$this->signatureAlgorithm = $signatureAlgorithm;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE);
// No public key for HMAC
if (!str_contains($this->signatureAlgorithm, 'HS')) {
$this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC);
}
} catch (\RuntimeException $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
$output->writeln('<info>The configuration seems correct.</info>');
return Command::SUCCESS;
}
}
@@ -0,0 +1,349 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Command;
use Jose\Bundle\JoseFramework\JoseFrameworkBundle;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManagerFactory;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\JWELoader;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\JWSLoader;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Bundle\FrameworkBundle\Command\AbstractConfigCommand;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Compiler\ValidateEnvPlaceholdersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\Yaml\Yaml;
/**
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
#[AsCommand(name: 'lexik:jwt:enable-encryption', description: 'Enable Web-Token encryption support.')]
final class EnableEncryptionConfigCommand extends AbstractConfigCommand
{
/**
* @deprecated
*/
protected static $defaultName = 'lexik:jwt:enable-encryption';
/**
* @var ?AlgorithmManagerFactory
*/
private $algorithmManagerFactory;
public function __construct(
?AlgorithmManagerFactory $algorithmManagerFactory = null
) {
parent::__construct();
$this->algorithmManagerFactory = $algorithmManagerFactory;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setName(static::$defaultName)
->setDescription('Enable Web-Token encryption support.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the modification of the configuration, even if already set.')
;
}
public function isEnabled(): bool
{
return $this->algorithmManagerFactory !== null;
}
/**
* {@inheritdoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$force = $input->getOption('force');
$this->checkRequirements();
$io = new SymfonyStyle($input, $output);
$io->title('Web-Token Encryption support');
$io->info('This tool will help you enabling the encryption support for Web-Token');
$algorithms = $this->algorithmManagerFactory->all();
$availableKeyEncryptionAlgorithms = array_map(
static function (Algorithm $algorithm): string {
return $algorithm->name();
},
array_filter($algorithms, static function (Algorithm $algorithm): bool {
return ($algorithm instanceof KeyEncryptionAlgorithm && $algorithm->name() !== 'dir');
})
);
$availableContentEncryptionAlgorithms = array_map(
static function (Algorithm $algorithm): string {
return $algorithm->name();
},
array_filter($algorithms, static function (Algorithm $algorithm): bool {
return $algorithm instanceof ContentEncryptionAlgorithm;
})
);
$keyEncryptionAlgorithmAlias = $io->choice('Key Encryption Algorithm', $availableKeyEncryptionAlgorithms);
$contentEncryptionAlgorithmAlias = $io->choice('Content Encryption Algorithm', $availableContentEncryptionAlgorithms);
$keyEncryptionAlgorithm = $algorithms[$keyEncryptionAlgorithmAlias];
$contentEncryptionAlgorithm = $algorithms[$contentEncryptionAlgorithmAlias];
$continueOnDecryptionFailure = 'yes' === $io->choice('Continue decryption on failure', ['yes', 'no'], 'no');
$extension = $this->findExtension('lexik_jwt_authentication');
$config = $this->getConfiguration($extension);
if (!isset($config['encoder']['service']) || $config['encoder']['service'] !== 'lexik_jwt_authentication.encoder.web_token') {
$io->error('Please migrate to WebToken first.');
return self::FAILURE;
}
if (!$force && ($config['access_token_issuance']['encryption']['enabled'] || $config['access_token_verification']['encryption']['enabled'])) {
$io->error('Encryption support is already enabled.');
return self::FAILURE;
}
$key = $this->generatePrivateKey($keyEncryptionAlgorithm);
$keyset = $this->generatePublicKeyset($key, $keyEncryptionAlgorithm->name());
$config['access_token_issuance']['encryption'] = [
'enabled' => true,
'key_encryption_algorithm' => $keyEncryptionAlgorithm->name(),
'content_encryption_algorithm' => $contentEncryptionAlgorithm->name(),
'key' => json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
];
$config['access_token_verification']['encryption'] = [
'enabled' => true,
'continue_on_decryption_failure' => $continueOnDecryptionFailure,
'header_checkers' => ['iat_with_clock_skew', 'nbf_with_clock_skew', 'exp_with_clock_skew'],
'allowed_key_encryption_algorithms' => [$keyEncryptionAlgorithm->name()],
'allowed_content_encryption_algorithms' => [$contentEncryptionAlgorithm->name()],
'keyset' => json_encode($keyset, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
];
$io->comment('Please replace the current configuration with the following parameters.');
$io->section('# config/packages/lexik_jwt_authentication.yaml');
$io->writeln(Yaml::dump([$extension->getAlias() => $config], 10));
$io->section('# End of file');
return self::SUCCESS;
}
private function generatePublicKeyset(JWK $key, string $algorithm): JWKSet
{
$keyset = new JWKSet([$key->toPublic()]);
switch ($key->get('kty')) {
case 'oct':
return $this->withOctKeys($keyset, $algorithm);
case 'OKP':
return $this->withOkpKeys($keyset, $algorithm, $key->get('crv'));
case 'EC':
return $this->withEcKeys($keyset, $algorithm, $key->get('crv'));
case 'RSA':
return $this->withRsaKeys($keyset, $algorithm);
default:
throw new \InvalidArgumentException('Unsupported key type.');
}
}
private function withOctKeys(JWKSet $keyset, string $algorithm): JWKSet
{
$size = $this->getKeySize($algorithm);
return $keyset
->with($this->createOctKey($size, $algorithm)->toPublic())
->with($this->createOctKey($size, $algorithm)->toPublic())
;
}
private function withRsaKeys(JWKSet $keyset, string $algorithm): JWKSet
{
return $keyset
->with($this->createRsaKey(2048, $algorithm)->toPublic())
->with($this->createRsaKey(2048, $algorithm)->toPublic())
;
}
private function withOkpKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
{
return $keyset
->with($this->createOkpKey($curve, $algorithm)->toPublic())
->with($this->createOkpKey($curve, $algorithm)->toPublic())
;
}
private function withEcKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
{
return $keyset
->with($this->createEcKey($curve, $algorithm)->toPublic())
->with($this->createEcKey($curve, $algorithm)->toPublic())
;
}
private function generatePrivateKey(KeyEncryptionAlgorithm $algorithm): JWK
{
$keyType = current($algorithm->allowedKeyTypes());
switch ($keyType) {
case 'oct':
return $this->createOctKey($this->getKeySize($algorithm->name()), $algorithm->name());
case 'OKP':
return $this->createOkpKey('X25519', $algorithm->name());
case 'EC':
return $this->createEcKey('P-256', $algorithm->name());
case 'RSA':
return $this->createRsaKey($this->getKeySize($algorithm->name()), $algorithm->name());
default:
throw new \InvalidArgumentException('Unsupported key type.');
}
}
private function checkRequirements(): void
{
$requirements = [
JoseFrameworkBundle::class => 'web-token/jwt-bundle',
JWKFactory::class => 'web-token/jwt-key-mgmt',
ClaimCheckerManager::class => 'web-token/jwt-checker',
JWEBuilder::class => 'web-token/jwt-encryption',
];
if ($this->algorithmManagerFactory === null) {
throw new \RuntimeException('The package "web-token/jwt-bundle" is missing. Please install it for using this migration tool.');
}
foreach (array_keys($requirements) as $requirement) {
if (!class_exists($requirement)) {
throw new \RuntimeException(sprintf('The package "%s" is missing. Please install it for using this migration tool.', $requirement));
}
}
}
private function getConfiguration(ExtensionInterface $extension): array
{
$container = $this->compileContainer();
$config = $this->getConfig($extension, $container);
$uselessParameters = ['secret_key', 'public_key', 'pass_phrase', 'private_key_path', 'public_key_path', 'additional_public_keys'];
foreach ($uselessParameters as $parameter) {
unset($config[$parameter]);
}
return $config;
}
private function createOctKey(int $size, string $algorithm): JWK
{
return JWKFactory::createOctKey($size, $this->getOptions($algorithm));
}
private function createRsaKey(int $size, string $algorithm): JWK
{
return JWKFactory::createRSAKey($size, $this->getOptions($algorithm));
}
private function createOkpKey(string $curve, string $algorithm): JWK
{
return JWKFactory::createOKPKey($curve, $this->getOptions($algorithm));
}
private function createEcKey(string $curve, string $algorithm): JWK
{
return JWKFactory::createECKey($curve, $this->getOptions($algorithm));
}
private function compileContainer(): ContainerBuilder
{
$kernel = clone $this->getApplication()->getKernel();
$kernel->boot();
$method = new \ReflectionMethod($kernel, 'buildContainer');
$container = $method->invoke($kernel);
$container->getCompiler()->compile($container);
return $container;
}
private function getConfig(ExtensionInterface $extension, ContainerBuilder $container)
{
return $container->resolveEnvPlaceholders(
$container->getParameterBag()->resolveValue(
$this->getConfigForExtension($extension, $container)
)
);
}
private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array
{
$extensionAlias = $extension->getAlias();
$extensionConfig = [];
foreach ($container->getCompilerPassConfig()->getPasses() as $pass) {
if ($pass instanceof ValidateEnvPlaceholdersPass) {
$extensionConfig = $pass->getExtensionConfig();
break;
}
}
if (isset($extensionConfig[$extensionAlias])) {
return $extensionConfig[$extensionAlias];
}
// Fall back to default config if the extension has one
if (!$extension instanceof ConfigurationExtensionInterface) {
throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias));
}
$configs = $container->getExtensionConfig($extensionAlias);
$configuration = $extension->getConfiguration($configs, $container);
$this->validateConfiguration($extension, $configuration);
return (new Processor())->processConfiguration($configuration, $configs);
}
private function getKeySize(string $algorithm): int
{
switch ($algorithm) {
case 'RSA1_5':
case 'RSA-OAEP':
case 'RSA-OAEP-256':
return 4096;
case 'A128KW':
case 'A128GCMKW':
case 'PBES2-HS256+A128KW':
return 128;
case 'A192KW':
case 'A192GCMKW':
case 'PBES2-HS384+A192KW':
return 192;
case 'A256KW':
case 'A256GCMKW':
case 'PBES2-HS512+A256KW':
return 256;
default:
throw new \LogicException('Unsupported algorithm');
}
}
private function getOptions(string $algorithm): array
{
return [
'use' => 'enc',
'alg' => $algorithm,
'kid' => Base64UrlSafe::encodeUnpadded(random_bytes(16))
];
}
}
@@ -0,0 +1,217 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
/**
* @author Beno!t POLASZEK <bpolaszek@gmail.com>
*/
#[AsCommand(name: self::NAME, description: 'Generate public/private keys for use in your application.')]
final class GenerateKeyPairCommand extends Command
{
private const NAME = 'lexik:jwt:generate-keypair';
private const ACCEPTED_ALGORITHMS = [
'RS256',
'RS384',
'RS512',
'HS256',
'HS384',
'HS512',
'ES256',
'ES384',
'ES512',
];
private Filesystem $filesystem;
private ?string $secretKey;
private ?string $publicKey;
private ?string $passphrase;
private string $algorithm;
public function __construct(Filesystem $filesystem, ?string $secretKey, ?string $publicKey, ?string $passphrase, string $algorithm)
{
$this->filesystem = $filesystem;
$this->secretKey = $secretKey;
$this->publicKey = $publicKey;
$this->passphrase = $passphrase;
$this->algorithm = $algorithm;
parent::__construct();
}
protected function configure(): void
{
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not update key files.');
$this->addOption('skip-if-exists', null, InputOption::VALUE_NONE, 'Do not update key files if they already exist.');
$this->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite key files if they already exist.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if (!in_array($this->algorithm, self::ACCEPTED_ALGORITHMS, true)) {
$io->error(sprintf('Cannot generate key pair with the provided algorithm `%s`.', $this->algorithm));
return Command::FAILURE;
}
[$secretKey, $publicKey] = $this->generateKeyPair($this->passphrase);
if (true === $input->getOption('dry-run')) {
$io->success('Your keys have been generated!');
$io->newLine();
$io->writeln(sprintf('Update your private key in <info>%s</info>:', $this->secretKey));
$io->writeln($secretKey);
$io->newLine();
$io->writeln(sprintf('Update your public key in <info>%s</info>:', $this->publicKey));
$io->writeln($publicKey);
return Command::SUCCESS;
}
if (null === $this->secretKey || null === $this->publicKey) {
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));
}
$alreadyExists = $this->filesystem->exists($this->secretKey) || $this->filesystem->exists($this->publicKey);
if ($alreadyExists) {
try {
$this->handleExistingKeys($input);
} catch (\RuntimeException $e) {
if (0 === $e->getCode()) {
$io->comment($e->getMessage());
return Command::SUCCESS;
}
$io->error($e->getMessage());
return Command::FAILURE;
}
if (!$io->confirm('You are about to replace your existing keys. Are you sure you wish to continue?')) {
$io->comment('Your action was canceled.');
return Command::SUCCESS;
}
}
$this->filesystem->dumpFile($this->secretKey, $secretKey);
$this->filesystem->dumpFile($this->publicKey, $publicKey);
$io->success('Done!');
return Command::SUCCESS;
}
private function handleExistingKeys(InputInterface $input): void
{
if (true === $input->getOption('skip-if-exists') && true === $input->getOption('overwrite')) {
throw new \RuntimeException('Both options `--skip-if-exists` and `--overwrite` cannot be combined.', 1);
}
if (true === $input->getOption('skip-if-exists')) {
throw new \RuntimeException('Your key files already exist, they won\'t be overriden.', 0);
}
if (false === $input->getOption('overwrite')) {
throw new \RuntimeException('Your keys already exist. Use the `--overwrite` option to force regeneration.', 1);
}
}
private function generateKeyPair(?string $passphrase): array
{
$config = $this->buildOpenSSLConfiguration();
$resource = \openssl_pkey_new($config);
if (false === $resource) {
throw new \RuntimeException(\openssl_error_string());
}
$success = \openssl_pkey_export($resource, $privateKey, $passphrase);
if (false === $success) {
throw new \RuntimeException(\openssl_error_string());
}
$publicKeyData = \openssl_pkey_get_details($resource);
if (false === $publicKeyData) {
throw new \RuntimeException(\openssl_error_string());
}
$publicKey = $publicKeyData['key'];
return [$privateKey, $publicKey];
}
private function buildOpenSSLConfiguration(): array
{
$digestAlgorithms = [
'RS256' => 'sha256',
'RS384' => 'sha384',
'RS512' => 'sha512',
'HS256' => 'sha256',
'HS384' => 'sha384',
'HS512' => 'sha512',
'ES256' => 'sha256',
'ES384' => 'sha384',
'ES512' => 'sha512',
];
$privateKeyBits = [
'RS256' => 2048,
'RS384' => 2048,
'RS512' => 4096,
'HS256' => 512,
'HS384' => 512,
'HS512' => 512,
'ES256' => 384,
'ES384' => 512,
'ES512' => 1024,
];
$privateKeyTypes = [
'RS256' => \OPENSSL_KEYTYPE_RSA,
'RS384' => \OPENSSL_KEYTYPE_RSA,
'RS512' => \OPENSSL_KEYTYPE_RSA,
'HS256' => \OPENSSL_KEYTYPE_DH,
'HS384' => \OPENSSL_KEYTYPE_DH,
'HS512' => \OPENSSL_KEYTYPE_DH,
'ES256' => \OPENSSL_KEYTYPE_EC,
'ES384' => \OPENSSL_KEYTYPE_EC,
'ES512' => \OPENSSL_KEYTYPE_EC,
];
$curves = [
'ES256' => 'secp256k1',
'ES384' => 'secp384r1',
'ES512' => 'secp521r1',
];
$config = [
'digest_alg' => $digestAlgorithms[$this->algorithm],
'private_key_type' => $privateKeyTypes[$this->algorithm],
'private_key_bits' => $privateKeyBits[$this->algorithm],
];
if (isset($curves[$this->algorithm])) {
$config['curve_name'] = $curves[$this->algorithm];
}
return $config;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Command;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* GenerateTokenCommand.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
#[AsCommand(name: 'lexik:jwt:generate-token', description: 'Generates a JWT token for a given user.')]
class GenerateTokenCommand extends Command
{
private JWTTokenManagerInterface $tokenManager;
/** @var \Traversable<int, UserProviderInterface> */
private \Traversable $userProviders;
public function __construct(JWTTokenManagerInterface $tokenManager, \Traversable $userProviders)
{
$this->tokenManager = $tokenManager;
$this->userProviders = $userProviders;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->addArgument('username', InputArgument::REQUIRED, 'Username of user to be retreived from user provider')
->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')
->addOption('user-class', 'c', InputOption::VALUE_REQUIRED, 'Userclass is used to determine which user provider to use')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->userProviders instanceof \Countable && 0 === \count($this->userProviders)) {
throw new \RuntimeException('You must have at least 1 configured user provider to generate a token.');
}
if (!$userClass = $input->getOption('user-class')) {
if (1 < \count($userProviders = iterator_to_array($this->userProviders))) {
throw new \RuntimeException('The "--user-class" option must be passed as there is more than 1 configured user provider.');
}
$userProvider = current($userProviders);
} else {
$userProvider = null;
foreach ($this->userProviders as $provider) {
if ($provider->supportsClass($userClass)) {
$userProvider = $provider;
break;
}
}
if (null === $userProvider) {
throw new \RuntimeException(sprintf('There is no configured user provider for class "%s".', $userClass));
}
}
$user = $userProvider->loadUserByIdentifier($input->getArgument('username'));
$payload = [];
if (null !== $input->getOption('ttl') && ((int) $input->getOption('ttl')) == 0) {
$payload['exp'] = 0;
} elseif (null !== $input->getOption('ttl') && ((int) $input->getOption('ttl')) > 0) {
$payload['exp'] = time() + $input->getOption('ttl');
}
$token = $this->tokenManager->createFromPayload($user, $payload);
$output->writeln([
'',
'<info>' . $token . '</info>',
'',
]);
return Command::SUCCESS;
}
}
@@ -0,0 +1,322 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Command;
use Jose\Bundle\JoseFramework\JoseFrameworkBundle;
use Jose\Component\Checker\ClaimCheckerManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\JWSLoader;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Bundle\FrameworkBundle\Command\AbstractConfigCommand;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Compiler\ValidateEnvPlaceholdersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\Yaml\Yaml;
/**
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
#[AsCommand(name: 'lexik:jwt:migrate-config', description: 'Migrate LexikJWTAuthenticationBundle configuration to the Web-Token one.')]
final class MigrateConfigCommand extends AbstractConfigCommand
{
/**
* @deprecated
*/
protected static $defaultName = 'lexik:jwt:migrate-config';
/**
* @var KeyLoaderInterface
*/
private $keyLoader;
/**
* @var string
*/
private $signatureAlgorithm;
/**
* @var string
*/
private $passphrase;
public function __construct(
KeyLoaderInterface $keyLoader,
string $passphrase,
string $signatureAlgorithm
) {
parent::__construct();
$this->keyLoader = $keyLoader;
$this->passphrase = $passphrase === '' ? null : $passphrase;
$this->signatureAlgorithm = $signatureAlgorithm;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setName(static::$defaultName)
->setDescription('Migrate the configuration to Web-Token')
;
}
/**
* {@inheritdoc}
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->checkRequirements();
$io = new SymfonyStyle($input, $output);
$io->title('Web-Token Migration tool');
$io->info('This tool will help you converting the current LexikJWTAuthenticationBundle configuration to support Web-Token');
try {
$key = $this->getKey();
$keyset = $this->getKeyset($key, $this->signatureAlgorithm);
} catch (\RuntimeException $e) {
$io->error('An error occurred: ' . $e->getMessage());
return self::FAILURE;
}
$extension = $this->findExtension('lexik_jwt_authentication');
$config = $this->getConfiguration($extension);
foreach ($config['set_cookies'] as $cookieConfig) {
if ($cookieConfig['split'] !== []) {
$io->error('Web-Token is not compatible with the cookie split feature. Please disable this option before using this migration tool.');
return self::FAILURE;
}
}
$config['encoder'] = ['service' => 'lexik_jwt_authentication.encoder.web_token'];
$config['access_token_issuance'] = [
'enabled' => true,
'signature' => [
'signature_algorithm' => $this->signatureAlgorithm,
'key' => json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]
];
$config['access_token_verification'] = [
'enabled' => true,
'signature' => [
'allowed_signature_algorithms' => [$this->signatureAlgorithm],
'keyset' => json_encode($keyset, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]
];
$io->comment('Please replace the current configuration with the following parameters.');
$io->section('# config/packages/lexik_jwt_authentication.yaml');
$io->writeln(Yaml::dump([$extension->getAlias() => $config], 10));
$io->section('# End of file');
return self::SUCCESS;
}
private function getKeyset(JWK $key, string $algorithm): JWKSet
{
$keyset = new JWKSet([$key->toPublic()]);
switch ($key->get('kty')) {
case 'oct':
return $this->withOctKeys($keyset, $algorithm);
case 'OKP':
return $this->withOkpKeys($keyset, $algorithm, $key->get('crv'));
case 'EC':
return $this->withEcKeys($keyset, $algorithm, $key->get('crv'));
case 'RSA':
return $this->withRsaKeys($keyset, $algorithm);
default:
throw new \InvalidArgumentException('Unsupported key type.');
}
}
private function withOctKeys(JWKSet $keyset, string $algorithm): JWKSet
{
$size = $this->getKeySize($algorithm);
return $keyset
->with($this->createOctKey($size, $algorithm)->toPublic())
->with($this->createOctKey($size, $algorithm)->toPublic())
;
}
private function withRsaKeys(JWKSet $keyset, string $algorithm): JWKSet
{
return $keyset
->with($this->createRsaKey(2048, $algorithm)->toPublic())
->with($this->createRsaKey(2048, $algorithm)->toPublic())
;
}
private function withOkpKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
{
return $keyset
->with($this->createOkpKey($curve, $algorithm)->toPublic())
->with($this->createOkpKey($curve, $algorithm)->toPublic())
;
}
private function withEcKeys(JWKSet $keyset, string $algorithm, string $curve): JWKSet
{
return $keyset
->with($this->createEcKey($curve, $algorithm)->toPublic())
->with($this->createEcKey($curve, $algorithm)->toPublic())
;
}
private function getKey(): JWK
{
$additionalValues = [
'use' => 'sig',
'alg' => $this->signatureAlgorithm,
];
// No public key for HMAC
if (false !== strpos($this->signatureAlgorithm, 'HS')) {
return JWKFactory::createFromSecret(
$this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC),
$additionalValues
);
}
return JWKFactory::createFromKey(
$this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE),
$this->passphrase,
$additionalValues
);
}
private function checkRequirements(): void
{
$requirements = [
JoseFrameworkBundle::class => 'web-token/jwt-bundle',
JWKFactory::class => 'web-token/jwt-key-mgmt',
ClaimCheckerManager::class => 'web-token/jwt-checker',
JWSBuilder::class => 'web-token/jwt-signature',
];
foreach (array_keys($requirements) as $requirement) {
if (!class_exists($requirement)) {
throw new \RuntimeException(sprintf('The package "%s" is missing. Please install it for using this migration tool.', $requirement));
}
}
}
private function getConfiguration(ExtensionInterface $extension): array
{
$container = $this->compileContainer();
$config = $this->getConfig($extension, $container);
$uselessParameters = ['secret_key', 'public_key', 'pass_phrase', 'private_key_path', 'public_key_path', 'additional_public_keys'];
foreach ($uselessParameters as $parameter) {
unset($config[$parameter]);
}
return $config;
}
private function createOctKey(int $size, string $algorithm): JWK
{
return JWKFactory::createOctKey($size, $this->getOptions($algorithm));
}
private function createRsaKey(int $size, string $algorithm): JWK
{
return JWKFactory::createRSAKey($size, $this->getOptions($algorithm));
}
private function createOkpKey(string $curve, string $algorithm): JWK
{
return JWKFactory::createOKPKey($curve, $this->getOptions($algorithm));
}
private function createEcKey(string $curve, string $algorithm): JWK
{
return JWKFactory::createECKey($curve, $this->getOptions($algorithm));
}
private function compileContainer(): ContainerBuilder
{
$kernel = clone $this->getApplication()->getKernel();
$kernel->boot();
$method = new \ReflectionMethod($kernel, 'buildContainer');
$container = $method->invoke($kernel);
$container->getCompiler()->compile($container);
return $container;
}
private function getConfig(ExtensionInterface $extension, ContainerBuilder $container)
{
return $container->resolveEnvPlaceholders(
$container->getParameterBag()->resolveValue(
$this->getConfigForExtension($extension, $container)
)
);
}
private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array
{
$extensionAlias = $extension->getAlias();
$extensionConfig = [];
foreach ($container->getCompilerPassConfig()->getPasses() as $pass) {
if ($pass instanceof ValidateEnvPlaceholdersPass) {
$extensionConfig = $pass->getExtensionConfig();
break;
}
}
if (isset($extensionConfig[$extensionAlias])) {
return $extensionConfig[$extensionAlias];
}
// Fall back to default config if the extension has one
if (!$extension instanceof ConfigurationExtensionInterface) {
throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias));
}
$configs = $container->getExtensionConfig($extensionAlias);
$configuration = $extension->getConfiguration($configs, $container);
$this->validateConfiguration($extension, $configuration);
return (new Processor())->processConfiguration($configuration, $configs);
}
private function getKeySize(string $algorithm): int
{
switch ($algorithm) {
case 'HS256':
case 'HS256/64':
return 256;
case 'HS384':
return 384;
case 'HS512':
return 512;
default:
throw new \LogicException('Unsupported algorithm');
}
}
private function getOptions(string $algorithm): array
{
return [
'use' => 'sig',
'alg' => $algorithm,
'kid' => Base64UrlSafe::encodeUnpadded(random_bytes(16))
];
}
}
@@ -0,0 +1,52 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ApiPlatformOpenApiPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('lexik_jwt_authentication.api_platform.openapi.factory') || !$container->hasParameter('security.firewalls')) {
return;
}
$checkPath = null;
$usernamePath = null;
$passwordPath = null;
$firewalls = $container->getParameter('security.firewalls');
foreach ($firewalls as $firewallName) {
if ($container->hasDefinition('security.authenticator.json_login.' . $firewallName)) {
$firewallOptions = $container->getDefinition('security.authenticator.json_login.' . $firewallName)->getArgument(4);
$checkPath = $firewallOptions['check_path'];
$usernamePath = $firewallOptions['username_path'];
$passwordPath = $firewallOptions['password_path'];
break;
}
}
$openApiFactoryDefinition = $container->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory');
$checkPathArg = $openApiFactoryDefinition->getArgument(1);
$usernamePathArg = $openApiFactoryDefinition->getArgument(2);
$passwordPathArg = $openApiFactoryDefinition->getArgument(3);
if (!$checkPath && !$checkPathArg) {
$container->removeDefinition('lexik_jwt_authentication.api_platform.openapi.factory');
return;
}
if (!$checkPathArg) {
$openApiFactoryDefinition->replaceArgument(1, $checkPath);
}
if (!$usernamePathArg) {
$openApiFactoryDefinition->replaceArgument(2, $usernamePath ?? 'username');
}
if (!$passwordPathArg) {
$openApiFactoryDefinition->replaceArgument(3, $passwordPath ?? 'password');
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CollectPayloadEnrichmentsPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('lexik_jwt_authentication.payload_enrichment')) {
return;
}
$container->getDefinition('lexik_jwt_authentication.payload_enrichment')
->replaceArgument(0, $this->findAndSortTaggedServices('lexik_jwt_authentication.payload_enrichment', $container));
}
}
@@ -0,0 +1,21 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class WireGenerateTokenCommandPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('lexik_jwt_authentication.generate_token_command') || !$container->hasDefinition('security.context_listener')) {
return;
}
$container
->getDefinition('lexik_jwt_authentication.generate_token_command')
->replaceArgument(1, $container->getDefinition('security.context_listener')->getArgument(1))
;
}
}
@@ -0,0 +1,294 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection;
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\HttpFoundation\Cookie;
/**
* LexikJWTAuthenticationBundle Configuration.
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('lexik_jwt_authentication');
$treeBuilder
->getRootNode()
->addDefaultsIfNotSet()
->children()
->scalarNode('public_key')
->info('The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key.')
->defaultNull()
->end()
->arrayNode('additional_public_keys')
->info('Multiple public keys to try to verify token signature. If none is given, it will use the key provided in "public_key".')
->scalarPrototype()->end()
->end()
->scalarNode('secret_key')
->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.')
->defaultNull()
->end()
->scalarNode('pass_phrase')
->info('The key passphrase (useless for HMAC)')
->defaultValue('')
->end()
->scalarNode('token_ttl')
->defaultValue(3600)
->end()
->booleanNode('allow_no_expiration')
->info('Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare.')
->defaultFalse()
->end()
->scalarNode('clock_skew')
->defaultValue(0)
->end()
->arrayNode('encoder')
->addDefaultsIfNotSet()
->children()
->scalarNode('service')
->defaultValue('lexik_jwt_authentication.encoder.lcobucci')
->end()
->scalarNode('signature_algorithm')
->defaultValue('RS256')
->cannotBeEmpty()
->end()
->end()
->end()
->scalarNode('user_id_claim')
->defaultValue('username')
->cannotBeEmpty()
->end()
->append($this->getTokenExtractorsNode())
->scalarNode('remove_token_from_body_when_cookies_used')
->defaultTrue()
->end()
->arrayNode('set_cookies')
->fixXmlConfig('set_cookie')
->normalizeKeys(false)
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('lifetime')
->defaultNull()
->info('The cookie lifetime. If null, the "token_ttl" option value will be used')
->end()
->enumNode('samesite')
->values([Cookie::SAMESITE_NONE, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT])
->defaultValue(Cookie::SAMESITE_LAX)
->end()
->scalarNode('path')->defaultValue('/')->cannotBeEmpty()->end()
->scalarNode('domain')->defaultNull()->end()
->scalarNode('secure')->defaultTrue()->end()
->scalarNode('httpOnly')->defaultTrue()->end()
->scalarNode('partitioned')->defaultFalse()->end()
->arrayNode('split')
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->arrayNode('api_platform')
->canBeEnabled()
->info('API Platform compatibility: add check_path in OpenAPI documentation.')
->children()
->scalarNode('check_path')
->defaultNull()
->info('The login check path to add in OpenAPI.')
->end()
->scalarNode('username_path')
->defaultNull()
->info('The path to the username in the JSON body.')
->end()
->scalarNode('password_path')
->defaultNull()
->info('The path to the password in the JSON body.')
->end()
->end()
->end()
->arrayNode('access_token_issuance')
->fixXmlConfig('access_token_issuance')
->canBeEnabled()
->children()
->arrayNode('signature')
->fixXmlConfig('signature')
->addDefaultsIfNotSet()
->children()
->scalarNode('algorithm')
->isRequired()
->info('The algorithm use to sign the access tokens.')
->end()
->scalarNode('key')
->isRequired()
->info('The signature key. It shall be JWK encoded.')
->end()
->end()
->end()
->arrayNode('encryption')
->fixXmlConfig('encryption')
->canBeEnabled()
->children()
->scalarNode('key_encryption_algorithm')
->isRequired()
->cannotBeEmpty()
->info('The key encryption algorithm is used to encrypt the token.')
->end()
->scalarNode('content_encryption_algorithm')
->isRequired()
->cannotBeEmpty()
->info('The key encryption algorithm is used to encrypt the token.')
->end()
->scalarNode('key')
->isRequired()
->info('The encryption key. It shall be JWK encoded.')
->end()
->end()
->end()
->end()
->end()
->arrayNode('access_token_verification')
->fixXmlConfig('access_token_verification')
->canBeEnabled()
->children()
->arrayNode('signature')
->fixXmlConfig('signature')
->addDefaultsIfNotSet()
->children()
->arrayNode('header_checkers')
->fixXmlConfig('header_checkers')
->scalarPrototype()->end()
->defaultValue([])
->info('The headers to be checked for validating the JWS.')
->end()
->arrayNode('claim_checkers')
->fixXmlConfig('claim_checkers')
->scalarPrototype()->end()
->defaultValue(['exp_with_clock_skew', 'iat_with_clock_skew', 'nbf_with_clock_skew'])
->info('The claims to be checked for validating the JWS.')
->end()
->arrayNode('mandatory_claims')
->fixXmlConfig('mandatory_claims')
->scalarPrototype()->end()
->defaultValue([])
->info('The list of claims that shall be present in the JWS.')
->end()
->arrayNode('allowed_algorithms')
->fixXmlConfig('allowed_algorithms')
->scalarPrototype()->end()
->requiresAtLeastOneElement()
->info('The algorithms allowed to be used for token verification.')
->end()
->scalarNode('keyset')
->isRequired()
->info('The signature keyset. It shall be JWKSet encoded.')
->end()
->end()
->end()
->arrayNode('encryption')
->fixXmlConfig('encryption')
->canBeEnabled()
->children()
->booleanNode('continue_on_decryption_failure')
->defaultFalse()
->info('If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted.')
->end()
->arrayNode('header_checkers')
->fixXmlConfig('header_checkers')
->scalarPrototype()->end()
->defaultValue(['iat_with_clock_skew', 'nbf_with_clock_skew', 'exp_with_clock_skew'])
->info('The headers to be checked for validating the JWE.')
->end()
->arrayNode('allowed_key_encryption_algorithms')
->fixXmlConfig('allowed_key_encryption_algorithms')
->scalarPrototype()->end()
->requiresAtLeastOneElement()
->info('The key encryption algorithm is used to encrypt the token.')
->end()
->arrayNode('allowed_content_encryption_algorithms')
->fixXmlConfig('allowed_content_encryption_algorithms')
->scalarPrototype()->end()
->requiresAtLeastOneElement()
->info('The key encryption algorithm is used to encrypt the token.')
->end()
->scalarNode('keyset')
->isRequired()
->info('The encryption keyset. It shall be JWKSet encoded.')
->end()
->end()
->end()
->end()
->end()
->arrayNode('blocklist_token')
->addDefaultsIfNotSet()
->canBeEnabled()
->children()
->scalarNode('cache')
->defaultValue('cache.app')
->info('Storage to track blocked tokens')
->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
private function getTokenExtractorsNode(): ArrayNodeDefinition
{
$builder = new TreeBuilder('token_extractors');
$node = $builder->getRootNode();
$node
->addDefaultsIfNotSet()
->children()
->arrayNode('authorization_header')
->addDefaultsIfNotSet()
->canBeDisabled()
->children()
->scalarNode('prefix')
->defaultValue('Bearer')
->end()
->scalarNode('name')
->defaultValue('Authorization')
->end()
->end()
->end()
->arrayNode('cookie')
->addDefaultsIfNotSet()
->canBeEnabled()
->children()
->scalarNode('name')
->defaultValue('BEARER')
->end()
->end()
->end()
->arrayNode('query_parameter')
->addDefaultsIfNotSet()
->canBeEnabled()
->children()
->scalarNode('name')
->defaultValue('bearer')
->end()
->end()
->end()
->arrayNode('split_cookie')
->canBeEnabled()
->children()
->arrayNode('cookies')
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
;
return $node;
}
}
@@ -0,0 +1,247 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection;
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Kernel;
/**
* This is the class that loads and manages your bundle configuration.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class LexikJWTAuthenticationExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('jwt_manager.xml');
$loader->load('key_loader.xml');
$loader->load('lcobucci.xml');
$loader->load('response_interceptor.xml');
$loader->load('token_authenticator.xml');
$loader->load('token_extractor.xml');
if (empty($config['public_key']) && empty($config['secret_key'])) {
$e = new InvalidConfigurationException('You must either configure a "public_key" or a "secret_key".');
$e->setPath('lexik_jwt_authentication');
throw $e;
}
$container->setParameter('lexik_jwt_authentication.pass_phrase', $config['pass_phrase']);
$container->setParameter('lexik_jwt_authentication.token_ttl', $config['token_ttl']);
$container->setParameter('lexik_jwt_authentication.clock_skew', $config['clock_skew']);
$container->setParameter('lexik_jwt_authentication.allow_no_expiration', $config['allow_no_expiration']);
$container->setParameter('lexik_jwt_authentication.user_id_claim', $config['user_id_claim']);
$encoderConfig = $config['encoder'];
$container->setAlias('lexik_jwt_authentication.encoder', new Alias($encoderConfig['service'], true));
$container->setAlias(JWTEncoderInterface::class, 'lexik_jwt_authentication.encoder');
$container->setAlias(
'lexik_jwt_authentication.key_loader',
new Alias('lexik_jwt_authentication.key_loader.raw', true)
);
$container
->findDefinition('lexik_jwt_authentication.key_loader')
->replaceArgument(0, $config['secret_key'])
->replaceArgument(1, $config['public_key']);
if (isset($config['additional_public_keys'])) {
$container
->findDefinition('lexik_jwt_authentication.key_loader')
->replaceArgument(3, $config['additional_public_keys']);
}
$container->setParameter('lexik_jwt_authentication.encoder.signature_algorithm', $encoderConfig['signature_algorithm']);
$tokenExtractors = $this->createTokenExtractors($container, $config['token_extractors']);
$container
->getDefinition('lexik_jwt_authentication.extractor.chain_extractor')
->replaceArgument(0, $tokenExtractors);
if (isset($config['remove_token_from_body_when_cookies_used'])) {
$container
->getDefinition('lexik_jwt_authentication.handler.authentication_success')
->replaceArgument(3, $config['remove_token_from_body_when_cookies_used']);
}
if ($config['set_cookies']) {
$loader->load('cookie.xml');
$cookieProviders = [];
foreach ($config['set_cookies'] as $name => $attributes) {
if ($attributes['partitioned'] && Kernel::VERSION < '6.4') {
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));
}
$container
->setDefinition($id = "lexik_jwt_authentication.cookie_provider.$name", new ChildDefinition('lexik_jwt_authentication.cookie_provider'))
->replaceArgument(0, $name)
->replaceArgument(1, $attributes['lifetime'] ?? ($config['token_ttl'] ?: 0))
->replaceArgument(2, $attributes['samesite'])
->replaceArgument(3, $attributes['path'])
->replaceArgument(4, $attributes['domain'])
->replaceArgument(5, $attributes['secure'])
->replaceArgument(6, $attributes['httpOnly'])
->replaceArgument(7, $attributes['split'])
->replaceArgument(8, $attributes['partitioned']);
$cookieProviders[] = new Reference($id);
}
$container
->getDefinition('lexik_jwt_authentication.handler.authentication_success')
->replaceArgument(2, new IteratorArgument($cookieProviders));
}
if (class_exists(Application::class)) {
$loader->load('console.xml');
$container
->getDefinition('lexik_jwt_authentication.generate_keypair_command')
->replaceArgument(1, $config['secret_key'])
->replaceArgument(2, $config['public_key'])
->replaceArgument(3, $config['pass_phrase'])
->replaceArgument(4, $encoderConfig['signature_algorithm']);
if (!$container->hasParameter('kernel.debug') || !$container->getParameter('kernel.debug')) {
$container->removeDefinition('lexik_jwt_authentication.migrate_config_command');
}
}
if ($this->isConfigEnabled($container, $config['api_platform'])) {
if (!class_exists(ApiPlatformBundle::class)) {
throw new LogicException('API Platform cannot be detected. Try running "composer require api-platform/core".');
}
$loader->load('api_platform.xml');
$container
->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory')
->replaceArgument(1, $config['api_platform']['check_path'] ?? null)
->replaceArgument(2, $config['api_platform']['username_path'] ?? null)
->replaceArgument(3, $config['api_platform']['password_path'] ?? null);
}
$this->processWithWebTokenConfig($config, $container, $loader);
if ($this->isConfigEnabled($container, $config['blocklist_token'])) {
$loader->load('blocklist_token.xml');
$blockListTokenConfig = $config['blocklist_token'];
$container->setAlias('lexik_jwt_authentication.blocklist_token.cache', $blockListTokenConfig['cache']);
} else {
$container->getDefinition('lexik_jwt_authentication.payload_enrichment.random_jti_enrichment')
->clearTag('lexik_jwt_authentication.payload_enrichment');
}
}
private function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig): array
{
$map = [];
if ($this->isConfigEnabled($container, $tokenExtractorsConfig['authorization_header'])) {
$authorizationHeaderExtractorId = 'lexik_jwt_authentication.extractor.authorization_header_extractor';
$container
->getDefinition($authorizationHeaderExtractorId)
->replaceArgument(0, $tokenExtractorsConfig['authorization_header']['prefix'])
->replaceArgument(1, $tokenExtractorsConfig['authorization_header']['name']);
$map[] = new Reference($authorizationHeaderExtractorId);
}
if ($this->isConfigEnabled($container, $tokenExtractorsConfig['query_parameter'])) {
$queryParameterExtractorId = 'lexik_jwt_authentication.extractor.query_parameter_extractor';
$container
->getDefinition($queryParameterExtractorId)
->replaceArgument(0, $tokenExtractorsConfig['query_parameter']['name']);
$map[] = new Reference($queryParameterExtractorId);
}
if ($this->isConfigEnabled($container, $tokenExtractorsConfig['cookie'])) {
$cookieExtractorId = 'lexik_jwt_authentication.extractor.cookie_extractor';
$container
->getDefinition($cookieExtractorId)
->replaceArgument(0, $tokenExtractorsConfig['cookie']['name']);
$map[] = new Reference($cookieExtractorId);
}
if ($this->isConfigEnabled($container, $tokenExtractorsConfig['split_cookie'])) {
$cookieExtractorId = 'lexik_jwt_authentication.extractor.split_cookie_extractor';
$container
->getDefinition($cookieExtractorId)
->replaceArgument(0, $tokenExtractorsConfig['split_cookie']['cookies']);
$map[] = new Reference($cookieExtractorId);
}
return $map;
}
private function processWithWebTokenConfig(array $config, ContainerBuilder $container, LoaderInterface $loader): void
{
if ($config['access_token_issuance']['enabled'] === false && $config['access_token_verification']['enabled'] === false) {
return;
}
$loader->load('web_token.xml');
if ($config['access_token_issuance']['enabled'] === true) {
$loader->load('web_token_issuance.xml');
$accessTokenBuilder = 'lexik_jwt_authentication.access_token_builder';
$accessTokenBuilderDefinition = $container->getDefinition($accessTokenBuilder);
$accessTokenBuilderDefinition
->replaceArgument(3, $config['access_token_issuance']['signature']['algorithm'])
->replaceArgument(4, $config['access_token_issuance']['signature']['key'])
;
if ($config['access_token_issuance']['encryption']['enabled'] === true) {
$accessTokenBuilderDefinition
->replaceArgument(5, $config['access_token_issuance']['encryption']['key_encryption_algorithm'])
->replaceArgument(6, $config['access_token_issuance']['encryption']['content_encryption_algorithm'])
->replaceArgument(7, $config['access_token_issuance']['encryption']['key'])
;
}
}
if ($config['access_token_verification']['enabled'] === true) {
$loader->load('web_token_verification.xml');
$accessTokenLoader = 'lexik_jwt_authentication.access_token_loader';
$accessTokenLoaderDefinition = $container->getDefinition($accessTokenLoader);
$accessTokenLoaderDefinition
->replaceArgument(3, $config['access_token_verification']['signature']['claim_checkers'])
->replaceArgument(4, $config['access_token_verification']['signature']['header_checkers'])
->replaceArgument(5, $config['access_token_verification']['signature']['mandatory_claims'])
->replaceArgument(6, $config['access_token_verification']['signature']['allowed_algorithms'])
->replaceArgument(7, $config['access_token_verification']['signature']['keyset'])
;
if ($config['access_token_verification']['encryption']['enabled'] === true) {
$accessTokenLoaderDefinition
->replaceArgument(8, $config['access_token_verification']['encryption']['continue_on_decryption_failure'])
->replaceArgument(9, $config['access_token_verification']['encryption']['header_checkers'])
->replaceArgument(10, $config['access_token_verification']['encryption']['allowed_key_encryption_algorithms'])
->replaceArgument(11, $config['access_token_verification']['encryption']['allowed_content_encryption_algorithms'])
->replaceArgument(12, $config['access_token_verification']['encryption']['keyset'])
;
}
}
}
}
@@ -0,0 +1,61 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Wires the "jwt" authenticator from user configuration.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTAuthenticatorFactory implements AuthenticatorFactoryInterface
{
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return -10;
}
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return 'jwt';
}
public function addConfiguration(NodeDefinition $node): void
{
$node
->children()
->scalarNode('provider')
->defaultNull()
->end()
->scalarNode('authenticator')
->defaultValue('lexik_jwt_authentication.security.jwt_authenticator')
->end()
->end()
;
}
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
{
$authenticatorId = 'security.authenticator.jwt.' . $firewallName;
$userProviderId = empty($config['provider']) ? $userProviderId : 'security.user.provider.concrete.' . $config['provider'];
$container
->setDefinition($authenticatorId, new ChildDefinition($config['authenticator']))
->replaceArgument(3, new Reference($userProviderId))
;
return $authenticatorId;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUser;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Creates the `lexik_jwt` user provider.
*
* @internal
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class JWTUserFactory implements UserProviderFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array $config): void
{
$container->setDefinition($id, new ChildDefinition('lexik_jwt_authentication.security.jwt_user_provider'))
->replaceArgument(0, $config['class']);
}
public function getKey(): string
{
return 'lexik_jwt';
}
public function addConfiguration(NodeDefinition $node): void
{
$node
->children()
->scalarNode('class')
->cannotBeEmpty()
->defaultValue(JWTUser::class)
->validate()
->ifTrue(fn ($class) => !is_subclass_of($class, JWTUserInterface::class))
->thenInvalid('The %s class must implement ' . JWTUserInterface::class . ' for using the "lexik_jwt" user provider.')
->end()
->end()
->end()
;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Encoder;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
/**
* HeaderAwareJWTEncoderInterface.
*/
interface HeaderAwareJWTEncoderInterface extends JWTEncoderInterface
{
/**
* @return string the encoded token string
*
* @throws JWTEncodeFailureException If an error occurred while trying to create
* the token (invalid crypto key, invalid payload...)
*/
public function encode(array $data, array $header = []);
}
@@ -0,0 +1,32 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Encoder;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
/**
* JWTEncoderInterface.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
interface JWTEncoderInterface
{
/**
* @return string the encoded token string
*
* @throws JWTEncodeFailureException If an error occurred while trying to create
* the token (invalid crypto key, invalid payload...)
*/
public function encode(array $data);
/**
* @param string $token
*
* @return array
*
* @throws JWTDecodeFailureException If an error occurred while trying to load the token
* (invalid signature, invalid crypto key, expired token...)
*/
public function decode($token);
}
@@ -0,0 +1,66 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Encoder;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\JWSProviderInterface;
/**
* Json Web Token encoder/decoder based on the lcobucci/jwt library.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LcobucciJWTEncoder implements JWTEncoderInterface, HeaderAwareJWTEncoderInterface
{
protected JWSProviderInterface $jwsProvider;
public function __construct(JWSProviderInterface $jwsProvider)
{
$this->jwsProvider = $jwsProvider;
}
/**
* {@inheritdoc}
*/
public function encode(array $payload, array $header = [])
{
try {
$jws = $this->jwsProvider->create($payload, $header);
} catch (\InvalidArgumentException $e) {
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);
}
if (!$jws->isSigned()) {
throw new JWTEncodeFailureException(JWTEncodeFailureException::UNSIGNED_TOKEN, 'Unable to create a signed JWT from the given configuration.', null, $payload);
}
return $jws->getToken();
}
/**
* {@inheritdoc}
*/
public function decode($token)
{
try {
$jws = $this->jwsProvider->load($token);
} catch (\Exception $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', $e);
}
if ($jws->isInvalid()) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid JWT Token', null, $jws->getPayload());
}
if ($jws->isExpired()) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token', null, $jws->getPayload());
}
if (!$jws->isVerified()) {
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());
}
return $jws->getPayload();
}
}
@@ -0,0 +1,65 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Encoder;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken\AccessTokenBuilder;
use Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken\AccessTokenLoader;
/**
* Json Web Token encoder/decoder based on the web-token framework.
*
* @author Florent Morsellis <florent.morselli@spomky-labs.com>
*/
final class WebTokenEncoder implements HeaderAwareJWTEncoderInterface
{
/**
* @var AccessTokenBuilder|null
*/
private $accessTokenBuilder;
/**
* @var AccessTokenLoader|null
*/
private $accessTokenLoader;
public function __construct(?AccessTokenBuilder $accessTokenBuilder, ?AccessTokenLoader $accessTokenLoader)
{
$this->accessTokenBuilder = $accessTokenBuilder;
$this->accessTokenLoader = $accessTokenLoader;
}
/**
* {@inheritdoc}
*/
public function encode(array $payload, array $header = [])
{
if (!$this->accessTokenBuilder) {
throw new \LogicException('The access token issuance features are not enabled.');
}
try {
return $this->accessTokenBuilder->build($header, $payload);
} catch (\InvalidArgumentException $e) {
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);
}
}
/**
* {@inheritdoc}
*/
public function decode($token)
{
if (!$this->accessTokenLoader) {
throw new \LogicException('The access token verification features are not enabled.');
}
try {
return $this->accessTokenLoader->load($token);
} catch (JWTFailureException $e) {
throw $e;
}
}
}
@@ -0,0 +1,53 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Contracts\EventDispatcher\Event;
/**
* AuthenticationFailureEvent.
*
* @author Emmanuel Vella <vella.emmanuel@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class AuthenticationFailureEvent extends Event
{
protected AuthenticationException $exception;
protected ?Response $response;
protected ?Request $request;
public function __construct(?AuthenticationException $exception, ?Response $response, ?Request $request = null)
{
$this->exception = $exception;
$this->response = $response;
$this->request = $request;
}
public function getException(): AuthenticationException
{
return $this->exception;
}
public function getResponse(): ?Response
{
return $this->response;
}
public function setResponse(Response $response): void
{
$this->response = $response;
}
public function getRequest(): ?Request
{
return $this->request;
}
public function setRequest(Request $request)
{
$this->request = $request;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* AuthenticationSuccessEvent.
*
* @author Dev Lexik <dev@lexik.fr>
*/
class AuthenticationSuccessEvent extends Event
{
protected array $data;
protected UserInterface $user;
protected Response $response;
public function __construct(array $data, UserInterface $user, Response $response)
{
$this->data = $data;
$this->user = $user;
$this->response = $response;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function getUser(): UserInterface
{
return $this->user;
}
public function getResponse(): Response
{
return $this->response;
}
}
@@ -0,0 +1,44 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
/**
* BeforeJWEComputationEvent event is dispatched just before the computation of the encrypted token.
* This can be used to add or modify the JWE header parameters.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class BeforeJWEComputationEvent
{
private $header;
/**
* @param array<string, mixed> $header
*/
public function __construct(array $header)
{
$this->header = $header;
}
public function setHeader(string $key, mixed $value): self
{
$this->header[$key] = $value;
return $this;
}
public function removeHeader(string $key): self
{
unset($this->header[$key]);
return $this;
}
/**
* @return array<string, mixed>
*/
public function getHeader(): array
{
return $this->header;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* JWTAuthenticatedEvent.
*/
class JWTAuthenticatedEvent extends Event
{
protected array $payload;
protected TokenInterface $token;
public function __construct(array $payload, TokenInterface $token)
{
$this->payload = $payload;
$this->token = $token;
}
public function getPayload(): array
{
return $this->payload;
}
public function setPayload(array $payload)
{
$this->payload = $payload;
}
public function getToken(): TokenInterface
{
return $this->token;
}
}
@@ -0,0 +1,48 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* JWTCreatedEvent.
*/
class JWTCreatedEvent extends Event
{
protected array $header;
protected array $data;
protected UserInterface $user;
public function __construct(array $data, UserInterface $user, array $header = [])
{
$this->data = $data;
$this->user = $user;
$this->header = $header;
}
public function getHeader(): array
{
return $this->header;
}
public function setHeader(array $header)
{
$this->header = $header;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data)
{
$this->data = $data;
}
public function getUser(): UserInterface
{
return $this->user;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* JWTDecodedEvent.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
class JWTDecodedEvent extends Event
{
protected array $payload;
protected bool $isValid;
public function __construct(array $payload)
{
$this->payload = $payload;
$this->isValid = true;
}
public function getPayload(): array
{
return $this->payload;
}
public function setPayload(array $payload)
{
$this->payload = $payload;
}
/**
* Mark payload as invalid.
*/
public function markAsInvalid(): void
{
$this->isValid = false;
$this->stopPropagation();
}
public function isValid(): bool
{
return $this->isValid;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Contracts\EventDispatcher\Event;
class JWTEncodedEvent extends Event
{
private string $jwtString;
public function __construct(string $jwtString)
{
$this->jwtString = $jwtString;
}
public function getJWTString(): string
{
return $this->jwtString;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
/**
* JWTExpiredEvent.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTExpiredEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
{
}
@@ -0,0 +1,35 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Interface for event classes that are dispatched when a JWT cannot be authenticated.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface JWTFailureEventInterface
{
/**
* Gets the response that will be returned after dispatching a
* {@link JWTFailureEventInterface} implementation.
*
* @return Response
*/
public function getResponse();
/**
* Gets the tied AuthenticationException object.
*
* @return AuthenticationException
*/
public function getException();
/**
* Calling this allows to return a custom Response immediately after
* the corresponding implementation of this event is dispatched.
*/
public function setResponse(Response $response);
}
@@ -0,0 +1,12 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
/**
* JWTInvalidEvent.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTInvalidEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
{
}
@@ -0,0 +1,21 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* JWTNotFoundEvent event is dispatched when a JWT cannot be found in a request
* covered by a firewall secured via lexik_jwt.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTNotFoundEvent extends AuthenticationFailureEvent implements JWTFailureEventInterface
{
public function __construct(AuthenticationException $exception = null, Response $response = null, Request $request = null)
{
parent::__construct($exception, $response, $request);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\CacheItemPoolBlockedTokenManager;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class BlockJWTListener
{
private $blockedTokenManager;
private $tokenExtractor;
private $jwtManager;
public function __construct(
BlockedTokenManagerInterface $blockedTokenManager,
TokenExtractorInterface $tokenExtractor,
JWTTokenManagerInterface $jwtManager
) {
$this->blockedTokenManager = $blockedTokenManager;
$this->tokenExtractor = $tokenExtractor;
$this->jwtManager = $jwtManager;
}
public function onLoginFailure(LoginFailureEvent $event): void
{
$exception = $event->getException();
if (($exception instanceof DisabledException) || ($exception->getPrevious() instanceof DisabledException)) {
$this->blockTokenFromRequest($event->getRequest());
}
}
public function onLogout(LogoutEvent $event): void
{
$this->blockTokenFromRequest($event->getRequest());
}
private function blockTokenFromRequest(Request $request): void
{
$token = $this->tokenExtractor->extract($request);
if ($token === false) {
// There's nothing to block if the token isn't in the request
return;
}
try {
$payload = $this->jwtManager->parse($token);
} catch (JWTDecodeFailureException $e) {
// Ignore decode failures, this would mean the token is invalid anyway
return;
}
try {
$this->blockedTokenManager->add($payload);
} catch (MissingClaimException $e) {
// We can't block a token missing the claims our system requires, so silently ignore this one
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface;
class RejectBlockedTokenListener
{
private $blockedTokenManager;
public function __construct(BlockedTokenManagerInterface $blockedTokenManager)
{
$this->blockedTokenManager = $blockedTokenManager;
}
/**
* @throws InvalidTokenException If the JWT is blocked
*/
public function __invoke(JWTAuthenticatedEvent $event): void
{
try {
if ($this->blockedTokenManager->has($event->getPayload())) {
throw new InvalidTokenException('JWT blocked');
}
} catch (MissingClaimException) {
// Do nothing if the required claims do not exist on the payload (older JWTs won't have the "jti" claim the manager requires)
}
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle;
/**
* Events.
*
* @author Dev Lexik <dev@lexik.fr>
*/
final class Events
{
/**
* Dispatched after the token generation to allow sending more data
* on the authentication success response.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent")
*/
public const AUTHENTICATION_SUCCESS = 'lexik_jwt_authentication.on_authentication_success';
/**
* Dispatched after an authentication failure.
* Hook into this event to add a custom error message in the response body.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent")
*/
public const AUTHENTICATION_FAILURE = 'lexik_jwt_authentication.on_authentication_failure';
/**
* Dispatched before the token payload is encoded by the configured encoder (JWTEncoder by default).
* Hook into this event to add extra fields to the payload.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent")
*/
public const JWT_CREATED = 'lexik_jwt_authentication.on_jwt_created';
/**
* Dispatched right after token string is created.
* Hook into this event to get token representation itself.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent")
*/
public const JWT_ENCODED = 'lexik_jwt_authentication.on_jwt_encoded';
/**
* Dispatched after the token payload has been decoded by the configured encoder (JWTEncoder by default).
* Hook into this event to perform additional validation on the received payload.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent")
*/
public const JWT_DECODED = 'lexik_jwt_authentication.on_jwt_decoded';
/**
* Dispatched after the token payload has been authenticated by the provider.
* Hook into this event to perform additional modification to the authenticated token using the payload.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent")
*/
public const JWT_AUTHENTICATED = 'lexik_jwt_authentication.on_jwt_authenticated';
/**
* Dispatched after the token has been invalidated by the provider.
* Hook into this event to add a custom error message in the response body.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent")
*/
public const JWT_INVALID = 'lexik_jwt_authentication.on_jwt_invalid';
/**
* Dispatched when no token can be found in a request.
* Hook into this event to set a custom response.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent")
*/
public const JWT_NOT_FOUND = 'lexik_jwt_authentication.on_jwt_not_found';
/**
* Dispatched when the token is expired.
* The expired token's payload can be retrieved by hooking into this event, so you can set a different
* response.
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent")
*/
public const JWT_EXPIRED = 'lexik_jwt_authentication.on_jwt_expired';
/**
* Dispatched before the JWE is computed.
* This event allow the JWE header parameters to be changed.
* It is only dispatched when using Web-Token
*
* @Event("Lexik\Bundle\JWTAuthenticationBundle\Event\BeforeJWEComputationEvent")
*/
public const BEFORE_JWE_COMPUTATION = 'lexik_jwt_authentication.before_jwe_computation';
}
@@ -0,0 +1,22 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Exception that should be thrown from an authenticator during the authentication process
* if a token is expired.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ExpiredTokenException extends AuthenticationException
{
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
return 'Expired JWT Token';
}
}
@@ -0,0 +1,31 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Missing key in the token payload during authentication.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class InvalidPayloadException extends AuthenticationException
{
private string $invalidKey;
/**
* @param string $invalidKey The key that cannot be found in the payload
*/
public function __construct(string $invalidKey)
{
$this->invalidKey = $invalidKey;
}
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
return sprintf('Unable to find key "%s" in the token payload.', $this->invalidKey);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Exception to be thrown in case of invalid token during an authentication process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class InvalidTokenException extends AuthenticationException
{
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
return 'Invalid JWT Token';
}
}
@@ -0,0 +1,17 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
/**
* JWTDecodeFailureException is thrown if an error occurs in the token decoding process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTDecodeFailureException extends JWTFailureException
{
public const INVALID_TOKEN = 'invalid_token';
public const UNVERIFIED_TOKEN = 'unverified_token';
public const EXPIRED_TOKEN = 'expired_token';
}
@@ -0,0 +1,15 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
/**
* JWTEncodeFailureException is thrown if an error occurs in the token encoding process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTEncodeFailureException extends JWTFailureException
{
public const INVALID_CONFIG = 'invalid_config';
public const UNSIGNED_TOKEN = 'unsigned_token';
}
@@ -0,0 +1,32 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
/**
* Base class for exceptions thrown during JWT creation/loading.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTFailureException extends \Exception
{
private string $reason;
private ?array $payload;
public function __construct(string $reason, string $message, \Throwable $previous = null, array $payload = null)
{
$this->reason = $reason;
$this->payload = $payload;
parent::__construct($message, 0, $previous);
}
public function getReason(): string
{
return $this->reason;
}
public function getPayload(): ?array
{
return $this->payload;
}
}
@@ -0,0 +1,15 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Throwable;
class MissingClaimException extends JWTFailureException
{
public function __construct(
string $claim,
Throwable $previous = null
) {
parent::__construct('missing_claim', sprintf('Missing required "%s" claim on JWT payload.', $claim), $previous);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Exception to be thrown in case of invalid token during an authentication process.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class MissingTokenException extends AuthenticationException
{
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
return 'JWT Token not found';
}
}
@@ -0,0 +1,30 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* User not found during authentication.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class UserNotFoundException extends AuthenticationException
{
private string $userIdentityField;
private string $identity;
public function __construct(string $userIdentityField, string $identity)
{
$this->userIdentityField = $userIdentityField;
$this->identity = $identity;
}
/**
* {@inheritdoc}
*/
public function getMessageKey(): string
{
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);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Helper;
/**
* JWTSplitter.
*
* @author Adam Lukacovic <adam@adamlukacovic.sk>
*
* @final
*/
class JWTSplitter
{
private string $header;
private string $payload;
private string $signature;
/**
* @var string
*/
private $jwt;
public function __construct(string $jwt)
{
$this->jwt = $jwt;
[$this->header, $this->payload, $this->signature] = explode('.', $jwt);
}
public function getParts(array $parts = []): string
{
if (!$parts) {
return $this->jwt;
}
return implode('.', array_intersect_key(get_object_vars($this), array_flip($parts)));
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (C) 2014-2020 Lexik <dev@lexik.fr>, Robin Chalas <robin.chalas@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,40 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\ApiPlatformOpenApiPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\CollectPayloadEnrichmentsPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTAuthenticatorFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTUserFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* LexikJWTAuthenticationBundle.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
class LexikJWTAuthenticationBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(new WireGenerateTokenCommandPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ApiPlatformOpenApiPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new CollectPayloadEnrichmentsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');
$extension->addUserProviderFactory(new JWTUserFactory());
$extension->addAuthenticatorFactory(new JWTAuthenticatorFactory());
}
}
@@ -0,0 +1,122 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\OpenApi;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model\MediaType;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\RequestBody;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\HttpFoundation\Response;
/**
* Decorates API Platform OpenApiFactory.
*
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*
* @final
*/
class OpenApiFactory implements OpenApiFactoryInterface
{
private OpenApiFactoryInterface $decorated;
private string $checkPath;
private string $usernamePath;
private string $passwordPath;
public function __construct(OpenApiFactoryInterface $decorated, string $checkPath, string $usernamePath, string $passwordPath)
{
$this->decorated = $decorated;
$this->checkPath = $checkPath;
$this->usernamePath = $usernamePath;
$this->passwordPath = $passwordPath;
}
/**
* {@inheritdoc}
*/
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->decorated)($context);
$openApi
->getComponents()->getSecuritySchemes()->offsetSet(
'JWT',
new \ArrayObject(
[
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
]
)
);
$openApi
->getPaths()
->addPath($this->checkPath, (new PathItem())->withPost(
(new Operation())
->withOperationId('login_check_post')
->withTags(['Login Check'])
->withResponses([
Response::HTTP_OK => [
'description' => 'User token created',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'token' => [
'readOnly' => true,
'type' => 'string',
'nullable' => false,
],
],
'required' => ['token'],
],
],
],
],
])
->withSummary('Creates a user token.')
->withDescription('Creates a user token.')
->withRequestBody(
(new RequestBody())
->withDescription('The login data')
->withContent(new \ArrayObject([
'application/json' => new MediaType(new \ArrayObject(new \ArrayObject([
'type' => 'object',
'properties' => $properties = array_merge_recursive($this->getJsonSchemaFromPathParts(explode('.', $this->usernamePath)), $this->getJsonSchemaFromPathParts(explode('.', $this->passwordPath))),
'required' => array_keys($properties),
]))),
]))
->withRequired(true)
)
));
return $openApi;
}
private function getJsonSchemaFromPathParts(array $pathParts): array
{
$jsonSchema = [];
if (count($pathParts) === 1) {
$jsonSchema[array_shift($pathParts)] = [
'type' => 'string',
'nullable' => false,
];
return $jsonSchema;
}
$pathPart = array_shift($pathParts);
$properties = $this->getJsonSchemaFromPathParts($pathParts);
$jsonSchema[$pathPart] = [
'type' => 'object',
'properties' => $properties,
'required' => array_keys($properties),
];
return $jsonSchema;
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.api_platform.openapi.factory" class="Lexik\Bundle\JWTAuthenticationBundle\OpenApi\OpenApiFactory" decorates="api_platform.openapi.factory" decoration-on-invalid="ignore" public="false">
<argument type="service" id="lexik_jwt_authentication.api_platform.openapi.factory.inner"/>
<argument /><!-- check path -->
<argument /><!-- username path -->
<argument /><!-- password path -->
</service>
</services>
</container>
@@ -0,0 +1,28 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.event_listener.block_jwt_listener" class="Lexik\Bundle\JWTAuthenticationBundle\EventListener\BlockJWTListener">
<argument type="service" id="lexik_jwt_authentication.blocked_token_manager"/>
<argument type="service" id="lexik_jwt_authentication.extractor.chain_extractor"/>
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/>
<tag name="kernel.event_listener" event="Symfony\Component\Security\Http\Event\LoginFailureEvent" method="onLoginFailure" dispatcher="event_dispatcher"/>
<tag name="kernel.event_listener" event="Symfony\Component\Security\Http\Event\LogoutEvent" method="onLogout" dispatcher="event_dispatcher"/>
</service>
<service id="lexik_jwt_authentication.event_listener.reject_blocked_token_listener" class="Lexik\Bundle\JWTAuthenticationBundle\EventListener\RejectBlockedTokenListener">
<argument type="service" id="lexik_jwt_authentication.blocked_token_manager"/>
<tag name="kernel.event_listener" event="lexik_jwt_authentication.on_jwt_authenticated"/>
</service>
<service id="lexik_jwt_authentication.blocked_token_manager" class="Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedToken\CacheItemPoolBlockedTokenManager">
<argument type="service" id="lexik_jwt_authentication.blocklist_token.cache"/>
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface" alias="lexik_jwt_authentication.blocked_token_manager" />
</services>
</container>
@@ -0,0 +1,42 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.check_config_command" class="Lexik\Bundle\JWTAuthenticationBundle\Command\CheckConfigCommand">
<argument type="service" id="lexik_jwt_authentication.key_loader" />
<argument type="string">%lexik_jwt_authentication.encoder.signature_algorithm%</argument> <!-- signature algorithm -->
<tag name="console.command" command="lexik:jwt:check-config" />
</service>
<service id="lexik_jwt_authentication.migrate_config_command" class="Lexik\Bundle\JWTAuthenticationBundle\Command\MigrateConfigCommand">
<argument type="service" id="lexik_jwt_authentication.key_loader" />
<argument type="string">%lexik_jwt_authentication.pass_phrase%</argument>
<argument type="string">%lexik_jwt_authentication.encoder.signature_algorithm%</argument>
<tag name="console.command" command="lexik:jwt:migrate-config" />
</service>
<service id="lexik_jwt_authentication.enable_encryption_config_command" class="Lexik\Bundle\JWTAuthenticationBundle\Command\EnableEncryptionConfigCommand">
<argument type="service" id="Jose\Component\Core\AlgorithmManagerFactory" on-invalid="null" />
<tag name="console.command" command="lexik:jwt:enable-encryption" />
</service>
<service id="lexik_jwt_authentication.generate_token_command" class="Lexik\Bundle\JWTAuthenticationBundle\Command\GenerateTokenCommand" public="true">
<argument type="service" id="lexik_jwt_authentication.jwt_manager" />
<argument type="collection" /> <!-- user providers -->
<tag name="console.command" command="lexik:jwt:generate-token" />
</service>
<service id="lexik_jwt_authentication.generate_keypair_command" class="Lexik\Bundle\JWTAuthenticationBundle\Command\GenerateKeyPairCommand">
<argument type="service" id="filesystem" />
<argument />
<argument />
<argument />
<argument />
<tag name="console.command" command="lexik:jwt:generate-keypair" />
</service>
</services>
</container>
@@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.cookie_provider" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider" abstract="true">
<argument>null</argument> <!-- Default name -->
<argument>null</argument> <!-- Default lifetime -->
<argument/> <!-- Default samesite -->
<argument/> <!-- Default path -->
<argument>null</argument> <!-- Default domain -->
<argument/> <!-- Default secure -->
<argument/> <!-- Default httpOnly -->
<argument>null</argument> <!-- Default split -->
<argument>false</argument> <!-- Default partitioned -->
</service>
</services>
</container>
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.jwt_manager" class="Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManager" public="true">
<argument type="service" id="lexik_jwt_authentication.encoder"/>
<argument type="service" id="event_dispatcher"/>
<argument>%lexik_jwt_authentication.user_id_claim%</argument>
<argument type="service" id="lexik_jwt_authentication.payload_enrichment"/>
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface" alias="lexik_jwt_authentication.jwt_manager" />
<service id="lexik_jwt_authentication.payload_enrichment.random_jti_enrichment" class="Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\RandomJtiEnrichment">
<tag name="lexik_jwt_authentication.payload_enrichment" priority="0" />
</service>
<service id="lexik_jwt_authentication.payload_enrichment" class="Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\ChainEnrichment">
<argument type="collection"/>
</service>
</services>
</container>
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.key_loader.abstract" abstract="true" public="false">
<argument/> <!-- private key -->
<argument/> <!-- public key -->
<argument>%lexik_jwt_authentication.pass_phrase%</argument>
<argument type="collection" /> <!-- additional public keys -->
</service>
<service id="lexik_jwt_authentication.key_loader.raw" class="Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader" parent="lexik_jwt_authentication.key_loader.abstract"/>
</services>
</container>
@@ -0,0 +1,21 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.encoder.lcobucci" class="Lexik\Bundle\JWTAuthenticationBundle\Encoder\LcobucciJWTEncoder">
<argument type="service" id="lexik_jwt_authentication.jws_provider.lcobucci" />
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\JWSProviderInterface" alias="lexik_jwt_authentication.jws_provider.lcobucci" />
<service id="lexik_jwt_authentication.jws_provider.lcobucci" class="Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\LcobucciJWSProvider" public="false">
<argument type="service" id="lexik_jwt_authentication.key_loader.raw"/>
<argument>%lexik_jwt_authentication.encoder.signature_algorithm%</argument>
<argument>%lexik_jwt_authentication.token_ttl%</argument>
<argument>%lexik_jwt_authentication.clock_skew%</argument>
<argument>%lexik_jwt_authentication.allow_no_expiration%</argument>
</service>
</services>
</container>
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.handler.authentication_success" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler">
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/>
<argument type="service" id="event_dispatcher"/>
<argument type="collection"/> <!-- Cookie providers -->
<argument>true</argument>
<tag name="monolog.logger" channel="security" />
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler" alias="lexik_jwt_authentication.handler.authentication_success" />
<service id="lexik_jwt_authentication.handler.authentication_failure" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler">
<tag name="monolog.logger" channel="security" />
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="translator" on-invalid="null" />
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler" alias="lexik_jwt_authentication.handler.authentication_failure" />
</services>
</container>
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.security.jwt_authenticator" class="Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator" abstract="true">
<argument type="service" id="lexik_jwt_authentication.jwt_manager"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="lexik_jwt_authentication.extractor.chain_extractor"/>
<argument /> <!-- User Provider -->
<argument type="service" id="translator" on-invalid="null" />
</service>
</services>
</container>
@@ -0,0 +1,29 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.extractor.chain_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\ChainTokenExtractor" public="false">
<argument type="collection" />
</service>
<service id="lexik_jwt_authentication.extractor.authorization_header_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\AuthorizationHeaderTokenExtractor">
<argument /> <!-- Header Value Prefix -->
<argument /> <!-- Header Value Name -->
</service>
<service id="lexik_jwt_authentication.extractor.query_parameter_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\QueryParameterTokenExtractor">
<argument /> <!-- Parameter Name -->
</service>
<service id="lexik_jwt_authentication.extractor.cookie_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\CookieTokenExtractor">
<argument /> <!-- Name -->
</service>
<service id="lexik_jwt_authentication.extractor.split_cookie_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\SplitCookieExtractor">
<argument /> <!-- Cookies -->
</service>
<service public="false" id="lexik_jwt_authentication.security.jwt_user_provider" class="Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserProvider">
<argument />
</service>
<service id="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface" alias="lexik_jwt_authentication.extractor.chain_extractor" />
</services>
</container>
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.encoder.web_token" class="Lexik\Bundle\JWTAuthenticationBundle\Encoder\WebTokenEncoder" public="false">
<argument type="service" id="lexik_jwt_authentication.access_token_builder" on-invalid="null" />
<argument type="service" id="lexik_jwt_authentication.access_token_loader" on-invalid="null" />
</service>
<service id="lexik_jwt_authentication.subscriber.access_token_time" class="Lexik\Bundle\JWTAuthenticationBundle\Subscriber\AdditionalAccessTokenClaimsAndHeaderSubscriber" public="false">
<argument on-invalid="null">%lexik_jwt_authentication.token_ttl%</argument>
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
@@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.access_token_builder" class="Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken\AccessTokenBuilder" public="false">
<argument type="service" id="Symfony\Contracts\EventDispatcher\EventDispatcherInterface" />
<argument type="service" id="Jose\Bundle\JoseFramework\Services\JWSBuilderFactory" />
<argument type="service" id="Jose\Bundle\JoseFramework\Services\JWEBuilderFactory" on-invalid="null" />
<argument /> <!-- Signature algorithm -->
<argument /> <!-- Signature key -->
<argument /> <!-- Key encryption algorithm -->
<argument /> <!-- Content encryption algorithm -->
<argument /> <!-- Encryption key -->
</service>
</services>
</container>
@@ -0,0 +1,42 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="lexik_jwt_authentication.access_token_loader" class="Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken\AccessTokenLoader" public="false">
<argument type="service" id="Jose\Bundle\JoseFramework\Services\JWSLoaderFactory" />
<argument type="service" id="Jose\Bundle\JoseFramework\Services\JWELoaderFactory" on-invalid="null" />
<argument type="service" id="Jose\Bundle\JoseFramework\Services\ClaimCheckerManagerFactory" />
<argument type="collection" /> <!-- Claim checkers -->
<argument type="collection"/> <!-- JWS header checkers -->
<argument type="collection"/> <!-- Mandatory claims -->
<argument type="collection" /> <!-- Allowed signature algorithms -->
<argument /> <!-- Signature keyset -->
<argument on-invalid="null" /> <!-- Continue on decryption failure -->
<argument type="collection" /> <!-- JWE header checkers -->
<argument type="collection" /> <!-- Allowed key encryption algorithms -->
<argument type="collection" /> <!-- Allowed content encryption algorithms -->
<argument on-invalid="null" /> <!-- Encryption keyset -->
</service>
<service id="lexik_jwt_authentication.web_token.iat_validator" class="Jose\Component\Checker\IssuedAtChecker" public="false">
<argument>%lexik_jwt_authentication.clock_skew%</argument>
<argument>true</argument>
<tag name="jose.checker.claim" alias="iat_with_clock_skew" />
<tag name="jose.checker.header" alias="iat_with_clock_skew" />
</service>
<service id="lexik_jwt_authentication.web_token.exp_validator" class="Jose\Component\Checker\ExpirationTimeChecker" public="false">
<argument>%lexik_jwt_authentication.clock_skew%</argument>
<argument>true</argument>
<tag name="jose.checker.claim" alias="exp_with_clock_skew" />
<tag name="jose.checker.header" alias="exp_with_clock_skew" />
</service>
<service id="lexik_jwt_authentication.web_token.nbf_validator" class="Jose\Component\Checker\NotBeforeChecker" public="false">
<argument>%lexik_jwt_authentication.clock_skew%</argument>
<argument>true</argument>
<tag name="jose.checker.claim" alias="nbf_with_clock_skew" />
<tag name="jose.checker.header" alias="nbf_with_clock_skew" />
</service>
</services>
</container>
@@ -0,0 +1,53 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* JWTAuthenticationFailureResponse.
*
* Response sent on failed JWT authentication (can be replaced by a custom Response).
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class JWTAuthenticationFailureResponse extends JsonResponse
{
private string $message;
public function __construct(string $message = 'Bad credentials', int $statusCode = Response::HTTP_UNAUTHORIZED)
{
$this->message = $message;
parent::__construct(null, $statusCode, ['WWW-Authenticate' => 'Bearer']);
}
/**
* Sets the response data with the statusCode & message included.
*/
public function setData(mixed $data = []): static
{
return parent::setData((array)$data + ["code" => $this->statusCode, "message" => $this->getMessage()]);
}
/**
* Sets the failure message.
*/
public function setMessage(string $message): JWTAuthenticationFailureResponse
{
$this->message = $message;
$this->setData();
return $this;
}
/**
* Gets the failure message.
*/
public function getMessage(): string
{
return $this->message;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Response sent on successful JWT authentication.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class JWTAuthenticationSuccessResponse extends JsonResponse
{
/**
* @param string $token Json Web Token
* @param array $data Extra data passed to the response
*/
public function __construct(string $token, array $data = [], array $jwtCookies = [])
{
if (!$jwtCookies) {
parent::__construct(['token' => $token] + $data);
return;
}
parent::__construct($data);
foreach ($jwtCookies as $cookie) {
$this->headers->setCookie($cookie);
}
}
}
@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\Token\JWTPostAuthenticationToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\PayloadAwareUserProviderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class JWTAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
private TokenExtractorInterface $tokenExtractor;
private JWTTokenManagerInterface $jwtManager;
private EventDispatcherInterface $eventDispatcher;
private UserProviderInterface $userProvider;
private ?TranslatorInterface $translator;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $eventDispatcher,
TokenExtractorInterface $tokenExtractor,
UserProviderInterface $userProvider,
TranslatorInterface $translator = null
) {
$this->tokenExtractor = $tokenExtractor;
$this->jwtManager = $jwtManager;
$this->eventDispatcher = $eventDispatcher;
$this->userProvider = $userProvider;
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$exception = new MissingTokenException('JWT Token not found', 0, $authException);
$event = new JWTNotFoundEvent($exception, new JWTAuthenticationFailureResponse($exception->getMessageKey()), $request);
$this->eventDispatcher->dispatch($event, Events::JWT_NOT_FOUND);
return $event->getResponse();
}
public function supports(Request $request): ?bool
{
return false !== $this->getTokenExtractor()->extract($request);
}
public function authenticate(Request $request): Passport
{
$token = $this->getTokenExtractor()->extract($request);
if ($token === false) {
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.');
}
try {
if (!$payload = $this->jwtManager->parse($token)) {
throw new InvalidTokenException('Invalid JWT Token');
}
} catch (JWTDecodeFailureException $e) {
if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
throw new ExpiredTokenException();
}
throw new InvalidTokenException('Invalid JWT Token', 0, $e);
}
$idClaim = $this->jwtManager->getUserIdClaim();
if (!isset($payload[$idClaim])) {
throw new InvalidPayloadException($idClaim);
}
$passport = new SelfValidatingPassport(
new UserBadge(
(string) $payload[$idClaim],
fn ($userIdentifier) => $this->loadUser($payload, $userIdentifier)
)
);
$passport->setAttribute('payload', $payload);
$passport->setAttribute('token', $token);
return $passport;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
}
$response = new JWTAuthenticationFailureResponse($errorMessage);
if ($exception instanceof ExpiredTokenException) {
$event = new JWTExpiredEvent($exception, $response, $request);
$eventName = Events::JWT_EXPIRED;
} else {
$event = new JWTInvalidEvent($exception, $response, $request);
$eventName = Events::JWT_INVALID;
}
$this->eventDispatcher->dispatch($event, $eventName);
return $event->getResponse();
}
/**
* Gets the token extractor to be used for retrieving a JWT token in the
* current request.
*
* Override this method for adding/removing extractors to the chain one or
* returning a different {@link TokenExtractorInterface} implementation.
*/
protected function getTokenExtractor(): TokenExtractorInterface
{
return $this->tokenExtractor;
}
/**
* Gets the jwt manager.
*/
protected function getJwtManager(): JWTTokenManagerInterface
{
return $this->jwtManager;
}
/**
* Gets the event dispatcher.
*/
protected function getEventDispatcher(): EventDispatcherInterface
{
return $this->eventDispatcher;
}
/**
* Gets the user provider.
*/
protected function getUserProvider(): UserProviderInterface
{
return $this->userProvider;
}
/**
* Loads the user to authenticate.
*
* @param array $payload The token payload
* @param string $identity The key from which to retrieve the user "identifier"
*/
protected function loadUser(array $payload, string $identity): UserInterface
{
if ($this->userProvider instanceof PayloadAwareUserProviderInterface) {
return $this->userProvider->loadUserByIdentifierAndPayload($identity, $payload);
}
if ($this->userProvider instanceof ChainUserProvider) {
foreach ($this->userProvider->getProviders() as $provider) {
try {
if ($provider instanceof PayloadAwareUserProviderInterface) {
return $provider->loadUserByIdentifierAndPayload($identity, $payload);
}
return $provider->loadUserByIdentifier($identity);
} catch (AuthenticationException $e) {
// try next one
}
}
$ex = new UserNotFoundException(sprintf('There is no user with identifier "%s".', $identity));
$ex->setUserIdentifier($identity);
throw $ex;
}
return $this->userProvider->loadUserByIdentifier($identity);
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
$token = new JWTPostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles(), $passport->getAttribute('token'));
$this->eventDispatcher->dispatch(new JWTAuthenticatedEvent($passport->getAttribute('payload'), $token), Events::JWT_AUTHENTICATED);
return $token;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\Token;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
class JWTPostAuthenticationToken extends PostAuthenticationToken
{
private string $token;
public function __construct(UserInterface $user, string $firewallName, array $roles, string $token)
{
parent::__construct($user, $firewallName, $roles);
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public function getCredentials(): string
{
return $this->token;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* AuthenticationFailureHandler.
*
* @author Dev Lexik <dev@lexik.fr>
*/
class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
protected EventDispatcherInterface $dispatcher;
private ?TranslatorInterface $translator;
public function __construct(EventDispatcherInterface $dispatcher, TranslatorInterface $translator = null)
{
$this->dispatcher = $dispatcher;
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
$statusCode = self::mapExceptionCodeToStatusCode($exception->getCode());
if ($this->translator) {
$errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), 'security');
}
$event = new AuthenticationFailureEvent(
$exception,
new JWTAuthenticationFailureResponse($errorMessage, $statusCode),
$request
);
$this->dispatcher->dispatch($event, Events::AUTHENTICATION_FAILURE);
return $event->getResponse();
}
/**
* @param string|int $exceptionCode
*/
private static function mapExceptionCodeToStatusCode($exceptionCode): int
{
$canMapToStatusCode = is_int($exceptionCode)
&& $exceptionCode >= 400
&& $exceptionCode < 500;
return $canMapToStatusCode
? $exceptionCode
: Response::HTTP_UNAUTHORIZED;
}
}
@@ -0,0 +1,80 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationSuccessResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* AuthenticationSuccessHandler.
*
* @author Dev Lexik <dev@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected JWTTokenManagerInterface $jwtManager;
protected EventDispatcherInterface $dispatcher;
protected bool $removeTokenFromBodyWhenCookiesUsed;
private iterable $cookieProviders;
/**
* @param iterable|JWTCookieProvider[] $cookieProviders
*/
public function __construct(JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher, iterable $cookieProviders = [], bool $removeTokenFromBodyWhenCookiesUsed = true)
{
$this->jwtManager = $jwtManager;
$this->dispatcher = $dispatcher;
$this->cookieProviders = $cookieProviders;
$this->removeTokenFromBodyWhenCookiesUsed = $removeTokenFromBodyWhenCookiesUsed;
}
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
{
return $this->handleAuthenticationSuccess($token->getUser());
}
public function handleAuthenticationSuccess(UserInterface $user, $jwt = null): Response
{
if (null === $jwt) {
$jwt = $this->jwtManager->create($user);
}
$jwtCookies = [];
foreach ($this->cookieProviders as $cookieProvider) {
$jwtCookies[] = $cookieProvider->createCookie($jwt);
}
$response = new JWTAuthenticationSuccessResponse($jwt, [], $jwtCookies);
$event = new AuthenticationSuccessEvent(['token' => $jwt], $user, $response);
$this->dispatcher->dispatch($event, Events::AUTHENTICATION_SUCCESS);
$responseData = $event->getData();
if ($jwtCookies && $this->removeTokenFromBodyWhenCookiesUsed) {
unset($responseData['token']);
}
if ($responseData) {
$response->setData($responseData);
} else {
$response->setStatusCode(Response::HTTP_NO_CONTENT);
}
return $response;
}
}
@@ -0,0 +1,81 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie;
use Lexik\Bundle\JWTAuthenticationBundle\Helper\JWTSplitter;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Kernel;
/**
* Creates secure JWT cookies.
*/
final class JWTCookieProvider
{
private ?string $defaultName;
private ?int $defaultLifetime;
private ?string $defaultSameSite;
private ?string $defaultPath;
private ?string $defaultDomain;
private bool $defaultSecure;
private bool $defaultHttpOnly;
private array $defaultSplit;
private bool $defaultPartitioned;
public function __construct(?string $defaultName = null, ?int $defaultLifetime = 0, ?string $defaultSameSite = Cookie::SAMESITE_LAX, ?string $defaultPath = '/', ?string $defaultDomain = null, bool $defaultSecure = true, bool $defaultHttpOnly = true, array $defaultSplit = [], bool $defaultPartitioned = false)
{
$this->defaultName = $defaultName;
$this->defaultLifetime = $defaultLifetime;
$this->defaultSameSite = $defaultSameSite;
$this->defaultPath = $defaultPath;
$this->defaultDomain = $defaultDomain;
$this->defaultSecure = $defaultSecure;
$this->defaultHttpOnly = $defaultHttpOnly;
$this->defaultSplit = $defaultSplit;
$this->defaultPartitioned = $defaultPartitioned;
if ($defaultPartitioned && Kernel::VERSION < '6.4') {
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));
}
}
/**
* Creates a secure cookie containing the passed JWT.
*
* For each argument (all args except $jwt), if omitted or set to null then the
* default value defined via the constructor will be used.
*/
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
{
if (!$name && !$this->defaultName) {
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__));
}
if (!$expiresAt && null === $this->defaultLifetime) {
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__));
}
if ($partitioned && Kernel::VERSION < '6.4') {
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));
}
$jwtParts = new JWTSplitter($jwt);
$jwt = $jwtParts->getParts($split ?: $this->defaultSplit);
if (null === $expiresAt) {
$expiresAt = 0 === $this->defaultLifetime ? 0 : (time() + $this->defaultLifetime);
}
return Cookie::create(
$name ?: $this->defaultName,
$jwt,
$expiresAt,
$path ?: $this->defaultPath,
$domain ?: $this->defaultDomain,
$secure ?: $this->defaultSecure,
$httpOnly ?: $this->defaultHttpOnly,
false,
$sameSite ?: $this->defaultSameSite,
$partitioned ?: $this->defaultPartitioned
);
}
}
@@ -0,0 +1,72 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;
/**
* User class for which to create instances from JWT tokens.
*
* Note: This is only useful when using the JWTUserProvider (database-less).
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTUser implements JWTUserInterface
{
private string $userIdentifier;
private array $roles;
public function __construct(string $userIdentifier, array $roles = [])
{
$this->userIdentifier = $userIdentifier;
$this->roles = $roles;
}
/**
* {@inheritdoc}
*/
public static function createFromPayload($username, array $payload): JWTUserInterface
{
if (isset($payload['roles'])) {
return new static($username, (array) $payload['roles']);
}
return new static($username);
}
public function getUsername(): string
{
return $this->getUserIdentifier();
}
public function getUserIdentifier(): string
{
return $this->userIdentifier;
}
/**
* {@inheritdoc}
*/
public function getRoles(): array
{
return $this->roles;
}
public function getPassword(): ?string
{
return null;
}
/**
* {@inheritdoc}
*/
public function getSalt(): ?string
{
return null;
}
/**
* {@inheritdoc}
*/
public function eraseCredentials(): void
{
}
}
@@ -0,0 +1,17 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
interface JWTUserInterface extends UserInterface
{
/**
* Creates a new instance from a given JWT payload.
*
* @param string $username
*
* @return JWTUserInterface
*/
public static function createFromPayload($username, array $payload);
}
@@ -0,0 +1,68 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* JWT User provider.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class JWTUserProvider implements PayloadAwareUserProviderInterface
{
private string $class;
private array $cache = [];
/**
* @param string $class The {@link JWTUserInterface} implementation FQCN for which to provide instances
*/
public function __construct(string $class)
{
$this->class = $class;
}
/**
* To be removed at the same time as symfony 5.4 support.
*/
public function loadUserByUsername(string $username): UserInterface
{
// to be removed at the same time as symfony 5.4 support
throw new \LogicException('This method is implemented for BC purpose and should never be called.');
}
/**
* {@inheritdoc}
*
* @param array $payload The JWT payload from which to create an instance
*/
public function loadUserByIdentifier(string $identifier, array $payload = []): UserInterface
{
return $this->loadUserByIdentifierAndPayload($identifier, $payload);
}
public function loadUserByIdentifierAndPayload(string $identifier, array $payload): UserInterface
{
if (isset($this->cache[$identifier])) {
return $this->cache[$identifier];
}
$class = $this->class;
return $this->cache[$identifier] = $class::createFromPayload($identifier, $payload);
}
/**
* {@inheritdoc}
*/
public function supportsClass($class): bool
{
return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
}
public function refreshUser(UserInterface $user): UserInterface
{
return $user; // noop
}
}
@@ -0,0 +1,17 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
interface PayloadAwareUserProviderInterface extends UserProviderInterface
{
/**
* Loads a user from an identifier and JWT token payload.
*
* @throws UserNotFoundException if the user is not found
*/
public function loadUserByIdentifierAndPayload(string $identifier, array $payload): UserInterface;
}
@@ -0,0 +1,66 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedToken;
use DateInterval;
use DateTimeImmutable;
use DateTimeZone;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\BlockedTokenManagerInterface;
use Psr\Cache\CacheItemPoolInterface;
class CacheItemPoolBlockedTokenManager implements BlockedTokenManagerInterface
{
private $cacheJwt;
public function __construct(CacheItemPoolInterface $cacheJwt)
{
$this->cacheJwt = $cacheJwt;
}
public function add(array $payload): bool
{
if (!isset($payload['exp'])) {
throw new MissingClaimException('exp');
}
$expiration = new DateTimeImmutable('@' . $payload['exp'], new DateTimeZone('UTC'));
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
// If the token is already expired, there's no point in adding it to storage
if ($expiration <= $now) {
return false;
}
$cacheExpiration = $expiration->add(new DateInterval('PT5M'));
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$cacheItem = $this->cacheJwt->getItem($payload['jti']);
$cacheItem->set([]);
$cacheItem->expiresAt($cacheExpiration);
$this->cacheJwt->save($cacheItem);
return true;
}
public function has(array $payload): bool
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
return $this->cacheJwt->hasItem($payload['jti']);
}
public function remove(array $payload): void
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$this->cacheJwt->deleteItem($payload['jti']);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingClaimException;
interface BlockedTokenManagerInterface
{
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function add(array $payload): bool;
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function has(array $payload): bool;
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function remove(array $payload): void;
}
@@ -0,0 +1,24 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
/**
* Interface for classes that are able to create and load JSON web signatures (JWS).
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface JWSProviderInterface
{
/**
* Creates a new JWS signature from a given payload.
*/
public function create(array $payload, array $header = []): CreatedJWS;
/**
* Loads an existing JWS signature from a given JWT token.
*/
public function load(string $token): LoadedJWS;
}
@@ -0,0 +1,228 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Ecdsa;
use Lcobucci\JWT\Signer\Hmac;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Hmac\Sha384;
use Lcobucci\JWT\Signer\Hmac\Sha512;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder as JWTBuilder;
use Lcobucci\JWT\Token\Parser as JWTParser;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\Validator;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\CreatedJWS;
use Lexik\Bundle\JWTAuthenticationBundle\Signature\LoadedJWS;
/**
* @final
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LcobucciJWSProvider implements JWSProviderInterface
{
private KeyLoaderInterface $keyLoader;
private Clock $clock;
private Signer $signer;
private ?int $ttl;
private ?int $clockSkew;
private bool $allowNoExpiration;
/**
* @throws \InvalidArgumentException If the given crypto engine is not supported
*/
public function __construct(KeyLoaderInterface $keyLoader, string $signatureAlgorithm, ?int $ttl, ?int $clockSkew, bool $allowNoExpiration = false, Clock $clock = null)
{
if (null === $clock) {
$clock = new SystemClock(new \DateTimeZone('UTC'));
}
$this->keyLoader = $keyLoader;
$this->clock = $clock;
$this->signer = $this->getSignerForAlgorithm($signatureAlgorithm);
$this->ttl = $ttl;
$this->clockSkew = $clockSkew;
$this->allowNoExpiration = $allowNoExpiration;
}
/**
* {@inheritdoc}
*/
public function create(array $payload, array $header = []): CreatedJWS
{
$jws = new JWTBuilder(new JoseEncoder(), ChainedFormatter::default());
foreach ($header as $k => $v) {
$jws = $jws->withHeader($k, $v);
}
$now = time();
$issuedAt = $payload['iat'] ?? $now;
unset($payload['iat']);
$jws = $jws->issuedAt(!$issuedAt instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$issuedAt}") : $issuedAt);
$exp = null;
if (null !== $this->ttl || isset($payload['exp'])) {
$exp = $payload['exp'] ?? $now + $this->ttl;
unset($payload['exp']);
}
if ($exp) {
$jws = $jws->expiresAt(!$exp instanceof \DateTimeImmutable ? new \DateTimeImmutable("@{$exp}") : $exp);
}
if (isset($payload['sub'])) {
$jws = $jws->relatedTo($payload['sub']);
unset($payload['sub']);
}
$jws = $this->addStandardClaims($jws, $payload);
foreach ($payload as $name => $value) {
$jws = $jws->withClaim($name, $value);
}
$e = $token = null;
try {
$token = $this->getSignedToken($jws);
} catch (\InvalidArgumentException) {
}
return new CreatedJWS((string) $token, null === $e);
}
/**
* {@inheritdoc}
*/
public function load(string $token): LoadedJWS
{
$jws = (new JWTParser(new JoseEncoder()))->parse($token);
$payload = [];
foreach ($jws->claims()->all() as $name => $value) {
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
}
$payload[$name] = $value;
}
return new LoadedJWS(
$payload,
$this->verify($jws),
false == $this->allowNoExpiration,
$jws->headers()->all(),
$this->clockSkew
);
}
private function getSignerForAlgorithm($signatureAlgorithm): Signer
{
$signerMap = [
'HS256' => Sha256::class,
'HS384' => Sha384::class,
'HS512' => Sha512::class,
'RS256' => Signer\Rsa\Sha256::class,
'RS384' => Signer\Rsa\Sha384::class,
'RS512' => Signer\Rsa\Sha512::class,
'ES256' => Signer\Ecdsa\Sha256::class,
'ES384' => Signer\Ecdsa\Sha384::class,
'ES512' => Signer\Ecdsa\Sha512::class,
];
if (!isset($signerMap[$signatureAlgorithm])) {
throw new \InvalidArgumentException(sprintf('The algorithm "%s" is not supported by %s', $signatureAlgorithm, self::class));
}
$signerClass = $signerMap[$signatureAlgorithm];
if (is_subclass_of($signerClass, Ecdsa::class) && method_exists($signerClass, 'create')) {
return $signerClass::create();
}
return new $signerClass();
}
private function getSignedToken(Builder $jws): string
{
$key = InMemory::plainText($this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : (string) $this->keyLoader->getPassphrase());
$token = $jws->getToken($this->signer, $key);
return $token->toString();
}
private function verify(Token $jwt): bool
{
$key = InMemory::plainText($this->signer instanceof Hmac ? $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PRIVATE) : $this->keyLoader->loadKey(KeyLoaderInterface::TYPE_PUBLIC));
$validator = new Validator();
$isValid = $validator->validate(
$jwt,
new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
new SignedWith($this->signer, $key)
);
$publicKeys = $this->keyLoader->getAdditionalPublicKeys();
if ($isValid || $this->signer instanceof Hmac || empty($publicKeys)) {
return $isValid;
}
// If the key used to verify the token is invalid, and it's not Hmac algorithm, try with additional public keys
foreach ($publicKeys as $key) {
$isValid = $validator->validate(
$jwt,
new LooseValidAt($this->clock, new \DateInterval("PT{$this->clockSkew}S")),
new SignedWith($this->signer, InMemory::plainText($key))
);
if ($isValid) {
return true;
}
}
return false;
}
private function addStandardClaims(Builder $builder, array &$payload): Builder
{
$mutatorMap = [
RegisteredClaims::AUDIENCE => 'permittedFor',
RegisteredClaims::ID => 'identifiedBy',
RegisteredClaims::ISSUER => 'issuedBy',
RegisteredClaims::NOT_BEFORE => 'canOnlyBeUsedAfter',
];
foreach ($payload as $claim => $value) {
if (!isset($mutatorMap[$claim])) {
continue;
}
$mutator = $mutatorMap[$claim];
unset($payload[$claim]);
if (\is_array($value)) {
$builder = \call_user_func_array([$builder, $mutator], $value);
continue;
}
$builder = $builder->{$mutator}($value);
}
return $builder;
}
}
@@ -0,0 +1,148 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\HeaderAwareJWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTEncodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment\NullEnrichment;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Provides convenient methods to manage JWT creation/verification.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class JWTManager implements JWTTokenManagerInterface
{
protected JWTEncoderInterface $jwtEncoder;
protected EventDispatcherInterface $dispatcher;
protected string $userIdClaim;
private $payloadEnrichment;
public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, string $userIdClaim, PayloadEnrichmentInterface $payloadEnrichment = null)
{
$this->jwtEncoder = $encoder;
$this->dispatcher = $dispatcher;
$this->userIdClaim = $userIdClaim;
$this->payloadEnrichment = $payloadEnrichment ?? new NullEnrichment();
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
public function create(UserInterface $user): string
{
$payload = ['roles' => $user->getRoles()];
$this->addUserIdentityToPayload($user, $payload);
$this->payloadEnrichment->enrich($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
public function createFromPayload(UserInterface $user, array $payload = []): string
{
$payload = array_merge(['roles' => $user->getRoles()], $payload);
$this->addUserIdentityToPayload($user, $payload);
$this->payloadEnrichment->enrich($user, $payload);
return $this->generateJwtStringAndDispatchEvents($user, $payload);
}
/**
* @return string The JWT token
*
* @throws JWTEncodeFailureException
*/
private function generateJwtStringAndDispatchEvents(UserInterface $user, array $payload): string
{
$jwtCreatedEvent = new JWTCreatedEvent($payload, $user);
$this->dispatcher->dispatch($jwtCreatedEvent, Events::JWT_CREATED);
if ($this->jwtEncoder instanceof HeaderAwareJWTEncoderInterface) {
$jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData(), $jwtCreatedEvent->getHeader());
} else {
$jwtString = $this->jwtEncoder->encode($jwtCreatedEvent->getData());
}
$jwtEncodedEvent = new JWTEncodedEvent($jwtString);
$this->dispatcher->dispatch($jwtEncodedEvent, Events::JWT_ENCODED);
return $jwtString;
}
/**
* {@inheritdoc}
*
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token): array|bool
{
if (!($payload = $this->jwtEncoder->decode($token->getCredentials()))) {
return false;
}
$event = new JWTDecodedEvent($payload);
$this->dispatcher->dispatch($event, Events::JWT_DECODED);
if (!$event->isValid()) {
return false;
}
return $event->getPayload();
}
/**
* {@inheritdoc}
*
* @throws JWTDecodeFailureException
*/
public function parse(string $token): array
{
$payload = $this->jwtEncoder->decode($token);
$event = new JWTDecodedEvent($payload);
$this->dispatcher->dispatch($event, Events::JWT_DECODED);
if (!$event->isValid()) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'The token was marked as invalid by an event listener after successful decoding.');
}
return $event->getPayload();
}
/**
* Add user identity to payload, username by default.
* Override this if you need to identify it by another property.
*/
protected function addUserIdentityToPayload(UserInterface $user, array &$payload): void
{
$accessor = PropertyAccess::createPropertyAccessor();
$payload[$this->userIdClaim] = $accessor->getValue($user, $accessor->isReadable($user, $this->userIdClaim) ? $this->userIdClaim : 'user_identifier');
}
public function getUserIdClaim(): string
{
return $this->userIdClaim;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* JWTTokenManagerInterface must be implemented by classes able to create/decode
* JWT tokens.
*
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Eric Lannez <eric.lannez@gmail.com>
*/
interface JWTTokenManagerInterface
{
/**
* @return string The JWT token
*/
public function create(UserInterface $user);
public function createFromPayload(UserInterface $user, array $payload = []): string;
/**
* @return array|false The JWT token payload or false if an error occurs
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token): array|bool;
/**
* Parses a raw JWT token and returns its payload
*/
public function parse(string $token): array;
/**
* Returns the claim used as identifier to load an user from a JWT payload.
*
* @return string
*/
public function getUserIdClaim();
}
@@ -0,0 +1,90 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Abstract class for key loaders.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @internal
*/
abstract class AbstractKeyLoader implements KeyLoaderInterface
{
private ?string $signingKey;
private ?string $publicKey;
private ?string $passphrase;
private array $additionalPublicKeys;
public function __construct(?string $signingKey = null, ?string $publicKey = null, ?string $passphrase = null, array $additionalPublicKeys = [])
{
$this->signingKey = $signingKey;
$this->publicKey = $publicKey;
$this->passphrase = $passphrase;
$this->additionalPublicKeys = $additionalPublicKeys;
}
/**
* {@inheritdoc}
*/
public function getPassphrase(): ?string
{
return $this->passphrase;
}
public function getSigningKey(): ?string
{
return $this->signingKey && is_file($this->signingKey) ? $this->readKey(self::TYPE_PRIVATE) : $this->signingKey;
}
public function getPublicKey(): ?string
{
return $this->publicKey && is_file($this->publicKey) ? $this->readKey(self::TYPE_PUBLIC) : $this->publicKey;
}
public function getAdditionalPublicKeys(): array
{
$contents = [];
foreach ($this->additionalPublicKeys as $key) {
if (!$key || !is_file($key) || !is_readable($key)) {
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));
}
$rawKey = @file_get_contents($key);
if (false === $rawKey) {
// Try invalidating the realpath cache
clearstatcache(true, $key);
$rawKey = file_get_contents($key);
}
$contents[] = $rawKey;
}
return $contents;
}
private function readKey($type): ?string
{
$isPublic = self::TYPE_PUBLIC === $type;
$key = $isPublic ? $this->publicKey : $this->signingKey;
if (!$key || !is_file($key) || !is_readable($key)) {
if ($isPublic) {
return null;
}
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));
}
$rawKey = @file_get_contents($key);
if (false === $rawKey) {
// Try invalidating the realpath cache
clearstatcache(true, $key);
$rawKey = file_get_contents($key);
}
return $rawKey;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface KeyDumperInterface
{
/**
* Dumps a key to be shared between parties.
*
* @return resource|string
*/
public function dumpKey();
}
@@ -0,0 +1,31 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Interface for classes that are able to load crypto keys.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface KeyLoaderInterface
{
public const TYPE_PUBLIC = 'public';
public const TYPE_PRIVATE = 'private';
/**
* Loads a key from a given type (public or private).
*
* @param resource|string|null $type
*
* @return resource|string|null
*/
public function loadKey($type);
public function getPassphrase(): ?string;
public function getSigningKey(): ?string;
public function getPublicKey(): ?string;
public function getAdditionalPublicKeys(): array;
}
@@ -0,0 +1,52 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader;
/**
* Reads crypto keys.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class RawKeyLoader extends AbstractKeyLoader implements KeyDumperInterface
{
/**
* @param string $type
*
* @return string
*
* @throws \RuntimeException If the key cannot be read
*/
public function loadKey($type)
{
if (!in_array($type, [self::TYPE_PUBLIC, self::TYPE_PRIVATE])) {
throw new \InvalidArgumentException(sprintf('The key type must be "public" or "private", "%s" given.', $type));
}
if (self::TYPE_PUBLIC === $type) {
return $this->dumpKey();
}
return $this->getSigningKey();
}
/**
* {@inheritdoc}
*/
public function dumpKey()
{
if ($publicKey = $this->getPublicKey()) {
return $publicKey;
}
$signingKey = $this->getSigningKey();
// no public key provided, compute it from signing key
try {
$publicKey = openssl_pkey_get_details(openssl_pkey_get_private($signingKey, $this->getPassphrase()))['key'];
} catch (\Throwable $e) {
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?');
}
return $publicKey;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ChainEnrichment implements PayloadEnrichmentInterface
{
private $enrichments;
/**
* @param PayloadEnrichmentInterface[] $enrichments
*/
public function __construct(array $enrichments)
{
$this->enrichments = $enrichments;
}
public function enrich(UserInterface $user, array &$payload): void
{
foreach ($this->enrichments as $enrichment) {
$enrichment->enrich($user, $payload);
}
}
}
@@ -0,0 +1,13 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class NullEnrichment implements PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void
{
}
}
@@ -0,0 +1,14 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichment;
use Lexik\Bundle\JWTAuthenticationBundle\Services\PayloadEnrichmentInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class RandomJtiEnrichment implements PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void
{
$payload['jti'] = bin2hex(random_bytes(16));
}
}
@@ -0,0 +1,10 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services;
use Symfony\Component\Security\Core\User\UserInterface;
interface PayloadEnrichmentInterface
{
public function enrich(UserInterface $user, array &$payload): void;
}
@@ -0,0 +1,141 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken;
use Jose\Bundle\JoseFramework\Services\JWEBuilder;
use Jose\Bundle\JoseFramework\Services\JWEBuilderFactory;
use Jose\Bundle\JoseFramework\Services\JWSBuilder;
use Jose\Bundle\JoseFramework\Services\JWSBuilderFactory;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
use Lexik\Bundle\JWTAuthenticationBundle\Event\BeforeJWEComputationEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class AccessTokenBuilder
{
/**
* @var JWSBuilder
*/
private $jwsBuilder;
/**
* @var null|JWEBuilder
*/
private $jweBuilder = null;
/**
* @var JWK
*/
private $signatureKey;
/**
* @var JWK|null
*/
private $encryptionKey;
/**
* @var string
*/
private $signatureAlgorithm;
/**
* @var string|null
*/
private $keyEncryptionAlgorithm;
/**
* @var string|null
*/
private $contentEncryptionAlgorithm;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
public function __construct(
EventDispatcherInterface $dispatcher,
JWSBuilderFactory $jwsBuilderFactory,
?JWEBuilderFactory $jweBuilderFactory,
string $signatureAlgorithm,
string $signatureKey,
?string $keyEncryptionAlgorithm,
?string $contentEncryptionAlgorithm,
?string $encryptionKey
) {
$this->jwsBuilder = $jwsBuilderFactory->create([$signatureAlgorithm]);
if ($jweBuilderFactory !== null && $keyEncryptionAlgorithm !== null && $contentEncryptionAlgorithm !== null) {
$this->jweBuilder = $jweBuilderFactory->create([$keyEncryptionAlgorithm, $contentEncryptionAlgorithm]);
}
$this->signatureKey = JWK::createFromJson($signatureKey);
$this->encryptionKey = $encryptionKey ? JWK::createFromJson($encryptionKey) : null;
$this->signatureAlgorithm = $signatureAlgorithm;
$this->keyEncryptionAlgorithm = $keyEncryptionAlgorithm;
$this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
$this->dispatcher = $dispatcher;
}
public function build(array $header, array $claims): string
{
$token = $this->buildJWS($header, $claims);
if ($this->jweBuilder !== null) {
$token = $this->buildJWE($claims, $token);
}
return $token;
}
/**
* @param array<string, mixed> $header
* @param array<string, mixed> $claims
*/
private function buildJWS(array $header, array $claims): string
{
$header['alg'] = $this->signatureAlgorithm;
if ($this->signatureKey->has('kid')) {
$header['kid'] = $this->signatureKey->get('kid');
}
$jws = $this->jwsBuilder
->create()
->withPayload(json_encode($claims, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
->addSignature($this->signatureKey, $header)
->build()
;
$token = (new JwsCompactSerializer())->serialize($jws);
return $token;
}
/**
* @param array<string, mixed> $header
*/
private function buildJWE(array $claims, string $payload): string
{
$header = [
'alg' => $this->keyEncryptionAlgorithm,
'enc' => $this->contentEncryptionAlgorithm,
'cty' => 'JWT',
'typ' => 'JWT',
];
if ($this->encryptionKey->has('kid')) {
$header['kid'] = $this->encryptionKey->get('kid');
}
foreach (['exp', 'iat', 'nbf'] as $claim) {
if (array_key_exists($claim, $claims)) {
$header[$claim] = $claims[$claim];
}
}
$event = $this->dispatcher->dispatch(new BeforeJWEComputationEvent($header), Events::BEFORE_JWE_COMPUTATION);
$jwe = $this->jweBuilder
->create()
->withPayload($payload)
->withSharedProtectedHeader($event->getHeader())
->addRecipient($this->encryptionKey)
->build()
;
return (new JweCompactSerializer())->serialize($jwe);
}
}
@@ -0,0 +1,126 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Services\WebToken;
use Jose\Bundle\JoseFramework\Services\ClaimCheckerManager;
use Jose\Bundle\JoseFramework\Services\ClaimCheckerManagerFactory;
use Jose\Bundle\JoseFramework\Services\HeaderCheckerManager;
use Jose\Bundle\JoseFramework\Services\JWELoader;
use Jose\Bundle\JoseFramework\Services\JWELoaderFactory;
use Jose\Bundle\JoseFramework\Services\JWSLoader;
use Jose\Bundle\JoseFramework\Services\JWSLoaderFactory;
use Jose\Component\Checker\InvalidClaimException;
use Jose\Component\Checker\MissingMandatoryClaimException;
use Jose\Component\Core\JWKSet;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
final class AccessTokenLoader
{
private $jwsLoader;
private $jwsHeaderCheckerManager;
private $claimCheckerManager;
private $jweLoader;
private $signatureKeyset;
private $encryptionKeyset;
/**
* @var string[]
*/
private $mandatoryClaims;
private $continueOnDecryptionFailure;
public function __construct(
JWSLoaderFactory $jwsLoaderFactory,
?JWELoaderFactory $jweLoaderFactory,
ClaimCheckerManagerFactory $claimCheckerManagerFactory,
array $claimChecker,
array $jwsHeaderChecker,
array $mandatoryClaims,
array $signatureAlgorithms,
string $signatureKeyset,
?bool $continueOnDecryptionFailure,
?array $jweHeaderChecker,
?array $keyEncryptionAlgorithms,
?array $contentEncryptionAlgorithms,
?string $encryptionKeyset
) {
$this->jwsLoader = $jwsLoaderFactory->create(['jws_compact'], $signatureAlgorithms, $jwsHeaderChecker);
if ($jweLoaderFactory !== null && $keyEncryptionAlgorithms !== null && $contentEncryptionAlgorithms !== null && $jweHeaderChecker !== null) {
$this->jweLoader = $jweLoaderFactory->create(['jwe_compact'], array_merge($keyEncryptionAlgorithms, $contentEncryptionAlgorithms), null, null, $jweHeaderChecker);
$this->continueOnDecryptionFailure = $continueOnDecryptionFailure;
}
$this->signatureKeyset = JWKSet::createFromJson($signatureKeyset);
$this->encryptionKeyset = $encryptionKeyset ? JWKSet::createFromJson($encryptionKeyset) : null;
$this->claimCheckerManager = $claimCheckerManagerFactory->create($claimChecker);
$this->mandatoryClaims = $mandatoryClaims;
}
public function load(string $token): array
{
$token = $this->loadJWE($token);
$data = $this->loadJWS($token);
try {
$this->claimCheckerManager->check($data, $this->mandatoryClaims);
} catch (MissingMandatoryClaimException|InvalidClaimException $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, $e->getMessage(), $e, $data);
} catch (\Throwable $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to load the token', $e, $data);
}
return $data;
}
/**
* @return array<string, mixed>
*/
private function loadJWS(string $token): array
{
$payload = null;
$data = null;
$signature = null;
try {
$jws = $this->jwsLoader->loadAndVerifyWithKeySet($token, $this->signatureKeyset, $signature);
} catch (\Throwable $e) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be loaded or the signature cannot be verified.');
}
if ($signature !== 0) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token shall contain only one signature.');
}
$payload = $jws->getPayload();
if (!$payload) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
}
$data = json_decode($payload, true);
if (!is_array($data)) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload. The token shall contain claims.');
}
return $data;
}
private function loadJWE(string $token): string
{
if (!$this->jweLoader) {
return $token;
}
$recipient = null;
try {
$jwe = $this->jweLoader->loadAndDecryptWithKeySet($token, $this->encryptionKeyset, $recipient);
} catch (\Throwable $e) {
if ($this->continueOnDecryptionFailure === true) {
return $token;
}
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token cannot be decrypted.', $e);
}
$token = $jwe->getPayload();
if (!$token || $recipient !== 0) {
throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token. The token has no valid content.');
}
return $token;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Signature;
/**
* Object representation of a newly created JSON Web Signature.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class CreatedJWS
{
private string $token;
private bool $signed;
public function __construct(string $token, bool $isSigned)
{
$this->token = $token;
$this->signed = $isSigned;
}
public function isSigned(): bool
{
return $this->signed;
}
public function getToken(): string
{
return $this->token;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Signature;
/**
* Object representation of a JSON Web Signature loaded from an
* existing JSON Web Token.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
final class LoadedJWS
{
public const VERIFIED = 'verified';
public const EXPIRED = 'expired';
public const INVALID = 'invalid';
private array $header;
private array $payload;
private ?string $state = null;
private int $clockSkew;
private bool $shouldCheckExpiration;
public function __construct(array $payload, bool $isVerified, bool $shouldCheckExpiration = true, array $header = [], int $clockSkew = 0)
{
$this->payload = $payload;
$this->header = $header;
$this->shouldCheckExpiration = $shouldCheckExpiration;
$this->clockSkew = $clockSkew;
if (true === $isVerified) {
$this->state = self::VERIFIED;
}
$this->checkIssuedAt();
$this->checkExpiration();
}
public function getHeader(): array
{
return $this->header;
}
public function getPayload(): array
{
return $this->payload;
}
public function isVerified(): bool
{
return self::VERIFIED === $this->state;
}
public function isExpired(): bool
{
$this->checkExpiration();
return self::EXPIRED === $this->state;
}
public function isInvalid(): bool
{
return self::INVALID === $this->state;
}
private function checkExpiration(): void
{
if (!$this->shouldCheckExpiration) {
return;
}
if (!isset($this->payload['exp']) || !is_numeric($this->payload['exp'])) {
$this->state = self::INVALID;
return;
}
if ($this->clockSkew <= time() - $this->payload['exp']) {
$this->state = self::EXPIRED;
}
}
/**
* Ensures that the iat claim is not in the future.
*/
private function checkIssuedAt(): void
{
if (isset($this->payload['iat']) && (int) $this->payload['iat'] - $this->clockSkew > time()) {
$this->state = self::INVALID;
}
}
}
@@ -0,0 +1,43 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\Subscriber;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class AdditionalAccessTokenClaimsAndHeaderSubscriber implements EventSubscriberInterface
{
/**
* @var int|null
*/
private $ttl;
public function __construct(?int $ttl)
{
$this->ttl = $ttl;
}
public static function getSubscribedEvents(): array
{
return [
Events::JWT_CREATED => [
['addClaims'],
],
];
}
public function addClaims(JWTCreatedEvent $event): void
{
$claims = [
'jti' => uniqid('', true),
'iat' => time(),
'nbf' => time(),
];
$data = $event->getData();
if (!array_key_exists('exp', $data) && $this->ttl > 0) {
$claims['exp'] = time() + $this->ttl;
}
$event->setData(array_merge($claims, $data));
}
}
@@ -0,0 +1,47 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* AuthorizationHeaderTokenExtractor.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
class AuthorizationHeaderTokenExtractor implements TokenExtractorInterface
{
protected ?string $prefix;
protected string $name;
public function __construct(?string $prefix, string $name)
{
$this->prefix = $prefix;
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function extract(Request $request)
{
if (!$request->headers->has($this->name)) {
return false;
}
$authorizationHeader = $request->headers->get($this->name);
if (empty($this->prefix)) {
return $authorizationHeader;
}
$headerParts = explode(' ', (string) $authorizationHeader);
if (!(2 === count($headerParts) && 0 === strcasecmp($headerParts[0], $this->prefix))) {
return false;
}
return $headerParts[1];
}
}
@@ -0,0 +1,93 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* ChainTokenExtractor is the class responsible of extracting a JWT token
* from a {@link Request} object using all mapped token extractors.
*
* Note: The extractor map is reinitialized to the configured extractors for
* each different instance.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ChainTokenExtractor implements \IteratorAggregate, TokenExtractorInterface
{
private array $map;
public function __construct(array $map)
{
$this->map = $map;
}
/**
* Adds a new token extractor to the map.
*/
public function addExtractor(TokenExtractorInterface $extractor)
{
$this->map[] = $extractor;
}
/**
* Removes a token extractor from the map.
*
* @param \Closure $filter A function taking an extractor as argument, used to find the extractor to remove.
*
* @return bool True in case of success, false otherwise
*/
public function removeExtractor(\Closure $filter)
{
$filtered = array_filter($this->map, $filter);
if (!$extractorToUnmap = current($filtered)) {
return false;
}
$key = array_search($extractorToUnmap, $this->map);
unset($this->map[$key]);
return true;
}
/**
* Clears the token extractor map.
*/
public function clearMap()
{
$this->map = [];
}
/**
* Iterates over the token extractors map calling {@see extract()}
* until a token is found.
*
* {@inheritdoc}
*/
public function extract(Request $request)
{
foreach ($this->getIterator() as $extractor) {
if ($token = $extractor->extract($request)) {
return $token;
}
}
return false;
}
/**
* Iterates over the mapped token extractors while generating them.
*
* @return \Traversable<int, TokenExtractorInterface>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->map as $extractor) {
if ($extractor instanceof TokenExtractorInterface) {
yield $extractor;
}
}
}
}
@@ -0,0 +1,28 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* CookieTokenExtractor.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
class CookieTokenExtractor implements TokenExtractorInterface
{
protected string $name;
public function __construct(string $name)
{
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function extract(Request $request)
{
return $request->cookies->get($this->name, false);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* QueryParameterTokenExtractor.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
class QueryParameterTokenExtractor implements TokenExtractorInterface
{
protected string $parameterName;
public function __construct(string $parameterName)
{
$this->parameterName = $parameterName;
}
/**
* {@inheritdoc}
*/
public function extract(Request $request)
{
return $request->query->get($this->parameterName, false);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* SplitCookieExtractor.
*
* @author Adam Lukacovic <adam@adamlukacovic.sk>
*/
class SplitCookieExtractor implements TokenExtractorInterface
{
private array $cookies;
public function __construct(array $cookies)
{
$this->cookies = $cookies;
}
/**
* {@inheritDoc}
*/
public function extract(Request $request)
{
$jwtCookies = [];
foreach ($this->cookies as $cookie) {
$jwtCookies[] = $request->cookies->get($cookie, false);
}
if (count($this->cookies) !== count(array_filter($jwtCookies))) {
return false;
}
return implode('.', $jwtCookies);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor;
use Symfony\Component\HttpFoundation\Request;
/**
* TokenExtractorInterface.
*
* @author Nicolas Cabot <n.cabot@lexik.fr>
*/
interface TokenExtractorInterface
{
/**
* @return string|false
*/
public function extract(Request $request);
}
+89
View File
@@ -0,0 +1,89 @@
{
"name": "lexik/jwt-authentication-bundle",
"type": "symfony-bundle",
"description": "This bundle provides JWT authentication for your Symfony REST API",
"keywords": ["Symfony", "bundle", "jwt", "jws", "authentication", "api", "rest"],
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
"license": "MIT",
"authors": [
{
"name": "Jeremy Barthe",
"email": "j.barthe@lexik.fr",
"homepage": "https://github.com/jeremyb"
},
{
"name": "Nicolas Cabot",
"email": "n.cabot@lexik.fr",
"homepage": "https://github.com/slashfan"
},
{
"name": "Cedric Girard",
"email": "c.girard@lexik.fr",
"homepage": "https://github.com/cedric-g"
},
{
"name": "Dev Lexik",
"email": "dev@lexik.fr",
"homepage": "https://github.com/lexik"
},
{
"name": "Robin Chalas",
"email": "robin.chalas@gmail.com",
"homepage": "https://github.com/chalasr"
},
{
"name": "Lexik Community",
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
}
],
"require": {
"php": ">=8.2",
"ext-openssl": "*",
"lcobucci/jwt": "^5.0",
"lcobucci/clock": "^3.0",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.4|^3.0",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/security-bundle": "^6.4|^7.0",
"symfony/translation-contracts": "^1.0|^2.0|^3.0"
},
"require-dev": {
"api-platform/core": "^3.0",
"symfony/browser-kit": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/dom-crawler": "^6.4|^7.0",
"symfony/filesystem": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0"
},
"suggest": {
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
},
"autoload": {
"psr-4": {
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"config": {
"sort-packages": true
},
"scripts": {
"test": [
"vendor/bin/simple-phpunit --exclude-group web-token",
"ENCODER=lcobucci vendor/bin/simple-phpunit --exclude-group web-token",
"ENCODER=lcobucci ALGORITHM=HS256 vendor/bin/simple-phpunit --exclude-group web-token",
"ENCODER=user_id_claim vendor/bin/simple-phpunit --exclude-group web-token",
"PROVIDER=lexik_jwt vendor/bin/simple-phpunit --exclude-group web-token"
]
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer;
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocScalarFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->sets([SetList::PSR_12]);
$config->rule(OrderedImportsFixer::class);
$config->ruleWithConfiguration(ArraySyntaxFixer::class, [
'syntax' => 'short',
]);
$config->parallel();
$config->paths([__DIR__]);
$config->skip([
__DIR__ . '/.github',
__DIR__ . '/vendor',
PhpdocScalarFixer::class
]);
};
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_71,
SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
]);
$rectorConfig->phpVersion(\Rector\ValueObject\PhpVersion::PHP_71);
$rectorConfig->importShortClasses(false);
$rectorConfig->importNames();
$rectorConfig->bootstrapFiles([
__DIR__ . '/vendor/autoload.php',
]);
$rectorConfig->parallel();
$rectorConfig->paths([__DIR__]);
$rectorConfig->skip([
// Path
__DIR__ . '/.github',
__DIR__ . '/DependencyInjection/Configuration.php',
__DIR__ . '/Tests/DependencyInjection/LexikJWTAuthenticationExtensionTest.php',
__DIR__ . '/vendor',
// Rules
JsonThrowOnErrorRector::class,
ReturnNeverTypeRector::class => [
__DIR__ . '/Security/User/JWTUserProvider.php',
],
]);
};