Skip to content

Commit

Permalink
[Security] Support multiple signature algorithms and JWK/JWKSet for O…
Browse files Browse the repository at this point in the history
…IDC tokens
  • Loading branch information
Spomky committed Mar 24, 2024
1 parent b5ee977 commit 949281a
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 86 deletions.
3 changes: 1 addition & 2 deletions composer.json
Expand Up @@ -158,8 +158,7 @@
"twig/cssinliner-extra": "^2.12|^3",
"twig/inky-extra": "^2.12|^3",
"twig/markdown-extra": "^2.12|^3",
"web-token/jwt-checker": "^3.1",
"web-token/jwt-signature-algorithm-ecdsa": "^3.1"
"web-token/jwt-library": "^3.3.2"
},
"conflict": {
"ext-psr": "<1.1|>=2",
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

* Mark class `ExpressionCacheWarmer` as `final`
* Support multiple signature algorithms for OIDC Token
* Support JWK or JWKSet for OIDC Token

7.0
---
Expand Down
Expand Up @@ -13,10 +13,10 @@

use Jose\Component\Core\Algorithm;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;

/**
* Configures a token handler for decoding and validating an OIDC token.
Expand All @@ -31,22 +31,15 @@ public function create(ContainerBuilder $container, string $id, array|string $co
->replaceArgument(4, $config['claim'])
);

if (!ContainerBuilder::willBeAvailable('web-token/jwt-core', Algorithm::class, ['symfony/security-bundle'])) {
throw new LogicException('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "composer require web-token/jwt-core".');
if (!ContainerBuilder::willBeAvailable('web-token/jwt-library', Algorithm::class, ['symfony/security-bundle'])) {
throw new LogicException('You cannot use the "oidc" token handler since "web-token/jwt-library" is not installed. Try running "composer require web-token/jwt-library".');
}

// @see Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory
// for supported algorithms
if (\in_array($config['algorithm'], ['ES256', 'ES384', 'ES512'], true)) {
$tokenHandlerDefinition->replaceArgument(0, new Reference('security.access_token_handler.oidc.signature.'.$config['algorithm']));
} else {
$tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature'))
->replaceArgument(0, $config['algorithm'])
);
}
$tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature'))
->replaceArgument(0, $config['algorithms']));

$tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwk'))
->replaceArgument(0, $config['key'])
$tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwkset'))
->replaceArgument(0, $config['keyset'])
);
}

Expand All @@ -60,6 +53,37 @@ public function addConfiguration(NodeBuilder $node): void
$node
->arrayNode($this->getKey())
->fixXmlConfig($this->getKey())
->validate()
->ifTrue(static fn ($v) => !isset($v['algorithm']) && !isset($v['algorithms']))
->thenInvalid('You must set either "algorithm" or "algorithms".')
->end()
->validate()
->ifTrue(static fn ($v) => !isset($v['key']) && !isset($v['keyset']))
->thenInvalid('You must set either "key" or "keyset".')
->end()
->beforeNormalization()
->ifTrue(static fn ($v) => isset($v['algorithm']) && \is_string($v['algorithm']))
->then(static function ($v) {
if (isset($v['algorithms'])) {
throw new InvalidConfigurationException('You cannot use both "algorithm" and "algorithms" at the same time.');
}
$v['algorithms'] = [$v['algorithm']];
unset($v['algorithm']);

return $v;
})
->end()
->beforeNormalization()
->ifTrue(static fn ($v) => isset($v['key']) && \is_string($v['key']))
->then(static function ($v) {
if (isset($v['keyset'])) {
throw new InvalidConfigurationException('You cannot use both "key" and "keyset" at the same time.');
}
$v['keyset'] = sprintf('{"keys":[%s]}', $v['key']);

return $v;
})
->end()
->children()

Check failure on line 87 in src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php:87:19: UndefinedMethod: Method Symfony\Component\Config\Definition\Builder\NodeDefinition::children does not exist (see https://psalm.dev/022)

Check failure on line 87 in src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php:87:19: UndefinedMethod: Method Symfony\Component\Config\Definition\Builder\NodeDefinition::children does not exist (see https://psalm.dev/022)
->scalarNode('claim')
->info('Claim which contains the user identifier (e.g.: sub, email..).')
Expand All @@ -72,14 +96,23 @@ public function addConfiguration(NodeBuilder $node): void
->arrayNode('issuers')
->info('Issuers allowed to generate the token, for validation purpose.')
->isRequired()
->prototype('scalar')->end()
->scalarPrototype()->end()
->end()
->scalarNode('algorithm')
->arrayNode('algorithm')
->info('Algorithm used to sign the token.')
->setDeprecated('symfony/security-bundle', '7.1', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "algorithms" option instead.')
->end()
->arrayNode('algorithms')
->info('Algorithms used to sign the token.')
->isRequired()
->scalarPrototype()->end()
->end()
->scalarNode('key')
->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).')
->setDeprecated('symfony/security-bundle', '7.1', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "keyset" option instead.')
->end()
->scalarNode('keyset')
->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).')
->isRequired()
->end()
->end()
Expand Down
@@ -1,43 +0,0 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;

use Jose\Component\Core\Algorithm as AlgorithmInterface;
use Jose\Component\Signature\Algorithm;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;

/**
* Creates a signature algorithm for {@see OidcTokenHandler}.
*
* @internal
*/
final class SignatureAlgorithmFactory
{
public static function create(string $algorithm): AlgorithmInterface
{
switch ($algorithm) {
case 'ES256':
case 'ES384':
case 'ES512':
if (!class_exists(Algorithm::class.'\\'.$algorithm)) {
throw new \LogicException(sprintf('You cannot use the "%s" signature algorithm since "web-token/jwt-signature-algorithm-ecdsa" is not installed. Try running "composer require web-token/jwt-signature-algorithm-ecdsa".', $algorithm));
}

$algorithm = Algorithm::class.'\\'.$algorithm;

return new $algorithm();
}

throw new InvalidArgumentException(sprintf('Unsupported signature algorithm "%s". Only ES* algorithms are supported. If you want to use another algorithm, create your TokenHandler as a service.', $algorithm));
}
}
Expand Up @@ -11,12 +11,19 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\AlgorithmManagerFactory;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Algorithm\ES384;
use Jose\Component\Signature\Algorithm\ES512;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory;
use Jose\Component\Signature\Algorithm\PS256;
use Jose\Component\Signature\Algorithm\PS384;
use Jose\Component\Signature\Algorithm\PS512;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\Algorithm\RS384;
use Jose\Component\Signature\Algorithm\RS512;
use Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor;
use Symfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor;
use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor;
Expand Down Expand Up @@ -77,28 +84,56 @@

->set('security.access_token_handler.oidc.jwk', JWK::class)
->abstract()
->deprecate('symfony/security-http', '7.1', 'The "%service_id%" service is deprecated. Please use "security.access_token_handler.oidc.jwkset" instead')
->factory([JWK::class, 'createFromJson'])
->args([
abstract_arg('signature key'),
])

->set('security.access_token_handler.oidc.signature', Algorithm::class)
->set('security.access_token_handler.oidc.jwkset', JWKSet::class)
->abstract()
->factory([SignatureAlgorithmFactory::class, 'create'])
->factory([JWKSet::class, 'createFromJson'])
->args([
abstract_arg('signature algorithm'),
abstract_arg('signature keyset'),
])

->set('security.access_token_handler.oidc.algorithm_manager_factory', AlgorithmManagerFactory::class)
->args([
tagged_iterator('security.access_token_handler.oidc.signature_algorithm'),
])

->set('security.access_token_handler.oidc.signature', AlgorithmManager::class)
->abstract()
->factory([service('security.access_token_handler.oidc.algorithm_manager_factory'), 'create'])
->args([
abstract_arg('signature algorithms'),
])

->set('security.access_token_handler.oidc.signature.ES256', ES256::class)
->parent('security.access_token_handler.oidc.signature')
->args(['index_0' => 'ES256'])
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.ES384', ES384::class)
->parent('security.access_token_handler.oidc.signature')
->args(['index_0' => 'ES384'])
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.ES512', ES512::class)
->parent('security.access_token_handler.oidc.signature')
->args(['index_0' => 'ES512'])
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.RS256', RS256::class)
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.RS384', RS384::class)
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.RS512', RS512::class)
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.PS256', PS256::class)
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.PS384', PS384::class)
->tag('security.access_token_handler.oidc.signature_algorithm')

->set('security.access_token_handler.oidc.signature.PS512', PS512::class)
->tag('security.access_token_handler.oidc.signature_algorithm')
;
};

0 comments on commit 949281a

Please sign in to comment.