10000 Add encryption support to OIDC tokens · symfony/symfony@9093bbc · GitHub
[go: up one dir, main page]

Skip to content

Commit 9093bbc

Browse files
committed
Add encryption support to OIDC tokens
The changes add encryption support to OpenID Connect (OIDC) tokens in the Symfony Security Bundle. This is useful in making the application more secure. They also ensure the tokens are correctly decrypted and validated before use. Additionally, tests have been expanded to cover these new scenarios.
1 parent 1a16ebc commit 9093bbc

File tree

5 files changed

+303
-64
lines changed

5 files changed

+303
-64
lines changed

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

Lines changed: 35 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'] === true) {
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,27 @@ 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+
->scalarPrototype()->end()
145+
->end()
146+
->scalarNode('keyset')
147+
->info('JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).')
148+
->isRequired()
149+
->end()
150+
->end()
151+
->end()
118152
->end()
119153
->end()
120154
;

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

Lines changed: 53 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,49 @@
135144

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

src/Symfony/Bundle/SecurityBundle/Tests/Functional/AccessTokenTest.php

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313

1414
use Jose\Component\Core\AlgorithmManager;
1515
use Jose\Component\Core\JWK;
16+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM;
17+
use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES;
18+
use Jose\Component\Encryption\JWEBuilder;
1619
use Jose\Component\Signature\Algorithm\ES256;
1720
use Jose\Component\Signature\JWSBuilder;
18-
use Jose\Component\Signature\Serializer\CompactSerializer;
21+
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
22+
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
1923
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
2024
use Symfony\Component\HttpClient\MockHttpClient;
2125
use Symfony\Component\HttpClient\Response\MockResponse;
@@ -349,34 +353,10 @@ public function testCustomUserLoader()
349353

350354
/**
351355
* @requires extension openssl
356+
* @dataProvider validAccessTokens
352357
*/
353-
public function testOidcSuccess()
358+
public function testOidcSuccess(string $token)
354359
{
355-
$time = time();
356-
$claims = [
357-
'iat' => $time,
358-
'nbf' => $time,
359-
'exp' => $time + 3600,
360-
'iss' => 'https://www.example.com',
361-
'aud' => 'Symfony OIDC',
362-
'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
363-
'username' => 'dunglas',
364-
];
365-
$token = (new CompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
366-
new ES256(),
367-
])))->create()
368-
->withPayload(json_encode($claims))
369-
// tip: use https://mkjwk.org/ to generate a JWK
370-
->addSignature(new JWK([
371-
'kty' => 'EC',
372-
'crv' => 'P-256',
373-
' F987 x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
374-
'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
375-
'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
376-
]), ['alg' => 'ES256'])
377-
->build()
378-
);
379-
380360
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
381361
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => \sprintf('Bearer %s', $token)]);
382362
$response = $client->getResponse();
@@ -386,6 +366,21 @@ public function testOidcSuccess()
386366
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
387367
}
388368

369+
/**
370+
* @requires extension openssl
371+
* @dataProvider invalidAccessTokens
372+
*/
373+
public function testOidcFailure(string $token)
374+
{
375+
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
376+
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => \sprintf('Bearer %s', $token)]);
377+
$response = $client->getResponse();
378+
379+
$this->assertInstanceOf(Response::class, $response);
380+
$this->assertSame(401, $response->getStatusCode());
381+
$this->assertSame('Bearer realm="My API",error="invalid_token",error_description="Invalid credentials."', $response->headers->get('WWW-Authenticate'));
382+
}
383+
389384
public function testCasSuccess()
390385
{
391386
$casResponse = new MockResponse(<<<BODY
@@ -408,4 +403,90 @@ public function testCasSuccess()
408403
$this->assertSame(200, $response->getStatusCode());
409404
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
410405
}
406+
407+
public function validAccessTokens(): array
408+
{
409+
$time = time();
410+
$claims = [
411+
'iat' => $time,
412+
'nbf' => $time,
413+
'exp' => $time + 3600,
414+
'iss' => 'https://www.example.com',
415+
'aud' => 'Symfony OIDC',
416+
'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
417+
'username' => 'dunglas',
418+
];
419+
$jws = $this->createJws($claims);
420+
$jwe = $this->createJwe($jws);
421+
422+
return [
423+
[$jws],
424+
[$jwe],
425+
];
426+
}
427+
428+
public function invalidAccessTokens(): array
429+
{
430+
$time = time();
431+
$claims = [
432+
'iat' => $time,
433+
'nbf' => $time,
434+
'exp' => $time + 3600,
435+
'iss' => 'https://www.example.com',
436+
'aud' => 'Symfony OIDC',
437+
'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
438+
'username' => 'dunglas',
439+
];
440+
441+
return [
442+
[$this->createJws([...$claims, 'aud' => 'Invalid Audience'])],
443+
[$this->createJws([...$claims, 'iss' => 'Invalid Issuer'])],
444+
[$this->createJws([...$claims, 'exp' => $time - 3600])],
445+
[$this->createJws([...$claims, 'nbf' => $time + 3600])],
446+
[$this->createJws([...$claims, 'iat' => $time + 3600])],
447+
[$this->createJws([...$claims, 'username' => 'Invalid Username'])],
448+
[$this->createJwe($this->createJws($claims), ['exp' => $time - 3600])],
449+
[$this->createJwe($this->createJws($claims), ['cty' => 'x-specific'])],
450+
];
451+
}
452+
453+
private function createJws(array $claims, array $header = []): string
454+
{
455+
return (new JwsCompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
456+
new ES256(),
457+
])))->create()
458+
->withPayload(json_encode($claims))
459+
// tip: use https://mkjwk.org/ to generate a JWK
460+
->addSignature(new JWK([
461+
'kty' => 'EC',
462+
'crv' => 'P-256',
463+
'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
464+
'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
465+
'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
466+
]), [...$header, 'alg' => 'ES256'])
467+
->build()
468+
);
469+
}
470+
471+
private function createJwe(string $input, array $header = []): string
472+
{
473+
$jwk = new JWK([
474+
'kty' => 'EC',
475+
'use' => 'enc',
476+
'crv' => 'P-256',
477+
'kid' => 'enc-1720876375',
478+
'x' => '4P27-OB2s5ZP3Zt5ExxQ9uFrgnGaMK6wT1oqd5bJozQ',
479+
'y' => 'CNh-ZbKJBvz6hJ8JOulXclACP2OuoO2PtqT6WC8tLcU',
480+
]);
481+
return (new JweCompactSerializer())->serialize(
482+
(new JWEBuilder(new AlgorithmManager([
483+
new ECDHES(), new A128GCM(),
484+
])))->create()
485+
->withPayload($input)
486+
->withSharedProtectedHeader(['alg' => 'ECDH-ES', 'enc' => 'A128GCM', ...$header])
487+
// tip: use https://mkjwk.org/ to generate a JWK
488+
->addRecipient($jwk)
489+
->build()
490+
);
491+
}
411492
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ security:
2727
algorithm: '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"}]}'
30+
encryption:
31+
enabled: true
32+
algorithms: ['ECDH-ES', 'A128GCM']
33+
keyset: '{"keys": [{"kty": "EC","d": "YG0HnRsaYv2cUj7TpgHcRX1poL9l4cskIuOi1gXv0Dg","use": "enc","crv": "P-256","kid": "enc-1720876375","x": "4P27-OB2s5ZP3Zt5ExxQ9uFrgnGaMK6wT1oqd5bJozQ","y": "CNh-ZbKJBvz6hJ8JOulXclACP2OuoO2PtqT6WC8tLcU","alg": "ECDH-ES"}]}'
3034
token_extractors: 'header'
3135
realm: 'My API'
3236

0 commit comments

Comments
 (0)
0