Skip to content

Commit 46a4432

Browse files
bug #62495 [Security][Http] Fix OIDC discovery when multiple HttpClient instances are used (Ali-HENDA)
This PR was merged into the 7.4 branch. Discussion ---------- [Security][Http] Fix OIDC discovery when multiple HttpClient instances are used | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix | License | MIT This PR fixes an issue in the OIDC JWKS discovery logic revealed in the discussion of #62369. $client->stream() was used incorrectly: $client could be undefined, and responses must be streamed using the same client instance that created them, which breaks when multiple HttpClientInterface instances are configured. The logic now performs sequential discovery per client, avoiding cross-client streaming and ensuring correctness. This PR also hardens the "use" check ($key['use'] ?? null). Commits ------- 7a885d29993 [Security] Fix OIDC discovery when using multiple HttpClient instances
2 parents 92f9cc6 + 9868490 commit 46a4432

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)