From d9301721282b626dec1cd7ce0da1732167b4916a Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 24 Mar 2024 21:56:27 +0100 Subject: [PATCH] [Security] Support multiple signature algorithms and JWK/JWKSet for OIDC tokens --- composer.json | 3 +- .../Bundle/SecurityBundle/CHANGELOG.md | 2 + .../AccessToken/OidcTokenHandlerFactory.php | 65 +++++-- .../Factory/SignatureAlgorithmFactory.php | 43 ----- .../security_authenticator_access_token.php | 57 +++++-- .../Factory/AccessTokenFactoryTest.php | 159 ++++++++++++++++++ .../app/AccessToken/config_oidc.yml | 2 +- .../Bundle/SecurityBundle/composer.json | 7 +- .../AccessToken/Oidc/OidcTokenHandler.php | 19 ++- .../AccessToken/Oidc/OidcTokenHandlerTest.php | 27 ++- .../Component/Security/Http/composer.json | 4 +- 11 files changed, 296 insertions(+), 92 deletions(-) diff --git a/composer.json b/composer.json index 76b34d312c131..f43a4e0f55fdb 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index f704e00d92de1..abc0c49762e9f 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -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 --- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php index 7be00eaff35df..a1b418129f088 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php @@ -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. @@ -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']) ); } @@ -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() ->scalarNode('claim') ->info('Claim which contains the user identifier (e.g.: sub, email..).') @@ -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() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php index feb63c26350be..e69de29bb2d1d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SignatureAlgorithmFactory.php @@ -1,43 +0,0 @@ - - * - * 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)); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php index 66716b23ad892..c0fced49ae9ca 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php @@ -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; @@ -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') ; }; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php index f3e12e8190ced..65e54af3c6f4b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php @@ -98,6 +98,165 @@ public function testCasTokenHandlerConfiguration() $this->assertNull($arguments[3]); } + public function testInvalidOidcTokenHandlerConfigurationKeyMissing() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The child config "keyset" under "access_token.token_handler.oidc" must be configured: JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationDuplicatedKeyParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'key' => 'key', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('You cannot use both "key" and "keyset" at the same time.'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationDuplicatedAlgorithmParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'algorithms' => ['RS256'], + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('You cannot use both "algorithm" and "algorithms" at the same time.'); + + $this->processConfig($config, $factory); + } + + public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithmParameters() + { + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => 'keyset', + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The child config "algorithms" under "access_token.token_handler.oidc" must be configured: Algorithms used to sign the token.'); + + $this->processConfig($config, $factory); + } + + /** + * @group legacy + * + * @expectedDeprecation Since symfony/security-bundle 7.1: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. + */ + public function testOidcTokenHandlerConfigurationWithSingleAlgorithm() + { + $container = new ContainerBuilder(); + $jwk = '{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}'; + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithm' => 'RS256', + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'key' => $jwk, + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + $finalizedConfig = $this->processConfig($config, $factory); + + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1')); + $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1')); + + $expected = [ + 'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature')) + ->replaceArgument(0, ['RS256']), + 'index_1' => (new ChildDefinition('security.access_token_handler.oidc.jwkset')) + ->replaceArgument(0, sprintf('{"keys":[%s]}', $jwk)), + 'index_2' => 'audience', + 'index_3' => ['https://www.example.com'], + 'index_4' => 'sub', + ]; + $this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments()); + } + + public function testOidcTokenHandlerConfigurationWithMultipleAlgorithms() + { + $container = new ContainerBuilder(); + $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}'; + $config = [ + 'token_handler' => [ + 'oidc' => [ + 'algorithms' => ['RS256', 'ES256'], + 'issuers' => ['https://www.example.com'], + 'audience' => 'audience', + 'keyset' => $jwkset, + ], + ], + ]; + + $factory = new AccessTokenFactory($this->createTokenHandlerFactories()); + $finalizedConfig = $this->processConfig($config, $factory); + + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1')); + $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1')); + + $expected = [ + 'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature')) + ->replaceArgument(0, ['RS256', 'ES256']), + 'index_1' => (new ChildDefinition('security.access_token_handler.oidc.jwkset')) + ->replaceArgument(0, $jwkset), + 'index_2' => 'audience', + 'index_3' => ['https://www.example.com'], + 'index_4' => 'sub', + ]; + $this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments()); + } + public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml index dd770e4520e41..68f8a1f9dd47a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml @@ -26,7 +26,7 @@ security: issuers: [ 'https://www.example.com' ] algorithm: 'ES256' # tip: use https://mkjwk.org/ to generate a JWK - key: '{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}' + keyset: '{"keys":[{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}]}' token_extractors: 'header' realm: 'My API' diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 0ae91f9cfb023..b335e73e07a9d 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -51,12 +51,7 @@ "symfony/validator": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", "twig/twig": "^3.0.4", - "web-token/jwt-checker": "^3.1", - "web-token/jwt-signature-algorithm-hmac": "^3.1", - "web-token/jwt-signature-algorithm-ecdsa": "^3.1", - "web-token/jwt-signature-algorithm-rsa": "^3.1", - "web-token/jwt-signature-algorithm-eddsa": "^3.1", - "web-token/jwt-signature-algorithm-none": "^3.1" + "web-token/jwt-library": "^3.3.2" }, "conflict": { "symfony/browser-kit": "<6.4", diff --git a/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php b/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php index e72e0d1eb67c4..fc07220670651 100644 --- a/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php +++ b/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php @@ -16,6 +16,7 @@ use Jose\Component\Core\Algorithm; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; +use Jose\Component\Core\JWKSet; use Jose\Component\Signature\JWSTokenSupport; use Jose\Component\Signature\JWSVerifier; use Jose\Component\Signature\Serializer\CompactSerializer; @@ -38,14 +39,22 @@ final class OidcTokenHandler implements AccessTokenHandlerInterface use OidcTrait; public function __construct( - private Algorithm $signatureAlgorithm, - private JWK $jwk, + private Algorithm|AlgorithmManager $signatureAlgorithm, + private JWK|JWKSet $jwkset, private string $audience, private array $issuers, private string $claim = 'sub', private ?LoggerInterface $logger = null, private ClockInterface $clock = new Clock(), ) { + if ($signatureAlgorithm instanceof Algorithm) { + trigger_deprecation('symfony/security-http', '7.1', 'First argument must be instance of %s, %s given.', AlgorithmManager::class, Algorithm::class); + $this->signatureAlgorithm = new AlgorithmManager([$signatureAlgorithm]); + } + if ($jwkset instanceof JWK) { + trigger_deprecation('symfony/security-http', '7.1', 'Second argument must be instance of %s, %s given.', JWKSet::class, JWK::class); + $this->jwkset = new JWKSet([$jwkset]); + } } public function getUserBadgeFrom(string $accessToken): UserBadge @@ -56,19 +65,19 @@ public function getUserBadgeFrom(string $accessToken): UserBadge try { // Decode the token - $jwsVerifier = new JWSVerifier(new AlgorithmManager([$this->signatureAlgorithm])); + $jwsVerifier = new JWSVerifier($this->signatureAlgorithm); $serializerManager = new JWSSerializerManager([new CompactSerializer()]); $jws = $serializerManager->unserialize($accessToken); $claims = json_decode($jws->getPayload(), true); // Verify the signature - if (!$jwsVerifier->verifyWithKey($jws, $this->jwk, 0)) { + if (!$jwsVerifier->verifyWithKeySet($jws, $this->jwkset, 0)) { throw new InvalidSignatureException(); } // Verify the headers $headerCheckerManager = new Checker\HeaderCheckerManager([ - new Checker\AlgorithmChecker([$this->signatureAlgorithm->name()]), + new Checker\AlgorithmChecker($this->signatureAlgorithm->list()), ], [ new JWSTokenSupport(), ]); diff --git a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php index ae3ca5308b06a..396a591f66c00 100644 --- a/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/AccessToken/Oidc/OidcTokenHandlerTest.php @@ -13,6 +13,7 @@ use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; +use Jose\Component\Core\JWKSet; use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; @@ -53,8 +54,8 @@ public function testGetsUserIdentifierFromSignedToken(string $claim, string $exp $loggerMock->expects($this->never())->method('error'); $userBadge = (new OidcTokenHandler( - new ES256(), - $this->getJWK(), + new AlgorithmManager([new ES256()]), + $this->getJWKSet(), self::AUDIENCE, ['https://www.example.com'], $claim, @@ -87,8 +88,8 @@ public function testThrowsAnErrorIfTokenIsInvalid(string $token) $this->expectExceptionMessage('Invalid credentials.'); (new OidcTokenHandler( - new ES256(), - $this->getJWK(), + new AlgorithmManager([new ES256()]), + $this->getJWKSet(), self::AUDIENCE, ['https://www.example.com'], 'sub', @@ -146,8 +147,8 @@ public function testThrowsAnErrorIfUserPropertyIsMissing() $this->expectExceptionMessage('Invalid credentials.'); (new OidcTokenHandler( - new ES256(), - self::getJWK(), + new AlgorithmManager([new ES256()]), + self::getJWKSet(), self::AUDIENCE, ['https://www.example.com'], 'email', @@ -177,4 +178,18 @@ private static function getJWK(): JWK 'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220', ]); } + + private static function getJWKSet(): JWKSet + { + return new JWKSet([ + new JWK([ + 'kty' => 'EC', + 'crv' => 'P-256', + 'x' => 'FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars', + 'y' => 'rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY', + 'd' => '4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0', + ]), + self::getJWK(), + ]); + } } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 4bf955b99358c..7431b3c5694ee 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/polyfill-mbstring": "~1.0", @@ -35,8 +36,7 @@ "symfony/security-csrf": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "psr/log": "^1|^2|^3", - "web-token/jwt-checker": "^3.1", - "web-token/jwt-signature-algorithm-ecdsa": "^3.1" + "web-token/jwt-library": "^3.3.2" }, "conflict": { "symfony/clock": "<6.4",