8000 [Security] Add OidcUserInfoTokenHandler and OidcUser · symfony/symfony@99a35f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 99a35f0

Browse files
vincentchalamonfabpot
authored andcommitted
[Security] Add OidcUserInfoTokenHandler and OidcUser
1 parent 1f4a1bc commit 99a35f0

31 files changed

+1534
-17
lines changed

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,12 @@
150150
"symfony/phpunit-bridge": "^5.4|^6.0",
151151
"symfony/runtime": "self.version",
152152
"symfony/security-acl": "~2.8|~3.0",
153+
"symfony/string": "^5.4|^6.0",
153154
"twig/cssinliner-extra": "^2.12|^3",
154155
"twig/inky-extra": "^2.12|^3",
155-
"twig/markdown-extra": "^2.12|^3"
156+
"twig/markdown-extra": "^2.12|^3",
157+
"web-token/jwt-checker": "^3.1",
158+
"web-token/jwt-signature-algorithm-ecdsa": "^3.1"
156159
},
157160
"conflict": {
158161
"ext-psr": "<1.1|>=2",

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Make `Security::login()` return the authenticator response
1212
* Deprecate the `security.firewalls.logout.csrf_token_generator` config option, use `security.firewalls.logout.csrf_token_manager` instead
1313
* Make firewalls event dispatcher traceable on debug mode
14+
* Add `TokenHandlerFactoryInterface`, `OidcUserInfoTokenHandlerFactory`, `OidcTokenHandlerFactory` and `ServiceTokenHandlerFactory` for `AccessTokenFactory`
1415

1516
6.2
1617
---
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
use Jose\Component\Core\Algorithm;
15+
use Jose\Component\Core\JWK;
16+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SignatureAlgorithmFactory;
17+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
18+
use Symfony\Component\DependencyInjection\ChildDefinition;
19+
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
/**
23+
* Configures a token handler for decoding and validating an OIDC token.
24+
*
25+
* @experimental
26+
*/
27+
class OidcTokenHandlerFactory implements TokenHandlerFactoryInterface
28+
{
29+
public function create(ContainerBuilder $container, string $id, array|string $config): void
30+
{
31+
$tokenHandlerDefinition = $container->setDefinition($id, new ChildDefinition('security.access_token_handler.oidc'));
32+
$tokenHandlerDefinition->replaceArgument(3, $config['claim']);
33+
$tokenHandlerDefinition->replaceArgument(4, $config['audience']);
34+
35+
// Create the signature algorithm and the JWK
36+
if (!ContainerBuilder::willBeAvailable('web-token/jwt-core', Algorithm::class, ['symfony/security-bundle'])) {
37+
$container->register('security.access_token_handler.oidc.signature', 'stdClass')
38+
->addError('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "web-token/jwt-core".');
39+
$container->register('security.access_token_handler.oidc.jwk', 'stdClass')
40+
->addError('You cannot use the "oidc" token handler since "web-token/jwt-core" is not installed. Try running "web-token/jwt-core".');
41+
} else {
42+
$container->register('security.access_token_handler.oidc.signature', Algorithm::class)
43+
->setFactory([SignatureAlgorithmFactory::class, 'create'])
44+
->setArguments([$config['signature']['algorithm']]);
45+
$container->register('security.access_token_handler.oidc.jwk', JWK::class)
46+
->setFactory([JWK::class, 'createFromJson'])
47+
->setArguments([$config['signature']['key']]);
48+
}
49+
$tokenHandlerDefinition->replaceArgument(0, new Reference('security.access_token_handler.oidc.signature'));
50+
$tokenHandlerDefinition->replaceArgument(1, new Reference('security.access_token_handler.oidc.jwk'));
51+
}
52+
53+
public function getKey(): string
54+
{
55+
return 'oidc';
56+
}
57+
58+
public function addConfiguration(NodeBuilder $node): void
59+
{
60+
$node
61+
->arrayNode($this->getKey())
62+
->fixXmlConfig($this->getKey())
63+
->children()
64+
->scalarNode('claim')
65+
->info('Claim which contains the user identifier (e.g.: sub, email..).')
66+
->defaultValue('sub')
67+
->end()
68+
->scalarNode('audience')
69+
->info('Audience set in the token, for validation purpose.')
70+
->defaultNull()
71+
->end()
72+
->arrayNode('signature')
73+
->isRequired()
74+
->children()
75+
->scalarNode('algorithm')
76+
->info('Algorithm used to sign the token.')
77+
->isRequired()
78+
->end()
79+
->scalarNode('key')
80+
->info('JSON-encoded JWK used to sign the token (must contain a "kty" key).')
81+
->isRequired()
82+
->end()
83+
->end()
84+
->end()
85+
->end()
86+
->end()
87+
;
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\HttpClient\HttpClient;
19+
20+
/**
21+
* Configures a token handler for an OIDC server.
22+
*
23+
* @experimental
24+
*/
25+
class OidcUserInfoTokenHandlerFactory implements TokenHandlerFactoryInterface
26+
{
27+
public function create(ContainerBuilder $container, string $id, array|string $config): void
28+
{
29+
$tokenHandlerDefinition = $container->setDefinition($id, new ChildDefinition('security.access_token_handler.oidc_user_info'));
30+
$tokenHandlerDefinition->replaceArgument(2, $config['claim']);
31+
32+
// Create the client service
33+
if (!isset($config['client']['id'])) {
34+
$clientDefinitionId = 'http_client.security.access_token_handler.oidc_user_info';
35+
if (!ContainerBuilder::willBeAvailable('symfony/http-client', HttpClient::class, ['symfony/security-bundle'])) {
36+
$container->register($clientDefinitionId, 'stdClass')
37+
->addError('You cannot use the "oidc_user_info" token handler since the HttpClient component is not installed. Try running "composer require symfony/http-client".');
38+
} else {
39+
$container->register($clientDefinitionId, HttpClient::class)
40+
->setFactory([HttpClient::class, 'create'])
41+
->setArguments([$config['client']])
42+
->addTag('http_client.client');
43+
}
44+
}
45+
46+
$tokenHandlerDefinition->replaceArgument(0, new Reference($config['client']['id'] ?? $clientDefinitionId));
47+
}
48+
49+
public function getKey(): string
50+
{
51+
return 'oidc_user_info';
52+
}
53+
54+
public function addConfiguration(NodeBuilder $node): void
55+
{
56+
$node
57+
->arrayNode($this->getKey())
58+
->fixXmlConfig($this->getKey())
59+
->children()
60+
->scalarNode('claim')
61+
->info('Claim which contains the user identifier (e.g.: sub, email..).')
62+
->defaultValue('sub')
63+
->end()
64+
->arrayNode('client')
65+
->info('HttpClient to call the OIDC server.')
66+
->isRequired()
67+
->beforeNormalization()
68+
->ifString()
69+
->then(static function ($v): array { return ['id' => $v]; })
70+
->end()
71+
->prototype('scalar')->end()
72+
->end()
73+
->end()
74+
->end()
75+
;
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* Configures a token handler from a service id.
20+
*
21+
* @see \Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory\AccessTokenFactoryTest
22+
*
23+
* @experimental
24+
*/
25+
class ServiceTokenHandlerFactory implements TokenHandlerFactoryInterface
26+
{
27+
public function create(ContainerBuilder $container, string $id, array|string $config): void
28+
{
29+
$container->setDefinition($id, new ChildDefinition($config));
30+
}
31+
32+
public function getKey(): string
33+
{
34+
return 'id';
35+
}
36+
37+
public function addConfiguration(NodeBuilder $node): void
38+
{
39+
$node->scalarNode($this->getKey())->end();
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken;
13+
14+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* Allows creating configurable token handlers.
19+
*
20+
* @experimental
21+
*/
22+
interface TokenHandlerFactoryInterface
23+
{
24+
/**
25+
* Creates a generic token handler service.
26+
*/
27+
public function create(ContainerBuilder $container, string $id, array|string $config): void;
28+
29+
/**
30+
* Gets a generic token handler configuration key.
31+
*/
32+
public function getKey(): string;
33+
34+
/**
35+
* Adds a generic token handler configuration.
36+
*/
37+
public function addConfiguration(NodeBuilder $node): void;
38+
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

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

14+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\TokenHandlerFactoryInterface;
1415
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
16+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1517
use Symfony\Component\DependencyInjection\ChildDefinition;
1618
use Symfony\Component\DependencyInjection\ContainerBuilder;
1719
use Symfony\Component\DependencyInjection\Reference;
@@ -27,7 +29,10 @@ final class AccessTokenFactory extends AbstractFactory implements StatelessAuthe
2729
{
2830
private const PRIORITY = -40;
2931

30-
public function __construct()
32+
/**
33+
* @param array<array-key, TokenHandlerFactoryInterface> $tokenHandlerFactories
34+
*/
35+
public function __construct(private readonly array $tokenHandlerFactories)
3136
{
3237
$this->options = [];
3338
$this->defaultFailureHandlerOptions = [];
@@ -40,7 +45,6 @@ public function addConfiguration(NodeDefinition $node): void
4045

4146
$builder = $node->children();
4247
$builder
43-
->scalarNode('token_handler')->isRequired()->end()
4448
->scalarNode('realm')->defaultNull()->end()
4549
->arrayNode('token_extractors')
4650
->fixXmlConfig('token_extractors')
@@ -55,6 +59,38 @@ public function addConfiguration(NodeDefinition $node): void
5559
->scalarPrototype()->end()
5660
->end()
5761
;
62+
63+
$tokenHandlerNodeBuilder = $builder
64+
->arrayNode('token_handler')
65+
->example([
66+
'id' => 'App\Security\CustomTokenHandler',
67+
])
68+
69+
->beforeNormalization()
70+
->ifString()
71+
->then(static function (string $v): array { return ['id' => $v]; })
72+
->end()
73+
74+
->beforeNormalization()
75+
->ifTrue(static function ($v) { return \is_array($v) && 1 < \count($v); })
76+
->then(static function () { throw new InvalidConfigurationException('You cannot configure multiple token handlers.'); })
77+
->end()
78+
79+
// "isRequired" must be set otherwise the following custom validation is not called
80+
->isRequired()
81+
->beforeNormalization()
82+
->ifTrue(static function ($v) { return \is_array($v) && !$v; })
83+
->then(static function () { throw new InvalidConfigurationException('You must set a token handler.'); })
84+
->end()
85+
86+
->children()
87+
;
88+
89+
foreach ($this->tokenHandlerFactories as $factory) {
90+
$factory->addConfiguration($tokenHandlerNodeBuilder);
91+
}
92+
93+
$tokenHandlerNodeBuilder->end();
5894
}
5995

6096
public function getPriority(): int
@@ -73,10 +109,11 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
73109
$failureHandler = isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null;
74110
$authenticatorId = sprintf('security.authenticator.access_token.%s', $firewallName);
75111
$extractorId = $this->createExtractor($container, $firewallName, $config['token_extractors']);
112+
$tokenHandlerId = $this->createTokenHandler($container, $firewallName, $config['token_handler'], $userProviderId);
76113

77114
$container
78115
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token'))
79-
->replaceArgument(0, new Reference($config['token_handler']))
116+
->replaceArgument(0, new Reference($tokenHandlerId))
80117
->replaceArgument(1, new Reference($extractorId))
81118
->replaceArgument(2, $userProviderId ? new Reference($userProviderId) : null)
82119
->replaceArgument(3, $successHandler)
@@ -110,4 +147,20 @@ private function createExtractor(ContainerBuilder $container, string $firewallNa
110147

111148
return $extractorId;
112149
}
150+
151+
private function createTokenHandler(ContainerBuilder $container, string $firewallName, array $config, ?string $userProviderId): string
152+
{
153+
$key = array_keys($config)[0];
154+
$id = sprintf('security.access_token_handler.%s', $firewallName);
155+
156+
foreach ($this->tokenHandlerFactories as $factory) {
157+
if ($key !== $factory->getKey()) {
158+
continue;
159+
}
160+
161+
$factory->create($container, $id, $config[$key], $userProviderId);
162+
}
163+
164+
return $id;
165+
}
113166
}

0 commit comments

Comments
 (0)
0