Repository: symfony/symfony
File: src/Symfony/Component/Security/Http/AccessToken/Oidc/OidcTokenHandler.php (PHP)

Code before:
     * advertised by the providers (via "Cache-Control: max-age" or "Expires" headers).
     */
    private function fetchDiscoveryKeys(): array
    {
        $clients = $this->discoveryClients;
        $logger = $this->logger;
        $keys = $this->discoveryCache->get($this->oidcConfigurationCacheKey, static function (ItemInterface $item) use ($clients, $logger): array {
            try {
                $configResponses = [];
                foreach ($clients as $client) {
                    $configResponses[] = $client->request('GET', '.well-known/openid-configuration', [
                        'user_data' => $client,
                    ]);
                }
                $jwkSetResponses = [];
                foreach ($client->stream($configResponses) as $response => $chunk) {
                    if ($chunk->isLast()) {
                        $jwkSetResponses[] = $response->getInfo('user_data')->request('GET', $response->toArray()['jwks_uri']);
                    }
                }
                $keys = [];
                $minTtl = null;
                foreach ($jwkSetResponses as $response) {
                    $currentTtl = null;
                    $headers = array_change_key_case($response->getHeaders(), \CASE_LOWER);
                    if (isset($headers['cache-control'][0]) && preg_match('/max-age=(\d+)/', $headers['cache-control'][0], $m)) {
                        $currentTtl = (int) $m[1];
                    } elseif (!empty($headers['expires'][0])) {
                        $expires = strtotime($headers['expires'][0]);
                        if ($expires > time()) {
                            $currentTtl = $expires - time();
                        }
                    }

                    // Apply the lowest TTL found to ensure all keys in the set are still valid
                    if (null !== $currentTtl && (null === $minTtl || $currentTtl < $minTtl)) {
                        $minTtl = $currentTtl;
                    }

                    foreach ($response->toArray()['keys'] as $key) {
                        if ('sig' === $key['use']) {
                            $keys[] = $key;
                        }
                    }
                }

                if (null !== $minTtl && $minTtl > 0) {
                    $item->expiresAfter($minTtl);
                }

                return $keys;

Review comment (performance):
```suggestion
                    if (preg_match('/max-age=(\d+)/', $headers['cache-control'][0] ?? '', $m)) {
```

Code after:
            new Checker\NotBeforeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: true),
            new Checker\ExpirationTimeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: true),
            new Checker\AudienceChecker($this->audience),
            new Checker\IssuerChecker($this->issuers),
        ];
        $claimCheckerManager = new ClaimCheckerManager($checkers);

        // if this check fails, an InvalidClaimException is thrown
        return $claimCheckerManager->check($claims);
    }

    private function decryptIfNeeded(string $accessToken): string
    {
        if (null === $this->decryptionKeyset || null === $this->decryptionAlgorithms) {
            $this->logger?->debug('The encrypted tokens (JWE) are not supported. Skipping.');

            return $accessToken;
        }

        $jweHeaderChecker = new Checker\HeaderCheckerManager(
            [
                new Checker\AlgorithmChecker($this->decryptionAlgorithms->list()),
                new Checker\CallableChecker('enc', fn ($value) => \in_array($value, $this->decryptionAlgorithms->list())),
                new Checker\CallableChecker('cty', fn ($value) => 'JWT' === $value),
                new Checker\IssuedAtChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: true),
                new Checker\NotBeforeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: true),
                new Checker\ExpirationTimeChecker(clock: $this->clock, allowedTimeDrift: 0, protectedHeaderOnly: true),
            ],
            [new JWETokenSupport()]
        );
        $jweDecrypter = new JWEDecrypter($this->decryptionAlgorithms, null);
        $serializerManager = new JWESerializerManager([new JweCompactSerializer()]);
        try {
            $jwe = $serializerManager->unserialize($accessToken);
            $jweHeaderChecker->check($jwe, 0);
            $result = $jweDecrypter->decryptUsingKeySet($jwe, $this->decryptionKeyset, 0);
            if (false === $result) {
                throw new \RuntimeException('The JWE could not be decrypted.');
            }

            $payload = $jwe->getPayload();
            if (null === $payload) {
                throw new \RuntimeException('The JWE payload is empty.');
            }

            return $payload;
        } catch (\InvalidArgumentException|\RuntimeException $e) {
            if ($this->enforceEncryption) {
                $this->logger?->error('An error occurred while decrypting the token.', [
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString(),