diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php index 7be00eaff35df..f59afef4596b3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php @@ -38,11 +38,21 @@ public function create(ContainerBuilder $container, string $id, array|string $co // @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'])); + $algorithmDefinition = new Reference('security.access_token_handler.oidc.signature.'.$config['algorithm']); } else { - $tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature')) + $algorithmDefinition = (new ChildDefinition('security.access_token_handler.oidc.signature')) ->replaceArgument(0, $config['algorithm']) - ); + ; + } + + $algorithmManagerDefinition = $container->setDefinition($id.'.algorithm_manager', (new ChildDefinition('security.access_token_handler.oidc.algorithm_manager')) + ->replaceArgument(0, [$algorithmDefinition]) + ); + + $tokenHandlerDefinition->replaceArgument(0, $algorithmManagerDefinition); + + if (!isset($config['key'])) { + throw new LogicException('You should defined key parameter in configuration.'); } $tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwk')) @@ -80,7 +90,6 @@ public function addConfiguration(NodeBuilder $node): void ->end() ->scalarNode('key') ->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).') - ->isRequired() ->end() ->end() ->end() 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..aaddcf39a5679 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 @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Jose\Component\Core\Algorithm; +use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\Algorithm\ES384; @@ -89,6 +90,12 @@ abstract_arg('signature algorithm'), ]) + ->set('security.access_token_handler.oidc.algorithm_manager', AlgorithmManager::class) + ->abstract() + ->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']) diff --git a/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php b/src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php index e72e0d1eb67c4..3f57b4c8a4e64 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; @@ -37,15 +38,23 @@ final class OidcTokenHandler implements AccessTokenHandlerInterface { use OidcTrait; + private AlgorithmManager $algorithmManager; + public function __construct( - private Algorithm $signatureAlgorithm, - private JWK $jwk, + private Algorithm|AlgorithmManager $signatureAlgorithm, + private JWK|JWKSet $jwk, private string $audience, private array $issuers, private string $claim = 'sub', private ?LoggerInterface $logger = null, private ClockInterface $clock = new Clock(), ) { + if ($this->signatureAlgorithm instanceof Algorithm) { + trigger_deprecation('symfony/security-http', '7.1', 'First argument must be instance of %s, %s given.', AlgorithmManager::class, Algorithm::class); + $this->algorithmManager = new AlgorithmManager([$this->signatureAlgorithm]); + } else { + $this->algorithmManager = $signatureAlgorithm; + } } public function getUserBadgeFrom(string $accessToken): UserBadge @@ -56,19 +65,27 @@ public function getUserBadgeFrom(string $accessToken): UserBadge try { // Decode the token - $jwsVerifier = new JWSVerifier(new AlgorithmManager([$this->signatureAlgorithm])); + $jwsVerifier = new JWSVerifier($this->algorithmManager); $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 ($this->jwk instanceof JWK) { + if (!$jwsVerifier->verifyWithKey($jws, $this->jwk, 0)) { + throw new InvalidSignatureException(); + } + } elseif ($this->jwk instanceof JWKSet) { + if (!$jwsVerifier->verifyWithKeySet($jws, $this->jwk, 0)) { + throw new InvalidSignatureException(); + } + } else { throw new InvalidSignatureException(); } // Verify the headers $headerCheckerManager = new Checker\HeaderCheckerManager([ - new Checker\AlgorithmChecker([$this->signatureAlgorithm->name()]), + new Checker\AlgorithmChecker($this->algorithmManager->list()), ], [ new JWSTokenSupport(), ]); diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md index 58f227f37383d..45c6b746f0377 100644 --- a/src/Symfony/Component/Security/Http/CHANGELOG.md +++ b/src/Symfony/Component/Security/Http/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `#[IsCsrfTokenValid]` attribute + * Deprecate passing a Algorithm object as the 1st argument to the constructor of `Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler` 7.0 --- 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..1172a944b2eb6 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; @@ -32,9 +33,9 @@ class OidcTokenHandlerTest extends TestCase private const AUDIENCE = 'Symfony OIDC'; /** - * @dataProvider getClaims + * @dataProvider getClaimsAndJwk */ - public function testGetsUserIdentifierFromSignedToken(string $claim, string $expected) + public function testGetsUserIdentifierFromSignedToken(string $claim, string $expected, JWK|JWKSet $jwk) { $time = time(); $claims = [ @@ -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()]), + $jwk, self::AUDIENCE, ['https://www.example.com'], $claim, @@ -69,10 +70,12 @@ public function testGetsUserIdentifierFromSignedToken(string $claim, string $exp $this->assertEquals($claims['sub'], $actualUser->getUserIdentifier()); } - public static function getClaims(): iterable + public static function getClaimsAndJwk(): iterable { - yield ['sub', 'e21bf182-1538-406e-8ccb-e25a17aba39f']; - yield ['email', 'foo@example.com']; + yield ['sub', 'e21bf182-1538-406e-8ccb-e25a17aba39f', self::getJWK()]; + yield ['email', 'foo@example.com', self::getJWK()]; + yield ['sub', 'e21bf182-1538-406e-8ccb-e25a17aba39f', self::getJWKSet()]; + yield ['email', 'foo@example.com', self::getJWKSet()]; } /** @@ -87,7 +90,7 @@ public function testThrowsAnErrorIfTokenIsInvalid(string $token) $this->expectExceptionMessage('Invalid credentials.'); (new OidcTokenHandler( - new ES256(), + new AlgorithmManager([new ES256()]), $this->getJWK(), self::AUDIENCE, ['https://www.example.com'], @@ -146,7 +149,7 @@ public function testThrowsAnErrorIfUserPropertyIsMissing() $this->expectExceptionMessage('Invalid credentials.'); (new OidcTokenHandler( - new ES256(), + new AlgorithmManager([new ES256()]), self::getJWK(), self::AUDIENCE, ['https://www.example.com'], @@ -177,4 +180,16 @@ private static function getJWK(): JWK 'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220', ]); } + + private static function getJWKSet(): JWKSet + { + // tip: use https://mkjwk.org/ to generate a JWK + return new JWKSet([new JWK([ + 'kty' => 'EC', + 'crv' => 'P-256', + 'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4', + 'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo', + 'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220', + ])]); + } } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 3f96dc20c137b..34d18376b213a 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",