Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 93f369a

Browse filesBrowse files
feat(security): OIDC discovery
1 parent c0c0a8f commit 93f369a
Copy full SHA for 93f369a

File tree

Expand file treeCollapse file tree

10 files changed

+271
-14
lines changed
Filter options
Expand file treeCollapse file tree

10 files changed

+271
-14
lines changed

‎UPGRADE-7.3.md

Copy file name to clipboardExpand all lines: UPGRADE-7.3.md
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Security
6060
* Add argument `$accessDecision` to `AccessDecisionManagerInterface::decide()` and `AuthorizationCheckerInterface::isGranted()`;
6161
it should be used to report the reason of a decision, including all the related votes.
6262

63+
* Add discovery support to `OidcTokenHandler` and `OidcUserInfoTokenHandler`
64+
6365
Console
6466
-------
6567

‎src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Deprecate the `security.hide_user_not_found` config option in favor of `security.expose_security_errors`
1111
* Add ability to fetch LDAP roles
1212
* Add `OAuth2TokenHandlerFactory` for `AccessTokenFactory`
13+
* Add discovery support to `OidcTokenHandler` and `OidcUserInfoTokenHandler`
1314

1415
7.2
1516
---

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcTokenHandlerFactory.php
+45-5Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Symfony\Component\DependencyInjection\ChildDefinition;
1818
use Symfony\Component\DependencyInjection\ContainerBuilder;
1919
use Symfony\Component\DependencyInjection\Exception\LogicException;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Contracts\HttpClient\HttpClientInterface;
2022

2123
/**
2224
* Configures a token handler for decoding and validating an OIDC token.
@@ -38,9 +40,29 @@ public function create(ContainerBuilder $container, string $id, array|string $co
3840
$tokenHandlerDefinition->replaceArgument(0, (new ChildDefinition('security.access_token_handler.oidc.signature'))
3941
->replaceArgument(0, $config['algorithms']));
4042

43+
if (isset($config['discovery'])) {
44+
if (!ContainerBuilder::willBeAvailable('symfony/http-client', HttpClientInterface::class, ['symfony/security-bundle'])) {
45+
throw new LogicException('You cannot use the "oidc" token handler with "discovery" since the HttpClient component is not installed. Try running "composer require symfony/http-client".');
46+
}
47+
48+
// disable JWKSet argument
49+
$tokenHandlerDefinition->replaceArgument(1, null);
50+
$tokenHandlerDefinition->addMethodCall(
51+
'enableDiscovery',
52+
[
53+
new Reference($config['discovery']['cache']['id']),
54+
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
55+
->replaceArgument(0, ['base_uri' => $config['discovery']['base_uri']]),
56+
"$id.oidc_configuration",
57+
"$id.oidc_jwk_set",
58+
]
59+
);
60+
61+
return;
62+
}
63+
4164
$tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwkset'))
42-
->replaceArgument(0, $config['keyset'])
43-
);
65+
->replaceArgument(0, $config['keyset']));
4466

4567
if ($config['encryption']['enabled']) {
4668
$algorithmManager = (new ChildDefinition('security.access_token_handler.oidc.encryption'))
@@ -74,8 +96,8 @@ public function addConfiguration(NodeBuilder $node): void
7496
->thenInvalid('You must set either "algorithm" or "algorithms".')
7597
->end()
7698
->validate()
77-
->ifTrue(static fn ($v) => !isset($v['key']) && !isset($v['keyset']))
78-
->thenInvalid('You must set either "key" or "keyset".')
99+
->ifTrue(static fn ($v) => !isset($v['discovery']) && !isset($v['key']) && !isset($v['keyset']))
100+
->thenInvalid('You must set either "discovery" or "key" or "keyset".')
79101
->end()
80102
->beforeNormalization()
81103
->ifTrue(static fn ($v) => isset($v['algorithm']) && \is_string($v['algorithm']))
@@ -101,6 +123,25 @@ public function addConfiguration(NodeBuilder $node): void
101123
})
102124
->end()
103125
->children()
126+
->arrayNode('discovery')
127+
->info('Enable the OIDC discovery.')
128+
->children()
129+
->scalarNode('base_uri')
130+
->info('Base URI of the OIDC server.')
131+
->isRequired()
132+
->cannotBeEmpty()
133+
->end()
134+
->arrayNode('cache')
135+
->children()
136+
->scalarNode('id')
137+
->info('Cache service id to use to cache the OIDC discovery configuration.')
138+
->isRequired()
139+
->cannotBeEmpty()
140+
->end()
141+
->end()
142+
->end()
143+
->end()
144+
->end()
104145
->scalarNode('claim')
105146
->info('Claim which contains the user identifier (e.g.: sub, email..).')
106147
->defaultValue('sub')
@@ -129,7 +170,6 @@ public function addConfiguration(NodeBuilder $node): void
129170
->end()
130171
->scalarNode('keyset')
131172
->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys).')
132-
->isRequired()
133173
->end()
134174
->arrayNode('encryption')
135175
->canBeEnabled()

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcUserInfoTokenHandlerFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/AccessToken/OidcUserInfoTokenHandlerFactory.php
+31-2Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Exception\LogicException;
1818
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Contracts\Cache\CacheInterface;
1920
use Symfony\Contracts\HttpClient\HttpClientInterface;
2021

2122
/**
@@ -34,9 +35,23 @@ public function create(ContainerBuilder $container, string $id, array|string $co
3435
throw new LogicException('You cannot use the "oidc_user_info" token handler since the HttpClient component is not installed. Try running "composer require symfony/http-client".');
3536
}
3637

37-
$container->setDefinition($id, new ChildDefinition('security.access_token_handler.oidc_user_info'))
38+
$tokenHandlerDefinition = $container->setDefinition($id, new ChildDefinition('security.access_token_handler.oidc_user_info'))
3839
->replaceArgument(0, $clientDefinition)
3940
->replaceArgument(2, $config['claim']);
41+
42+
if (isset($config['discovery'])) {
43+
if (!ContainerBuilder::willBeAvailable('symfony/cache', CacheInterface::class, ['symfony/security-bundle'])) {
44+
throw new LogicException('You cannot use the "oidc_user_info" token handler with "discovery" since the Cache component is not installed. Try running "composer require symfony/cache".');
45+
}
46+
47+
$tokenHandlerDefinition->addMethodCall(
48+
'enableDiscovery',
49+
[
50+
new Reference($config['discovery']['cache']['id']),
51+
"$id.oidc_configuration",
52+
]
53+
);
54+
}
4055
}
4156

4257
public function getKey(): string
@@ -55,10 +70,24 @@ public function addConfiguration(NodeBuilder $node): void
5570
->end()
5671
->children()
5772
->scalarNode('base_uri')
58-
->info('Base URI of the userinfo endpoint on the OIDC server.')
73+
->info('Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured).')
5974
->isRequired()
6075
->cannotBeEmpty()
6176
->end()
77+
->arrayNode('discovery')
78+
->info('Enable the OIDC discovery.')
79+
->children()
80+
->arrayNode('cache')
81+
->children()
82+
->scalarNode('id')
83+
->info('Cache service id to use to cache the OIDC discovery configuration.')
84+
->isRequired()
85+
->cannotBeEmpty()
86+
->end()
87+
->end()
88+
->end()
89+
->end()
90+
->end()
6291
->scalarNode('claim')
6392
->info('Claim which contains the user identifier (e.g. sub, email, etc.).')
6493
->defaultValue('sub')

‎src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_access_token.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@
9292
service('clock'),
9393
])
9494

95+
->set('security.access_token_handler.oidc_discovery.http_client', HttpClientInterface::class)
96+
->abstract()
97+
->factory([service('http_client'), 'withOptions'])
98+
->args([abstract_arg('http client options')])
99+
95100
->set('security.access_token_handler.oidc.jwk', JWK::class)
96101
->abstract()
97102
->deprecate('symfony/security-http', '7.1', 'The "%service_id%" service is deprecated. Please use "security.access_token_handler.oidc.jwkset" instead')

‎src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php
+95-1Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public function testInvalidOidcTokenHandlerConfigurationKeyMissing()
114114
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
115115

116116
$this->expectException(InvalidConfigurationException::class);
117-
$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 public keys).');
117+
$this->expectExceptionMessage('You must set either "discovery" or "key" or "keyset".');
118118

119119
$this->processConfig($config, $factory);
120120
}
@@ -340,6 +340,58 @@ public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithm()
340340
$this->processConfig($config, $factory);
341341
}
342342

343+
public function testOidcTokenHandlerConfigurationWithDiscovery()
344+
{
345+
$container = new ContainerBuilder();
346+
$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"}]}';
347+
$config = [
348+
'token_handler' => [
349+
'oidc' => [
350+
'discovery' => [
351+
'base_uri' => 'https://www.example.com/realms/demo/',
352+
'cache' => [
353+
'id' => 'oidc_cache',
354+
],
355+
],
356+
'algorithms' => ['RS256', 'ES256'],
357+
'issuers' => ['https://www.example.com'],
358+
'audience' => 'audience',
359+
],
360+
],
361+
];
362+
363+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
364+
$finalizedConfig = $this->processConfig($config, $factory);
365+
366+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
367+
368+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
369+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
370+
371+
$expectedArgs = [
372+
'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature'))
373+
->replaceArgument(0, ['RS256', 'ES256']),
374+
'index_1' => null,
375+
'index_2' => 'audience',
376+
'index_3' => ['https://www.example.com'],
377+
'index_4' => 'sub',
378+
];
379+
$expectedCalls = [
380+
[
381+
'enableDiscovery',
382+
[
383+
new Reference('oidc_cache'),
384+
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
385+
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/']),
386+
'security.access_token_handler.firewall1.oidc_configuration',
387+
'security.access_token_handler.firewall1.oidc_jwk_set',
388+
],
389+
],
390+
];
391+
$this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
392+
$this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
393+
}
394+
343395
public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
344396
{
345397
$container = new ContainerBuilder();
@@ -407,6 +459,48 @@ public static function getOidcUserInfoConfiguration(): iterable
407459
yield ['https://www.example.com/realms/demo/protocol/openid-connect/userinfo'];
408460
}
409461

462+
public function testOidcUserInfoTokenHandlerConfigurationWithDiscovery()
463+
{
464+
$container = new ContainerBuilder();
465+
$config = [
466+
'token_handler' => [
467+
'oidc_user_info' => [
468+
'discovery' => [
469+
'cache' => [
470+
'id' => 'oidc_cache',
471+
],
472+
],
473+
'base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
474+
],
475+
],
476+
];
477+
478+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
479+
$finalizedConfig = $this->processConfig($config, $factory);
480+
481+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
482+
483+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
484+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
485+
486+
$expectedArgs = [
487+
'index_0' => (new ChildDefinition('security.access_token_handler.oidc_user_info.http_client'))
488+
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo']),
489+
'index_2' => 'sub',
490+
];
491+
$expectedCalls = [
492+
[
493+
'enableDiscovery',
494+
[
495+
new Reference('oidc_cache'),
496+
'security.access_token_handler.firewall1.oidc_configuration',
497+
],
498+
],
499+
];
500+
$this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
501+
$this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
502+
}
503+
410504
public function testMultipleTokenHandlersSet()
411505
{
412506
$config = [

‎src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AccessToken/config_oidc.yml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ security:
2424
claim: 'username'
2525
audience: 'Symfony OIDC'
2626
issuers: [ 'https://www.example.com' ]
27-
algorithm: 'ES256'
27+
algorithms: [ 'ES256' ]
2828
# tip: use https://mkjwk.org/ to generate a JWK
2929
keyset: '{"keys":[{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}]}'
3030
encryption:

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.