Skip to content

Commit e7644e7

Browse files
introduce TokenHandlerFactory
1 parent 4c9394f commit e7644e7

File tree

9 files changed

+244
-21
lines changed

9 files changed

+244
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\NodeDefinition;
15+
use Symfony\Component\DependencyInjection\ChildDefinition;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* Configure a token handler from a service id.
20+
*
21+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
22+
*/
23+
class IdTokenHandlerFactory implements TokenHandlerFactoryInterface
24+
{
25+
public function create(ContainerBuilder $container, string $id, array|string $config): void
26+
{
27+
$container->setDefinition($id, new ChildDefinition($config));
28+
}
29+
30+
public function getKey(): string
31+
{
32+
return 'id';
33+
}
34+
35+
public function addConfiguration(NodeDefinition $node): void
36+
{
37+
$node
38+
->children()
39+
->scalarNode($this->getKey())->end()
40+
->end()
41+
;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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\NodeDefinition;
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+
* Configure a token handler for an OIDC server.
22+
*
23+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
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->setArgument(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+
$container->register($clientDefinitionId, HttpClient::class)
36+
->setFactory([HttpClient::class, 'create'])
37+
->setArguments([$config['client']])
38+
->addTag('http_client.client')
39+
;
40+
$config['client'] = ['id' => $clientDefinitionId];
41+
}
42+
43+
$tokenHandlerDefinition->setArgument(0, new Reference($config['client']['id']));
44+
}
45+
46+
public function getKey(): string
47+
{
48+
return 'oidc_user_info';
49+
}
50+
51+
public function addConfiguration(NodeDefinition $node): void
52+
{
53+
$node
54+
->children()
55+
->arrayNode($this->getKey())
56+
->fixXmlConfig($this->getKey())
57+
->children()
58+
->scalarNode('claim')->default('sub')->end()
59+
->arrayNode('client')
60+
->isRequired()
61+
->beforeNormalization()
62+
->ifString()
63+
->then(static function ($v): array { return ['id' => $v]; })
64+
->end()
65+
->prototype('array')->end()
66+
->end()
67+
->end()
68+
->end()
69+
;
70+
}
71+
}
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\NodeDefinition;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* TokenHandlerFactoryInterface is the interface for all token handler factories.
19+
*
20+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
21+
*/
22+
interface TokenHandlerFactoryInterface
23+
{
24+
/**
25+
* Create a generic token handler service.
26+
*/
27+
public function create(ContainerBuilder $container, string $id, array|string $config): void;
28+
29+
/**
30+
* Get a generic token handler configuration key.
31+
*/
32+
public function getKey(): string;
33+
34+
/**
35+
* Add a generic token handler configuration.
36+
*/
37+
public function addConfiguration(NodeDefinition $node): void;
38+
}

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

+54-3
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
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 = [];
@@ -39,7 +44,6 @@ public function addConfiguration(NodeDefinition $node): void
3944
$builder = $node->children();
4045

4146
$builder
42-
->scalarNode('token_handler')->isRequired()->end()
4347
->scalarNode('user_provider')->defaultNull()->end()
4448
->scalarNode('realm')->defaultNull()->end()
4549
->scalarNode('success_handler')->defaultNull()->end()
@@ -57,6 +61,36 @@ public function addConfiguration(NodeDefinition $node): void
5761
->scalarPrototype()->end()
5862
->end()
5963
;
64+
65+
$tokenHandlerNodeBuilder = $builder
66+
->arrayNode('token_handler')
67+
->example([
68+
'id' => 'App\Security\CustomTokenHandler',
69+
])
70+
->requiresAtLeastOneElement()
71+
->useAttributeAsKey('name')
72+
->isRequired()
73+
->beforeNormalization()
74+
->ifString()
75+
->then(static function (string $v): array { return ['id' => $v]; })
76+
->end()
77+
->prototype('array')
78+
;
79+
80+
foreach ($this->tokenHandlerFactories as $factory) {
81+
$factory->addConfiguration($tokenHandlerNodeBuilder);
82+
}
83+
84+
$tokenHandlerNodeBuilder
85+
->beforeNormalization()
86+
->ifTrue(static function ($v) { return \is_array($v) && 1 < \count($v); })
87+
->then(static function () { throw new InvalidConfigurationException('You cannot configure multiple token handlers.'); })
88+
->end()
89+
->beforeNormalization()
90+
->ifTrue(static function ($v) { return \is_array($v) && 1 > \count($v); })
91+
->then(static function () { throw new InvalidConfigurationException('You must set a token handler.'); })
92+
->end()
93+
;
6094
}
6195

6296
public function getPriority(): int
@@ -76,10 +110,11 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
76110
$failureHandler = isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null;
77111
$authenticatorId = sprintf('security.authenticator.access_token.%s', $firewallName);
78112
$extractorId = $this->createExtractor($container, $firewallName, $config['token_extractors']);
113+
$tokenHandlerId = $this->createTokenHandler($container, $firewallName, $config['token_handler']);
79114

80115
$container
81116
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.access_token'))
82-
->replaceArgument(0, new Reference($config['token_handler']))
117+
->replaceArgument(0, new Reference($tokenHandlerId))
83118
->replaceArgument(1, new Reference($extractorId))
84119
->replaceArgument(2, $userProvider)
85120
->replaceArgument(3, $successHandler)
@@ -115,4 +150,20 @@ private function createExtractor(ContainerBuilder $container, string $firewallNa
115150

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

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

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
abstract_arg('access token extractors'),
4343
])
4444

45+
// OIDC
4546
->set('security.access_token_handler.oidc_user_info', OidcUserInfoTokenHandler::class)
47+
->abstract()
4648
;
4749
};

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

+21-2
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ public function testBasicServiceConfiguration()
3737
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
3838
}
3939

40-
public function testDefaultServiceConfiguration()
40+
/**
41+
* @dataProvider getGenericTokenHandlers
42+
*/
43+
public function testGenericTokenHandlerConfiguration(array|string $tokenHandler)
4144
{
4245
$container = new ContainerBuilder();
4346
$config = [
44-
'token_handler' => 'in_memory_token_handler_service_id',
47+
'token_handler' => $tokenHandler,
4548
];
4649

4750
$factory = new AccessTokenFactory();
@@ -52,6 +55,22 @@ public function testDefaultServiceConfiguration()
5255
$this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
5356
}
5457

58+
public function getGenericTokenHandlers(): iterable
59+
{
60+
yield ['in_memory_token_handler_service_id'];
61+
yield [['id' => 'in_memory_token_handler_service_id']];
62+
yield [['oidc_user_info' => ['client' => 'oidc.client']]];
63+
yield [[
64+
'oidc_user_info' => [
65+
'claim' => 'email',
66+
'client' => ['base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo'],
67+
],
68+
]];
69+
}
70+
71+
// TODO Test if multiple token handlers are configured
72+
// TODO Test if no token handler is configured
73+
5574
public function testNoExtractorsDefined()
5675
{
5776
$this->expectException(InvalidConfigurationException::class);

src/Symfony/Component/Security/Http/AccessToken/Oidc/Exception/InvalidOidcUserException.php renamed to src/Symfony/Component/Security/Http/AccessToken/Oidc/Exception/InvalidOidcUserInfoException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
* @author Vincent Chalamon <vincentchalamon@gmail.com>
2020
*/
21-
class InvalidOidcUserException extends AuthenticationException
21+
class InvalidOidcUserInfoException extends AuthenticationException
2222
{
2323
public function getMessageKey(): string
2424
{

src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcUserInfoTokenHandler.php

+5-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
1616
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
17-
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\InvalidOidcUserException;
17+
use Symfony\Component\Security\Http\AccessToken\Oidc\Exception\InvalidOidcUserInfoException;
1818
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
1919
use Symfony\Contracts\HttpClient\HttpClientInterface;
2020

@@ -27,9 +27,8 @@ final class OidcUserInfoTokenHandler implements AccessTokenHandlerInterface
2727
{
2828
public function __construct(
2929
private HttpClientInterface $client,
30-
private ?LoggerInterface $logger = null,
31-
private string $claim = 'sub',
32-
private string $userinfoUrl = 'protocol/openid-connect/userinfo'
30+
private ?LoggerInterface $logger = null,
31+
private string $claim = 'sub'
3332
) {
3433
}
3534

@@ -38,12 +37,12 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
3837
try {
3938
// Call the OIDC server to retrieve the user info
4039
// If the token is invalid or expired, the OIDC server will return an error
41-
$userinfo = $this->client->request('GET', $this->userinfoUrl, [
40+
$userinfo = $this->client->request('GET', '', [
4241
'auth_bearer' => $accessToken,
4342
])->toArray();
4443

4544
if (empty($userinfo[$this->claim])) {
46-
throw new InvalidOidcUserException(sprintf('"%s" property not found on OIDC server response.', $this->claim));
45+
throw new InvalidOidcUserInfoException(sprintf('"%s" claim not found on OIDC server response.', $this->claim));
4746
}
4847

4948
return new UserBadge($userinfo[$this->claim]);

0 commit comments

Comments
 (0)