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

Code before:

            throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
        }
    }

    /**
     * Computes the JWKS and sets the cache item TTL from provider headers.
     *
     * The cache entry lifetime is automatically adjusted based on the lowest TTL
     * advertised by the providers (via "Cache-Control: max-age" or "Expires" headers).
     *
     * @internal this method is public to enable async offline cache population
     */
    public function computeDiscoveryKeys(ItemInterface $item): array
    {
        $clients = $this->discoveryClients;
        if (!$clients) {
            throw new \LogicException('No OIDC discovery client configured.');
        }
        $logger = $this->logger;
        try {
            $keys = [];
            $minTtl = null;

            foreach ($clients as $client) {
                $config = $client->request('GET', '.well-known/openid-configuration')->toArray();

                $jwksUri = $config['jwks_uri'] ?? null;
                if (!\is_string($jwksUri) || '' === $jwksUri) {
                    throw new \RuntimeException('The "jwks_uri" is missing from the OIDC discovery document.');
                }

                $response = $client->request('GET', $jwksUri);
                $headers = $response->getHeaders();
                if (preg_match('/max-age=(\d+)/', $headers['cache-control'][0] ?? '', $m)) {
                    $currentTtl = (int) $m[1];
                } elseif (0 >= $currentTtl = strtotime($headers['expires'][0] ?? '@0') - time()) {
                    $currentTtl = null;
                }

                // 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'] ?? null)) {
                        $keys[] = $key;
                    }
                }
            }

Review comment (suggestion):
To benefit from concurrency, you should start all requests first (storing responses in some variables) and *then* accessing the blocking getters (like `toArray`) on those responses in a separate loop, so that the network IO of all requests can progress while your code waits for the first response.

Code after:

            throw new BadCredentialsException('Invalid credentials.', $e->getCode(), $e);
        }
    }

    /**
     * Computes the JWKS and sets the cache item TTL from provider headers.
     *
     * The cache entry lifetime is automatically adjusted based on the lowest TTL
     * advertised by the providers (via "Cache-Control: max-age" or "Expires" headers).
     *
     * @internal this method is public to enable async offline cache population
     */
    public function computeDiscoveryKeys(ItemInterface $item): array
    {
        $clients = $this->discoveryClients;
        if (!$clients) {
            throw new \LogicException('No OIDC discovery client configured.');
        }
        $logger = $this->logger;
        try {
            $keys = [];
            $minTtl = null;
            $configResponses = [];
            $jwkSetResponses = [];

            foreach ($clients as $client) {
                $configResponses[] = [$client, $client->request('GET', '.well-known/openid-configuration')];
            }

            foreach ($configResponses as [$client, $response]) {
                $config = $response->toArray();

                $jwksUri = $config['jwks_uri'] ?? null;
                if (!\is_string($jwksUri) || '' === $jwksUri) {
                    throw new \RuntimeException('The "jwks_uri" is missing from the OIDC discovery document.');
                }

                $jwkSetResponses[] = $client->request('GET', $jwksUri);
            }

            foreach ($jwkSetResponses as $response) {
                $headers = $response->getHeaders();
                if (preg_match('/max-age=(\d+)/', $headers['cache-control'][0] ?? '', $m)) {
                    $currentTtl = (int) $m[1];
                } elseif (0 >= $currentTtl = strtotime($headers['expires'][0] ?? '@0') - time()) {
                    $currentTtl = null;
                }

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