8000 feat(security): OIDC discovery · symfony/symfony@afc9b6e · GitHub
[go: up one dir, main page]

Skip to content

Commit afc9b6e

Browse files
feat(security): OIDC discovery
1 parent a980a46 commit afc9b6e

File tree

10 files changed

+277
-13
lines changed

10 files changed

+277
-13
lines changed

UPGRADE-7.2.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Security
1818

1919
* Add `$token` argument to `UserCheckerInterface::checkPostAuth()`
2020
* Deprecate argument `$secret` of `RememberMeToken` and `RememberMeAuthenticator`
21+
* Add discovery support to `OidcTokenHandler` and `OidcUserInfoTokenHandler`
2122

2223
String
2324
------

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Allow configuring the secret used to sign login links
8+
* Add discovery support to `OidcTokenHandler` and `OidcUserInfoTokenHandler`
89

910
7.1
1011
---

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

+45-5
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

4668
public function getKey(): string
@@ -58,8 +80,8 @@ public function addConfiguration(NodeBuilder $node): void
5880
->thenInvalid('You must set either "algorithm" or "algorithms".')
5981
->end()
6082
->validate()
61-
->ifTrue(static fn ($v) => !isset($v['key']) && !isset($v['keyset']))
62-
->thenInvalid('You must set either "key" or "keyset".')
83+
->ifTrue(static fn ($v) => !isset($v['discovery']) && !isset($v['key']) && !isset($v['keyset']))
84+
->thenInvalid('You must set either "discovery" or "key" or "keyset".')
6385
->end()
6486
->beforeNormalization()
6587
->ifTrue(static fn ($v) => isset($v['algorithm']) && \is_string($v['algorithm']))
@@ -85,6 +107,25 @@ public function addConfiguration(NodeBuilder $node): void
85107
})
86108
->end()
87109
->children()
110+
->arrayNode('discovery')
111+
->info('Enable the OIDC discovery.')
112+
->children()
113+
->scalarNode('base_uri')
114+
->info('Base URI of the OIDC server.')
115+
->isRequired()
116+
->cannotBeEmpty()
117+
->end()
118+
->arrayNode('cache')
119+
->children()
120+
->scalarNode('id')
121+
->info('Cache service id to use to cache the OIDC discovery configuration.')
122+
->isRequired()
123+
->cannotBeEmpty()
124+
->end()
125+
->end()
126+
->end()
127+
->end()
128+
->end()
88129
->scalarNode('claim')
89130
->info('Claim which contains the user identifier (e.g.: sub, email..).')
90131
->defaultValue('sub')
@@ -113,7 +154,6 @@ public function addConfiguration(NodeBuilder $node): void
113154
->end()
114155
->scalarNode('keyset')
115156
->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).')
116-
->isRequired()
117157
->end()
118158
->end()
119159
->end()

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

+31-2
93C6
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

+5
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@
8282
service('clock'),
8383
])
8484

85+
->set('security.access_token_handler.oidc_discovery.http_client', HttpClientInterface::class)
86+
->abstract()
87+
->factory([service('http_client'), 'withOptions'])
88+
->args([abstract_arg('http client options')])
89+
8590
->set('security.access_token_handler.oidc.jwk', JWK::class)
8691
->abstract()
8792
->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

+95-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public function testInvalidOidcTokenHandlerConfigurationKeyMissing()
113113
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
114114

115115
$this->expectException(InvalidConfigurationException::class);
116-
$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).');
116+
$this->expectExceptionMessage('You must set either "discovery" or "key" or "keyset".');
117117

118118
$this->processConfig($config, $factory);
119119
}
@@ -257,6 +257,58 @@ public function testOidcTokenHandlerConfigurationWithMultipleAlgorithms()
257257
$this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
258258
}
259259

260+
public function testOidcTokenHandlerConfigurationWithDiscovery()
261+
{
262+
$container = new ContainerBuilder();
263+
$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"}]}';
264+
$config = [
265+
'token_handler' => [
266+
'oidc' => [
267+
'discovery' => [
268+
'base_uri' => 'https://www.example.com/realms/demo/',
269+
'cache' => [
270+
'id' => 'oidc_cache',
271+
],
272+
],
273+
'algorithms' => ['RS256', 'ES256'],
274+
'issuers' => ['https://www.example.com'],
275+
'audience' => 'audience',
276+
],
277+
],
278+
];
279+
280+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
281+
$finalizedConfig = $this->processConfig($config, $factory);
282+
283+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
284+
285+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
286+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
287+
288+
$expectedArgs = [
289+
'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature'))
290+
->replaceArgument(0, ['RS256', 'ES256']),
291+
'index_1' => null,
292+
'index_2' => 'audience',
293+
'index_3' => ['https://www.example.com'],
294+
'index_4' => 'sub',
295+
];
296+
$expectedCalls = [
297+
[
298+
'enableDiscovery',
299+
[
300+
new Reference('oidc_cache'),
301+
(new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
302+
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/']),
303+
'security.access_token_handler.firewall1.oidc_configuration',
304+
'security.access_token_handler.firewall1.oidc_jwk_set',
305+
]
306+
],
307+
];
308+
$this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
309+
$this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
310+
}
311+
260312
public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
261313
{
262314
$container = new ContainerBuilder();
@@ -324,6 +376,48 @@ public static function getOidcUserInfoConfiguration(): iterable
324376
yield ['https://www.example.com/realms/demo/protocol/openid-connect/userinfo'];
325377
}
326378

379+
public function testOidcUserInfoTokenHandlerConfigurationWithDiscovery()
380+
{
381+
$container = new ContainerBuilder();
382+
$config = [
383+
'token_handler' => [
384+
'oidc_user_info' => [
385+
'discovery' => [
386+
'cache' => [
387+
'id' => 'oidc_cache',
388+
],
389+
],
390+
'base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
391+
],
392+
],
393+
];
394+
395+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
396+
$finalizedConfig = $this->processConfig($config, $factory);
397+
398+
$factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
399+
400+
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
401+
$this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
402+
403+
$expectedArgs = [
404+
'index_0' => (new ChildDefinition('security.access_token_handler.oidc_user_info.http_client'))
405+
->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo']),
406+
'index_2' => 'sub',
407+
];
408+
$expectedCalls = [
409+
[
410+
'enableDiscovery',
411+
[
412+
new Reference('oidc_cache'),
413+
'security.access_token_handler.firewall1.oidc_configuration',
414+
]
415+
],
416+
];
417+
$this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
418+
$this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
419+
}
420+
327421
public function testMultipleTokenHandlersSet()
328422
{
329423
$config = [

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

+1-1
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
token_extractors: 'header'

0 commit comments

Comments
 (0)
0