8000 [Security] OAuth2 Introspection Endpoint (RFC7662) · symfony/symfony@04c53b4 · GitHub
[go: up one dir, main page]

Skip to content

Commit 04c53b4

Browse files
committed
[Security] OAuth2 Introspection Endpoint (RFC7662)
In addition to the excellent work of @vincentchalamon #48272, this PR allows getting the data from the OAuth2 Introspection Endpoint. This endpoint is defined in the [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662). It returns the following information that is used to retrieve the user: * If the access token is active * A set of claims that are similar to the OIDC one, including the `sub` or the `username`.
1 parent 36a920e commit 04c53b4

File tree

12 files changed

+481
-64
lines changed

12 files changed

+481
-64
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class UnusedTagsPass implements CompilerPassInterface
8585
'routing.route_loader',
8686
'scheduler.schedule_provider',
8787
'scheduler.task',
88+
'security.access_token_handler.oidc.encryption_algorithm',
8889
'security.access_token_handler.oidc.signature_algorithm',
8990
'security.authenticator.login_linker',
9091
'security.expression_language_provider',

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `Security::isGrantedForUser()` to test user authorization without relying on the session. For example, users not currently logged in, or while processing a message from a message queue
8+
* Add encryption support to `OidcTokenHandler` (JWE)
89

910
7.2
1011
---

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ public function create(ContainerBuilder $container, string $id, array|string $co
4141
$tokenHandlerDefinition->replaceArgument(1, (new ChildDefinition('security.access_token_handler.oidc.jwkset'))
4242
->replaceArgument(0, $config['keyset'])
4343
);
44+
45+
if ($config['encryption']['enabled']) {
46+
$algorithmManager = (new ChildDefinition('security.access_token_handler.oidc.encryption'))
47+
->replaceArgument(0, $config['encryption']['algorithms']);
48+
$keyset = (new ChildDefinition('security.access_token_handler.oidc.jwkset'))
49+
->replaceArgument(0, $config['encryption']['keyset']);
50+
51+
$tokenHandlerDefinition->addMethodCall(
52+
'enabledJweSupport',
53+
[
54+
$keyset,
55+
$algorithmManager,
56+
$config['encryption']['enforce'],
57+
]
58+
);
59+
}
4460
}
4561

4662
public function getKey(): string
@@ -112,9 +128,28 @@ public function addConfiguration(NodeBuilder $node): void
112128
->setDeprecated('symfony/security-bundle', '7.1', 'The "%node%" option is deprecated and will be removed in 8.0. Use the "keyset" option instead.')
113129
->end()
114130
->scalarNode('keyset')
115-
->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).')
131+
->info('JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys).')
116132
->isRequired()
117133
->end()
134+
->arrayNode('encryption')
135+
->canBeEnabled()
136+
->children()
137+
->booleanNode('enforce')
138+
->info('When enabled, the token shall be encrypted.')
139+
->defaultFalse()
140+
->end()
141+
->arrayNode('algorithms')
142+
->info('Algorithms used to decrypt the token.')
143+
->isRequired()
144+
->requiresAtLeastOneElement()
145+
->scalarPrototype()->end()
146+
->end()
147+
->scalarNode('keyset')
148+
->info('JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).')
149+
->isRequired()
150+
->end()
151+
->end()
152+
->end()
118153
->end()
119154
->end()
120155
;

src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,29 @@
338338
<xsd:choice maxOccurs="unbounded">
339339
<xsd:element name="issuers" type="oidc_issuers" minOccurs="0" maxOccurs="1" />
340340
<xsd:element name="issuer" type="password_hasher" minOccurs="0" maxOccurs="unbounded" />
341+
<xsd:element name="encryption" type="oidc_encryption" />
341342
</xsd:choice>
342343
<xsd:attribute name="claim" type="xsd:string" />
343344
<xsd:attribute name="audience" type="xsd:string" use="required" />
344345
<xsd:attribute name="algorithm" type="xsd:string" use="required" />
345346
<xsd:attribute name="key" type="xsd:string" use="required" />
346347
</xsd:complexType>
347348

349+
<xsd:complexType name="oidc_encryption">
350+
<xsd:choice maxOccurs="unbounded">
351+
<xsd:element name="algorithms" type="oidc_encryption_algorithms" minOccurs="1" maxOccurs="1" />
352+
</xsd:choice>
353+
<xsd:attribute name="enabled" type="xsd:boolean" />
354+
<xsd:attribute name="enforce" type="xsd:boolean" />
355+
<xsd:attribute name="keyset" type="xsd:string" use="required" />
356+
</xsd:complexType>
357+
358+
<xsd:complexType name="oidc_encryption_algorithms">
359+
<xsd:sequence>
360+
<xsd:element name="algorithm" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
361+
</xsd:sequence>
362+
</xsd:complexType>
363+
348364
<xsd:complexType name="oidc_issuers">
349365
<xsd:sequence>
350366
<xsd:element name="issuer" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
use Jose\Component\Core\AlgorithmManagerFactory;
1616
use Jose\Component\Core\JWK;
1717
use Jose\Component\Core\JWKSet;
18+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
19+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM;
20+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A192CBCHS384;
21+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A192GCM;
22+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
23+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
24+
use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES;
25+
use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHSS;
26+
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
1827
use Jose\Component\Signature\Algorithm\ES256;
1928
use Jose\Component\Signature\Algorithm\ES384;
2029
use Jose\Component\Signature\Algorithm\ES512;
@@ -135,5 +144,47 @@
135144

136145
->set('security.access_token_handler.oidc.signature.PS512', PS512::class)
137146
->tag('security.access_token_handler.oidc.signature_algorithm')
147+
148+
// Encryption
149+
// Note that - all xxxKW algorithms are not defined as an extra dependency is required
150+
// - The RSA_1.5 is missing as deprecated
151+
->set('security.access_token_handler.oidc.encryption_algorithm_manager_factory', AlgorithmManagerFactory::class)
152+
->args([
153+
tagged_iterator('security.access_token_handler.oidc.encryption_algorithm'),
154+
])
155+
156+
->set('security.access_token_handler.oidc.encryption', AlgorithmManager::class)
157+
->abstract()
158+
->factory([service('security.access_token_handler.oidc.encryption_algorithm_manager_factory'), 'create'])
159+
->args([
160+
abstract_arg('encryption algorithms'),
161+
])
162+
163+
->set('security.access_token_handler.oidc.encryption.RSAOAEP', RSAOAEP::class)
164+
->tag('security.access_token_handler.oidc.encryption_algorithm')
165+
166+
->set('security.access_token_handler.oidc.encryption.ECDHES', ECDHES::class)
167+
->tag('security.access_token_handler.oidc.encryption_algorithm')
168+
169+
->set('security.access_token_handler.oidc.encryption.ECDHSS', ECDHSS::class)
170+
->tag('security.access_token_handler.oidc.encryption_algorithm')
171+
172+
->set('security.access_token_handler.oidc.encryption.A128CBCHS256', A128CBCHS256::class)
173+
->tag('security.access_token_handler.oidc.encryption_algorithm')
174+
175+
->set('security.access_token_handler.oidc.encryption.A192CBCHS384', A192CBCHS384::class)
176+
->tag('security.access_token_handler.oidc.encryption_algorithm')
177+
178+
->set('security.access_token_handler.oidc.encryption.A256CBCHS512', A256CBCHS512::class)
179+
->tag('security.access_token_handler.oidc.encryption_algorithm')
180+
181+
->set('security.access_token_handler.oidc.encryption.A128GCM', A128GCM::class)
182+
->tag('security.access_token_handler.oidc.encryption_algorithm')
183+
184+
->set('security.access_token_handler.oidc.encryption.A192GCM', A192GCM::class)
185+
->tag('security.access_token_handler.oidc.encryption_algorithm')
186+
187+
->set('security.access_token_handler.oidc.encryption.A256GCM', A256GCM::class)
188+
->tag('security.access_token_handler.oidc.encryption_algorithm')
138189
;
139190
};

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

Lines changed: 83 additions & 1 deletion
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('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).');
117117

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

260+
public function testOidcTokenHandlerConfigurationWithEncryption()
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+
'algorithms' => ['RS256', 'ES256'],
268+
'issuers' => ['https://www.example.com'],
269+
'audience' => 'audience',
270+
'keyset' => $jwkset,
271+
'encryption' => [
272+
'enabled' => true,
273+
'keyset' => $jwkset,
274+
'algorithms' => ['RSA-OAEP', 'RSA1_5'],
275+
],
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+
289+
public function testInvalidOidcTokenHandlerConfigurationMissingEncryptionKeyset()
290+
{
291+
$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"}]}';
292+
$config = [
293+
'token_handler' => [
294+
'oidc' => [
295+
'algorithms' => ['RS256', 'ES256'],
296+
'issuers' => ['https://www.example.com'],
297+
'audience' => 'audience',
298+
'keyset' => $jwkset,
299+
'encryption' => [
300+
'enabled' => true,
301+
'algorithms' => ['RSA-OAEP', 'RSA1_5'],
302+
],
303+
],
304+
],
305+
];
306+
307+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
308+
309+
$this->expectException(InvalidConfigurationException::class);
310+
$this->expectExceptionMessage('The child config "keyset" under "access_token.token_handler.oidc.encryption" must be configured: JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).');
311+
312+
$this->processConfig($config, $factory);
313+
}
314+
315+
public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithm()
316+
{
317+
$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"}]}';
318+
$config = [
319+
'token_handler' => [
320+
'oidc' => [
321+
'algorithms' => ['RS256', 'ES256'],
322+
'issuers' => ['https://www.example.com'],
323+
'audience' => 'audience',
324+
'keyset' => $jwkset,
325+
'encryption' => [
326+
'enabled' => true,
327+
'keyset' => $jwkset,
328+
'algorithms' => [],
329+
],
330+
],
331+
],
332+
];
333+
334+
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
335+
336+
$this->expectException(InvalidConfigurationException::class);
337+
$this->expectExceptionMessage('The path "access_token.token_handler.oidc.encryption.algorithms" should have at least 1 element(s) defined.');
338+
339+
$this->processConfig($config, $factory);
340+
}
341+
260342
public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
261343
{
262344
$container = new ContainerBuilder();

0 commit comments

Comments
 (0)
0