assertHttpsUrl($issuerUrl, new OidcDiscoveryException('Issuer URL must be an absolute HTTPS URL.')); $issuerUrl = rtrim($issuerUrl, '/'); $cacheKey = 'oidc:discovery:'.hash('sha256', $issuerUrl); return Cache::remember($cacheKey, 3600, function () use ($issuerUrl): OidcDiscoveryDocument { $url = $issuerUrl.'/.well-known/openid-configuration'; try { $response = Http::timeout(5)->connectTimeout(3)->acceptJson()->get($url); } catch (Throwable $e) { throw new OidcDiscoveryException("Failed to fetch discovery document: {$e->getMessage()}", previous: $e); } if ($response->failed()) { throw new OidcDiscoveryException("Discovery endpoint returned HTTP {$response->status()}"); } $json = $response->json(); if (! is_array($json) || $json === []) { throw new OidcDiscoveryException('Discovery endpoint returned invalid JSON.'); } $discovery = OidcDiscoveryDocument::fromArray($json); if (rtrim($discovery->issuer, '/') !== $issuerUrl) { throw new OidcDiscoveryException('Discovery issuer does not match the configured issuer URL.'); } return $discovery; }); } /** * Fetch the JWKS for the given URI. * * When $forceRefresh is true the cached document is bypassed so freshly * rotated signing keys become visible immediately. A short cooldown still * prevents a flood of upstream requests if many logins miss the same kid. * * @return array */ public function jwks(string $jwksUri, bool $forceRefresh = false): array { $this->assertHttpsUrl($jwksUri, new OidcJwksException('JWKS URI must be an absolute HTTPS URL.')); $cacheKey = 'oidc:jwks:'.hash('sha256', $jwksUri); if ($forceRefresh) { $cooldownKey = $cacheKey.':refresh'; if (Cache::add($cooldownKey, true, 60)) { Cache::forget($cacheKey); } } return Cache::remember($cacheKey, 21600, function () use ($jwksUri): array { try { $response = Http::timeout(5)->connectTimeout(3)->acceptJson()->get($jwksUri); } catch (Throwable $e) { throw new OidcJwksException("Failed to fetch JWKS: {$e->getMessage()}", previous: $e); } if ($response->failed()) { throw new OidcJwksException("JWKS endpoint returned HTTP {$response->status()}"); } $json = $response->json(); if (! is_array($json) || ! is_array($json['keys'] ?? null)) { throw new OidcJwksException("JWKS endpoint returned an invalid payload without 'keys'."); } return $json; }); } private function assertHttpsUrl(string $url, Throwable $exception): void { $parts = parse_url($url); if (($parts['scheme'] ?? null) !== 'https' || ! is_string($parts['host'] ?? null) || $parts['host'] === '') { throw $exception; } } }