Skip to content

Commit

Permalink
feature #817 Feat: add keypair generation command (bpolaszek)
Browse files Browse the repository at this point in the history
This PR was submitted for the master branch but it was squashed and merged into the 2.x branch instead.

Discussion
----------

Feat: add keypair generation command

Hello there,

I'd like to remove the most annoying part of the bundle installation: leverage openssl extension to generate the key pair in a one-liner 🙂

If you like this one, with the `-n` option, we could update the flex recipe (unfamiliar with this, but I assume that's doable) so that public / private keys are generated on the fly during `composer install`.

Thoughts?
Ben

Commits
-------

a4c2d58 Feat: add keypair generation command
  • Loading branch information
chalasr committed Feb 9, 2021
2 parents 060c58a + a4c2d58 commit 02a8f68
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 02a8f68

Please sign in to comment.