Skip to content

Commit 9868490

Browse files
committed
[Security] Fix OIDC discovery when using multiple HttpClient instances
1 parent 296007d commit 9868490

File tree

2 files changed

+100
-13
lines changed

2 files changed

+100
-13
lines changed

AccessToken/Oidc/OidcTokenHandler.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,24 +149,31 @@ public function getUserBadgeFrom(string $accessToken): UserBadge
149149
public function computeDiscoveryKeys(ItemInterface $item): array
150150
{
151151
$clients = $this->discoveryClients;
152+
if (!$clients) {
153+
throw new \LogicException('No OIDC discovery client configured.');
154+
}
152155
$logger = $this->logger;
153-
154156
try {
157+
$keys = [];
158+
$minTtl = null;
155159
$configResponses = [];
160+
$jwkSetResponses = [];
161+
156162
foreach ($clients as $client) {
157-
$configResponses[] = $client->request('GET', '.well-known/openid-configuration', [
158-
'user_data' => $client,
159-
]);
163+
$configResponses[] = [$client, $client->request('GET', '.well-known/openid-configuration')];
160164
}
161165

162-
$jwkSetResponses = [];
163-
foreach ($client->stream($configResponses) as $response => $chunk) {
164-
if ($chunk->isLast()) {
165-
$jwkSetResponses[] = $response->getInfo('user_data')->request('GET', $response->toArray()['jwks_uri']);
166+
foreach ($configResponses as [$client, $response]) {
167+
$config = $response->toArray();
168+
169+
$jwksUri = $config['jwks_uri'] ?? null;
170+
if (!\is_string($jwksUri) || '' === $jwksUri) {
171+
throw new \RuntimeException('The "jwks_uri" is missing from the OIDC discovery document.');
166172
}
173+
174+
$jwkSetResponses[] = $client->request('GET', $jwksUri);
167175
}
168-
$keys = [];
169-
$minTtl = null;
176+
170177
foreach ($jwkSetResponses as $response) {
171178
$headers = $response->getHeaders();
172179
if (preg_match('/max-age=(\d+)/', $headers['cache-control'][0] ?? '', $m)) {
@@ -181,7 +188,7 @@ public function computeDiscoveryKeys(ItemInterface $item): array
181188
}
182189

183190
foreach ($response->toArray()['keys'] as $key) {
184-
if ('sig' === $key['use']) {
191+
if ('sig' === ($key['use'] ?? null)) {
185192
$keys[] = $key;
186193
}
187194
}
@@ -198,6 +205,7 @@ public function computeDiscoveryKeys(ItemInterface $item): array
198205
'error' => $e->getMessage(),
199206
'trace' => $e->getTraceAsString(),
200207
]);
208+
201209
throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
202210
}
203211
}

Tests/AccessToken/Oidc/OidcTokenHandlerTest.php

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\Security\Core\User\OidcUser;
2929
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;
3030
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
31+
use Symfony\Contracts\Cache\ItemInterface;
3132

3233
#[RequiresPhpExtension('openssl')]
3334
class OidcTokenHandlerTest extends TestCase
@@ -316,7 +317,8 @@ public function testDiscoveryCachesJwksAccordingToCacheControl()
316317
{
317318
$time = time();
318319
$claims = [
319-
'iat' => $time, 'nbf' => $time,
320+
'iat' => $time,
321+
'nbf' => $time,
320322
'exp' => $time + 3600,
321323
'iss' => 'https://www.example.com',
322324
'aud' => self::AUDIENCE,
@@ -355,7 +357,8 @@ public function testDiscoveryCachesJwksAccordingToExpires()
355357
{
356358
$time = time();
357359
$claims = [
358-
'iat' => $time, 'nbf' => $time,
360+
'iat' => $time,
361+
'nbf' => $time,
359362
'exp' => $time + 3600,
360363
'iss' => 'https://www.example.com',
361364
'aud' => self::AUDIENCE,
@@ -390,4 +393,80 @@ public function testDiscoveryCachesJwksAccordingToExpires()
390393
$this->assertSame('user-expires', $handler->getUserBadgeFrom($token)->getUserIdentifier());
391394
$this->assertSame(2, $requestCount);
392395
}
396+
397+
public function testComputeDiscoveryKeysReturnsEmptyWhenNoClients()
398+
{
399+
$cache = new ArrayAdapter();
400+
$handler = new OidcTokenHandler(
401+
new AlgorithmManager([new ES256()]),
402+
null,
403+
self::AUDIENCE,
404+
['https://www.example.com']
405+
);
406+
407+
$handler->enableDiscovery($cache, [], 'oidc_empty_clients');
408+
409+
$item = $this->createMock(ItemInterface::class);
410+
$item->expects($this->never())->method('expiresAfter');
411+
412+
$this->expectException(\LogicException::class);
413+
$this->expectExceptionMessage('No OIDC discovery client configured.');
414+
$handler->computeDiscoveryKeys($item);
415+
}
416+
417+
public function testDiscoveryThrowsWhenJwksUriIsMissing()
418+
{
419+
$time = time();
420+
$claims = [
421+
'iat' => $time,
422+
'nbf' => $time,
423+
'exp' => $time + 3600,
424+
'iss' => 'https://www.example.com',
425+
'aud' => self::AUDIENCE,
426+
'sub' => 'user-missing-jwks-uri',
427+
];
428+
$token = self::buildJWS(json_encode($claims));
429+
430+
$httpClient = new MockHttpClient([
431+
new JsonMockResponse(['issuer' => 'https://www.example.com']),
432+
]);
433+
434+
$cache = new ArrayAdapter();
435+
$handler = new OidcTokenHandler(
436+
new AlgorithmManager([new ES256()]),
437+
null,
438+
self::AUDIENCE,
439+
['https://www.example.com']
440+
);
441+
$handler->enableDiscovery($cache, $httpClient, 'oidc_missing_jwks_uri');
442+
443+
$this->expectException(BadCredentialsException::class);
444+
$handler->getUserBadgeFrom($token);
445+
}
446+
447+
public function testDiscoveryIgnoresNonSignatureKeys()
448+
{
449+
$httpClient = new MockHttpClient([
450+
new JsonMockResponse(['jwks_uri' => 'https://www.example.com/jwks.json']),
451+
new JsonMockResponse([
452+
'keys' => [
453+
array_merge(self::getJWK()->all(), ['use' => 'enc']),
454+
array_merge(self::getSecondJWK()->all(), []),
455+
],
456+
]),
457+
]);
458+
459+
$cache = new ArrayAdapter();
460+
$handler = new OidcTokenHandler(
461+
new AlgorithmManager([new ES256()]),
462+
null,
463+
self::AUDIENCE,
464+
['https://www.example.com']
465+
);
466+
$handler->enableDiscovery($cache, $httpClient, 'oidc_non_sig_keys');
467+
468+
$item = $this->createMock(ItemInterface::class);
469+
$item->expects($this->never())->method('expiresAfter');
470+
$this->assertSame([], $handler->computeDiscoveryKeys($item));
471+
}
393472
}

0 commit comments

Comments
 (0)