The start of something beautiful
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+349
@@ -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))
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user