Skip to content

[Security] Add clock dependency to OidcTokenHandler #50477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

use Jose\Component\Core\Algorithm;
use Jose\Component\Core\JWK;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -28,26 +27,28 @@ class OidcTokenHandlerFactory implements TokenHandlerFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array|string $config): void
{
$tokenHandlerDefinition = $container->setDefinition($id, new ChildDefinition('security.access_token_handler.oidc'));
$tokenHandlerDefinition->replaceArgument(3, $config['claim']);
$tokenHandlerDefinition->replaceArgument(4, $config['audience']);
$tokenHandlerDefinition = $container->setDefinition($id, (new ChildDefinition('security.access_token_handler.oidc'))
->replaceArgument(4, $config['claim'])
->replaceArgument(5, $config['audience'])
);

// Create the signature algorithm and the JWK
if (!ContainerBuilder::willBeAvailable('web-token/jwt-core', Algorithm::class, ['symfony/security-bundle'])) {
$container->register('security.access_token_handler.oidc.signature', 'stdClass')
$container->register('security.access_token_handler.oidc.signature', Algorithm::class)
->addError('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "web-token/jwt-core".');
$container->register('security.access_token_handler.oidc.jwk', 'stdClass')
$container->register('security.access_token_handler.oidc.jwk', JWK::class)
->addError('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "web-token/jwt-core".');
}

if (\in_array($config['algorithm'], ['ES256', 'ES384', 'ES512'], true)) {
$tokenHandlerDefinition->replaceArgument(0, new Reference('security.access_token_handler.oidc.signature.'.$config['algorithm']));
} else {
$container->register('security.access_token_handler.oidc.signature', Algorithm::class)
->setFactory([SignatureAlgorithmFactory::class, 'create'])
->setArguments([$config['signature']['algorithm']]);
$container->register('security.access_token_handler.oidc.jwk', JWK::class)
->setFactory([JWK::class, 'createFromJson'])
->setArguments([$config['signature']['key']]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the wiring. Before this change, all firewalls would share a single jwk+algo definition, since varying key/algo were registered under the same id

$tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature')))
->replaceArgument(0, $config['algorithm']);
}
$tokenHandlerDefinition->replaceArgument(0, new Reference('security.access_token_handler.oidc.signature'));
$tokenHandlerDefinition->replaceArgument(1, new Reference('security.access_token_handler.oidc.jwk'));

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

public function getKey(): string
Expand All @@ -69,18 +70,13 @@ public function addConfiguration(NodeBuilder $node): void
->info('Audience set in the token, for validation purpose.')
->defaultNull()
->end()
->arrayNode('signature')
->scalarNode('algorithm')
->info('Algorithm used to sign the token.')
->isRequired()
->end()
->scalarNode('key')
->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).')
->isRequired()
->children()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing one unneeded nesting level in the config /cc @vincentchalamon for the doc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature level was useful in anticipation of #50441. But removing it is more MVP. Let's talk about it later on #50441

->scalarNode('algorithm')
->info('Algorithm used to sign the token.')
->isRequired()
->end()
->scalarNode('key')
->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).')
->isRequired()
->end()
->end()
->end()
->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

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

use Jose\Component\Core\Algorithm as SignatureAlgorithm;
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;
Expand All @@ -23,29 +23,21 @@
*/
final class SignatureAlgorithmFactory
{
public static function create(string $algorithm): SignatureAlgorithm
public static function create(string $algorithm): AlgorithmInterface
{
switch ($algorithm) {
case 'ES256':
if (!class_exists(Algorithm\ES256::class)) {
throw new \LogicException('You cannot use the "ES256" signature algorithm since "web-token/jwt-signature-algorithm-ecdsa" is not installed. Try running "composer require web-token/jwt-signature-algorithm-ecdsa".');
}

return new Algorithm\ES256();
case 'ES384':
if (!class_exists(Algorithm\ES384::class)) {
throw new \LogicException('You cannot use the "ES384" signature algorithm since "web-token/jwt-signature-algorithm-ecdsa" is not installed. Try running "composer require web-token/jwt-signature-algorithm-ecdsa".');
}

return new Algorithm\ES384();
case 'ES512':
if (!class_exists(Algorithm\ES512::class)) {
throw new \LogicException('You cannot use the "ES512" signature algorithm since "web-token/jwt-signature-algorithm-ecdsa" is not installed. Try running "composer require web-token/jwt-signature-algorithm-ecdsa".');
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));
}

return new Algorithm\ES512();
default:
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));
$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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -335,16 +335,8 @@
</xsd:complexType>

<xsd:complexType name="oidc">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="signature" type="oidc_signature" />
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="claim" type="xsd:string" />
<xsd:attribute name="audience" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="oidc_signature">
<xsd:attribute name="algorithm" type="xsd:string" use="required" />
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:complexType>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Jose\Component\Core\Algorithm;
use Jose\Component\Core\JWK;
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 Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor;
use Symfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor;
use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor;
Expand Down Expand Up @@ -62,10 +68,37 @@
->abstract()
->args([
abstract_arg('signature algorithm'),
abstract_arg('jwk'),
abstract_arg('signature key'),
service('logger')->nullOnInvalid(),
service('clock'),
'sub',
null,
])

->set('security.access_token_handler.oidc.jwk', JWK::class)
->abstract()
->factory([JWK::class, 'createFromJson'])
->args([
abstract_arg('signature key'),
])

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

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

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

->set('security.access_token_handler.oidc.signature.ES512', ES512::class)
->parent('security.access_token_handler.oidc.signature')
->args(['index_0' => 'ES512'])
;
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ security:
oidc:
claim: 'username'
audience: 'Symfony OIDC'
signature:
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"}'
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"}'
token_extractors: 'header'
realm: 'My API'

Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Bundle/SecurityBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"php": ">=8.1",
"composer-runtime-api": ">=2.1",
"ext-xml": "*",
"symfony/clock": "^6.3",
"symfony/config": "^6.1",
"symfony/dependency-injection": "^6.2",
"symfony/event-dispatcher": "^5.4|^6.0",
Expand All @@ -38,7 +39,7 @@
"symfony/dom-crawler": "^5.4|^6.0",
"symfony/expression-language": "^5.4|^6.0",
"symfony/form": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/framework-bundle": "^6.3",
"symfony/ldap": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
"symfony/rate-limiter": "^5.4|^6.0",
Expand All @@ -59,7 +60,7 @@
"conflict": {
"symfony/browser-kit": "<5.4",
"symfony/console": "<5.4",
"symfony/framework-bundle": "<5.4",
"symfony/framework-bundle": "<6.3",
"symfony/http-client": "<5.4",
"symfony/ldap": "<5.4",
"symfony/twig-bundle": "<5.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\NativeClock;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NativeClock should never be used directly since 6.3

use Symfony\Component\Clock\Clock;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\InvalidSignatureException;
Expand All @@ -41,6 +42,7 @@ public function __construct(
private Algorithm $signatureAlgorithm,
private JWK $jwk,
private ?LoggerInterface $logger = null,
private ClockInterface $clock = new Clock(),
private string $claim = 'sub',
private ?string $audience = null
) {
Expand Down Expand Up @@ -74,11 +76,10 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
$headerCheckerManager->check($jws, 0);

// Verify the claims
$clock = class_exists(NativeClock::class) ? new NativeClock() : null;
$checkers = [
new Checker\IssuedAtChecker(0, false, $clock),
new Checker\NotBeforeChecker(0, false, $clock),
new Checker\ExpirationTimeChecker(0, false, $clock),
new Checker\IssuedAtChecker(0, false, $this->clock),
new Checker\NotBeforeChecker(0, false, $this->clock),
new Checker\ExpirationTimeChecker(0, false, $this->clock),
];
if ($this->audience) {
$checkers[] = new Checker\AudienceChecker($this->audience);
Expand All @@ -93,7 +94,7 @@ public function getUserBadgeFrom(string $accessToken): UserBadge

// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
return new UserBadge($claims[$this->claim], fn () => $this->createUser($claims), $claims);
} catch (\Throwable $e) {
} catch (\Exception $e) {
$this->logger?->error('An error while decoding and validating the token.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function getUserBadgeFrom(string $accessToken): UserBadge

// UserLoader argument can be overridden by a UserProvider on AccessTokenAuthenticator::authenticate
return new UserBadge($claims[$this->claim], fn () => $this->createUser($claims), $claims);
} catch (\Throwable $e) {
} catch (\Exception $e) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should never catch Throwable unless reasons which should be explicit/obvious

$this->logger?->error('An error occurred on OIDC server.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Jose\Component\Signature\Serializer\CompactSerializer;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\OidcUser;
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;
Expand Down Expand Up @@ -55,6 +56,7 @@ public function testGetsUserIdentifierFromSignedToken(string $claim, string $exp
new ES256(),
$this->getJWK(),
$loggerMock,
new Clock(),
$claim,
self::AUDIENCE
))->getUserBadgeFrom($token);
Expand Down Expand Up @@ -88,6 +90,7 @@ public function testThrowsAnErrorIfTokenIsInvalid(string $token)
new ES256(),
$this->getJWK(),
$loggerMock,
new Clock(),
'sub',
self::AUDIENCE
))->getUserBadgeFrom($token);
Expand Down Expand Up @@ -146,6 +149,7 @@ public function testThrowsAnErrorIfUserPropertyIsMissing()
new ES256(),
self::getJWK(),
$loggerMock,
new Clock(),
'email',
self::AUDIENCE
))->getUserBadgeFrom($token);
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Security/Http/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"require-dev": {
"symfony/cache": "^5.4|^6.0",
"symfony/clock": "^6.3",
"symfony/expression-language": "^5.4|^6.0",
"symfony/http-client-contracts": "^3.0",
"symfony/rate-limiter": "^5.4|^6.0",
Expand All @@ -37,6 +38,7 @@
"web-token/jwt-signature-algorithm-ecdsa": "^3.1"
},
"conflict": {
"symfony/clock": "<6.3",
"symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9",
"symfony/http-client-contracts": "<3.0",
"symfony/security-bundle": "<5.4",
Expand Down