Skip to content
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 @@ -48,16 +48,18 @@ public function create(ContainerBuilder $container, string $id, array|string $co

// disable JWKSet argument
$tokenHandlerDefinition->replaceArgument(1, null);
$tokenHandlerDefinition->addMethodCall(
'enableDiscovery',
[
new Reference($config['discovery']['cache']['id']),
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
->replaceArgument(0, ['base_uri' => $config['discovery']['base_uri']]),
"$id.oidc_configuration",
"$id.oidc_jwk_set",
]
);

$clients = [];
foreach ($config['discovery']['base_uri'] as $uri) {
$clients[] = (new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
->replaceArgument(0, ['base_uri' => $uri]);
}

$tokenHandlerDefinition->addMethodCall('enableDiscovery', [
new Reference($config['discovery']['cache']['id']),
$clients,
"$id.oidc_configuration",
]);

return;
}
Expand Down Expand Up @@ -93,7 +95,7 @@ public function create(ContainerBuilder $container, string $id, array|string $co
;
}

$firewall = substr($id, strlen('security.access_token_handler.'));
$firewall = substr($id, \strlen('security.access_token_handler.'));
$container->getDefinition('security.access_token_handler.oidc.command.generate')
->addMethodCall('addGenerator', [
$firewall,
Expand Down Expand Up @@ -157,10 +159,11 @@ public function addConfiguration(NodeBuilder $node): void
->arrayNode('discovery')
->info('Enable the OIDC discovery.')
->children()
->scalarNode('base_uri')
->arrayNode('base_uri')
->acceptAndWrap(['string'])
->info('Base URI of the OIDC server.')
->isRequired()
->cannotBeEmpty()
->scalarPrototype()->end()
->end()
->arrayNode('cache')
->children()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

$container->loadFromExtension('security', [
'providers' => [
'default' => [
'memory' => null,
],
],
'firewalls' => [
'firewall1' => [
'provider' => 'default',
'access_token' => [
'token_handler' => [
'oidc_user_info' => [
'base_uri' => [
'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
'https://www.github.com/realms/demo/protocol/openid-connect/userinfo',
],
'discovery' => [
'cache' => [
'id' => 'oidc_cache',
],
],
],
],
],
],
],
]);
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithm()
public function testOidcTokenHandlerConfigurationWithDiscovery()
{
$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' => [
Expand Down Expand Up @@ -384,10 +383,68 @@ public function testOidcTokenHandlerConfigurationWithDiscovery()
'enableDiscovery',
[
new Reference('oidc_cache'),
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
[
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/']),
],
'security.access_token_handler.firewall1.oidc_configuration',
],
],
];
$this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
$this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
}

public function testOidcTokenHandlerConfigurationWithMultipleDiscoveryBaseUri()
{
$container = new ContainerBuilder();
$config = [
'token_handler' => [
'oidc' => [
'discovery' => [
'base_uri' => [
'https://www.example.com/realms/demo/',
'https://www.api.com/realms/api/',
],
'cache' => [
'id' => 'oidc_cache',
],
],
'algorithms' => ['RS256', 'ES256'],
'issuers' => ['https://www.example.com'],
'audience' => 'audience',
],
],
];

$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'));

$expectedArgs = [
'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature'))
->replaceArgument(0, ['RS256', 'ES256']),
'index_1' => null,
'index_2' => 'audience',
'index_3' => ['https://www.example.com'],
'index_4' => 'sub',
];
$expectedCalls = [
[
'enableDiscovery',
[
new Reference('oidc_cache'),
[
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/']),
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
->replaceArgument(0, ['base_uri' => 'https://www.api.com/realms/api/']),
],
'security.access_token_handler.firewall1.oidc_configuration',
'security.access_token_handler.firewall1.oidc_jwk_set',
],
],
];
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/SecurityBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"symfony/password-hasher": "^6.4|^7.0|^8.0",
"symfony/security-core": "^7.4|^8.0",
"symfony/security-csrf": "^6.4|^7.0|^8.0",
"symfony/security-http": "^7.3|^8.0",
"symfony/security-http": "^7.4|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ final class OidcTokenHandler implements AccessTokenHandlerInterface
private bool $enforceEncryption = false;

private ?CacheInterface $discoveryCache = null;
private ?HttpClientInterface $discoveryClient = null;
private ?string $oidcConfigurationCacheKey = null;
private ?string $oidcJWKSetCacheKey = null;

/**
* @var HttpClientInterface[]
*/
private array $discoveryClients = [];

public function __construct(
private Algorithm|AlgorithmManager $signatureAlgorithm,
Expand Down Expand Up @@ -78,12 +81,18 @@ public function enableJweSupport(JWKSet $decryptionKeyset, AlgorithmManager $dec
$this->enforceEncryption = $enforceEncryption;
}

public function enableDiscovery(CacheInterface $cache, HttpClientInterface $client, string $oidcConfigurationCacheKey, string $oidcJWKSetCacheKey): void
/**
* @param HttpClientInterface|HttpClientInterface[] $client
*/
public function enableDiscovery(CacheInterface $cache, array|HttpClientInterface $client, string $oidcConfigurationCacheKey, ?string $oidcJWKSetCacheKey = null): void
{
if (null !== $oidcJWKSetCacheKey) {
trigger_deprecation('symfony/security-http', '7.4', 'Passing $oidcJWKSetCacheKey parameter to "%s()" is deprecated.', __METHOD__);
}

$this->discoveryCache = $cache;
$this->discoveryClient = $client;
$this->discoveryClients = \is_array($client) ? $client : [$client];
$this->oidcConfigurationCacheKey = $oidcConfigurationCacheKey;
$this->oidcJWKSetCacheKey = $oidcJWKSetCacheKey;
}

public function getUserBadgeFrom(string $accessToken): UserBadge
Expand All @@ -92,45 +101,51 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
throw new \LogicException('You cannot use the "oidc" token handler since "web-token/jwt-signature" and "web-token/jwt-checker" are not installed. Try running "composer require web-token/jwt-signature web-token/jwt-checker".');
}

if (!$this->discoveryCache && !$this->signatureKeyset) {
if (!$this->discoveryClients && !$this->signatureKeyset) {
throw new \LogicException('You cannot use the "oidc" token handler without JWKSet nor "discovery". Please configure JWKSet in the constructor, or call "enableDiscovery" method.');
}

$jwkset = $this->signatureKeyset;
if ($this->discoveryCache) {
try {
$oidcConfiguration = json_decode($this->discoveryCache->get($this->oidcConfigurationCacheKey, function (): string {
$response = $this->discoveryClient->request('GET', '.well-known/openid-configuration');

return $response->getContent();
}), true, 512, \JSON_THROW_ON_ERROR);
} catch (\Throwable $e) {
$this->logger?->error('An error occurred while requesting OIDC configuration.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}

try {
$jwkset = JWKSet::createFromJson(
$this->discoveryCache->get($this->oidcJWKSetCacheKey, function () use ($oidcConfiguration): string {
$response = $this->discoveryClient->request('GET', $oidcConfiguration['jwks_uri']);
// we only need signature key
$keys = array_filter($response->toArray()['keys'], static fn (array $key) => 'sig' === $key['use']);

return json_encode(['keys' => $keys]);
})
);
} catch (\Throwable $e) {
$this->logger?->error('An error occurred while requesting OIDC certs.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}
if ($this->discoveryClients) {
$clients = $this->discoveryClients;
$logger = $this->logger;
$keys = $this->discoveryCache->get($this->oidcConfigurationCacheKey, static function () use ($clients, $logger): array {
try {
$configResponses = [];
foreach ($clients as $client) {
$configResponses[] = $client->request('GET', '.well-known/openid-configuration', [
'user_data' => $client,
]);
}

$jwkSetResponses = [];
foreach ($client->stream($configResponses) as $response => $chunk) {
if ($chunk->isLast()) {
$jwkSetResponses[] = $response->getInfo('user_data')->request('GET', $response->toArray()['jwks_uri']);
}
}

$keys = [];
foreach ($jwkSetResponses as $response) {
foreach ($response->toArray()['keys'] as $key) {
if ('sig' === $key['use']) {
$keys[] = $key;
}
}
}

return $keys;
} catch (\Exception $e) {
$logger?->error('An error occurred while requesting OIDC certs.', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
}
});

$jwkset = JWKSet::createFromKeyData(['keys' => $keys]);
}

try {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ CHANGELOG
* Allow subclassing `#[IsGranted]`
* Add `$tokenSource` argument to `#[IsCsrfTokenValid]` to support reading tokens from the query string or headers
* Deprecate `RememberMeDetails::getUserFqcn()`, the user FQCN will be removed from the remember-me cookie in 8.0
* Allow configuring multiple OIDC discovery base URIs

7.3
---
Expand Down
Loading