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 1c79c79

Browse filesBrowse files
committed
feature #48272 [Security] Add OidcUserInfoTokenHandler and OidcUser (vincentchalamon)
This PR was squashed before being merged into the 6.3 branch. Discussion ---------- [Security] Add OidcUserInfoTokenHandler and OidcUser | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | symfony/symfony-docs#17463 Hi, This PR aims to complete [the previous one](#46428) from `@Spomky` with an AccessTokenHandler ready-to-use with an OIDC server (Keycloak, Auth0). ## TODO - [x] Rebase from 6.3 - [x] Rebase from #48285 - [x] Rebase from #48594 - [x] Write doc (symfony/symfony-docs#17463) - [x] Add TokenHandlerFactory - [x] Add ServiceTokenHandlerFactory for BC layer - [x] Add OidcUserInfoTokenHandlerFactory - [x] Add OidcTokenHandlerFactory (using web-token/jwt-*) - [x] Implement OidcUser to keep user claims from OIDC server - [x] Update doc PR about claims usage in a custom UserProvider - [x] ~Update doc PR about OidcUserProvider usage~ (abandonned) ## Usage ```yaml # usage with a custom client security: firewalls: main: pattern: ^/ access_token: token_handler: oidc_user_info: client: oidc.client ``` ```yaml # usage with generic HttpClient security: firewalls: main: pattern: ^/ access_token: token_handler: oidc_user_info: claim: email client: base_uri: https://www.example.com/realms/demo/protocol/openid-connect/userinfo ``` ```yaml # usage with token decode (no call to OIDC server) security: firewalls: main: pattern: ^/ access_token: token_handler: oidc: signature: # Algorithm used to sign the JWS algorithm: 'HS256' # A JSON-encoded JWK key: '{"kty":"...","k":"..."}' ``` ```php # usage with a custom UserProvider class CustomUserProvider implements UserProviderInterface { public function loadUserByIdentifier(string $identifier, array $claims = []): UserInterface { // do some magic } } ``` Commits ------- 99a35f0 [Security] Add OidcUserInfoTokenHandler and OidcUser
2 parents 9d8a9b6 + 99a35f0 commit 1c79c79
Copy full SHA for 1c79c79

31 files changed

+1534
-17
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+4-1Lines 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

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
@@ -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
---
+89Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AccessTokenFactory.php
+56-3Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.