Skip to content

Commit

Permalink
Feat: add keypair generation command
Browse files Browse the repository at this point in the history
  • Loading branch information
bpolaszek authored and chalasr committed Feb 9, 2021
1 parent 060c58a commit a4c2d58
Show file tree
Hide file tree
Showing 6 changed files with 474 additions and 3 deletions.
225 changes: 225 additions & 0 deletions Command/GenerateKeyPairCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Command;

use Symfony\Component\Console\Command\Command;
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>
*/
final class GenerateKeyPairCommand extends Command
{
private const ACCEPTED_ALGORITHMS = [
'RS256',
'RS384',
'RS512',
'HS256',
'HS384',
'HS512',
'ES256',
'ES384',
'ES512',
];

protected static $defaultName = 'lexik:jwt:generate-keypair';

/**
* @var Filesystem
*/
private $filesystem;

/**
* @var string
*/
private $secretKey;

/**
* @var string
*/
private $publicKey;

/**
* @var string|null
*/
private $passphrase;

/**
* @var string
*/
private $algorithm;

public function __construct(Filesystem $filesystem, string $secretKey, string $publicKey, ?string $passphrase, string $algorithm)
{
parent::__construct();
$this->filesystem = $filesystem;
$this->secretKey = $secretKey;
$this->publicKey = $publicKey;
$this->passphrase = $passphrase;
$this->algorithm = $algorithm;
}

protected function configure(): void
{
$this->setDescription('Generate public/private keys for use in your application.');
$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 1;
}

[$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 0;
}

$alreadyExists = $this->filesystem->exists($this->secretKey) || $this->filesystem->exists($this->publicKey);

if (true === $alreadyExists) {
try {
$this->handleExistingKeys($input);
} catch (\RuntimeException $e) {
if (0 === $e->getCode()) {
$io->comment($e->getMessage());

return 0;
}

$io->error($e->getMessage());

return 1;
}

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 0;
}
}

$this->filesystem->dumpFile($this->secretKey, $secretKey);
$this->filesystem->dumpFile($this->publicKey, $publicKey);

$io->success('Done!');

return 0;
}

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($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' => 384,
'HS384' => 384,
'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;
}
}
7 changes: 7 additions & 0 deletions DependencyInjection/LexikJWTAuthenticationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ public function load(array $configs, ContainerBuilder $container)
->getDefinition('lexik_jwt_authentication.handler.authentication_success')
->replaceArgument(2, new IteratorArgument($cookieProviders));
}

$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']);
}

private static function createTokenExtractors(ContainerBuilder $container, array $tokenExtractorsConfig)
Expand Down
9 changes: 9 additions & 0 deletions Resources/config/console.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
<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>
12 changes: 9 additions & 3 deletions Resources/doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ return [
#### Generate the SSL keys:

``` bash
$ mkdir -p config/jwt
$ openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
$ openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
$ php bin/console lexik:jwt:generate-keypair
```

Your keys will land in `config/jwt/private.pem` and `config/jwt/public.pem` (unless you configured a different path).

Available options:
- `--skip-if-exists` will silently do nothing if keys already exist.
- `--overwrite` will overwrite your keys if they already exist.

Otherwise, an error will be raised to prevent you from overwriting your keys accidentally.

Configuration
-------------

Expand Down
3 changes: 3 additions & 0 deletions Tests/DependencyInjection/AutowiringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ private static function createContainerBuilder(array $configs = [])
'kernel.runtime_environment' => 'test',
'env(base64:default::SYMFONY_DECRYPTION_SECRET)' => 'dummy',
'kernel.build_dir' => __DIR__,
'env(default::resolve:JWT_SECRET_KEY)' => __DIR__,
'env(default::resolve:JWT_PUBLIC_KEY)' => __DIR__,
'env(default::JWT_PASSPHRASE)' => 'dummy',
]));

$container->registerExtension(new SecurityExtension());
Expand Down
Loading

0 comments on commit a4c2d58

Please sign in to comment.