Ctrl K
ComunidadGuías…

app/Auth/Oidc/OidcDiscoveryService.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php

namespace App\Auth\Oidc;

use App\Auth\Oidc\Exceptions\OidcDiscoveryException;
use App\Auth\Oidc\Exceptions\OidcJwksException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Throwable;

class OidcDiscoveryService
{
    public function discover(string $issuerUrl): OidcDiscoveryDocument
    {
        $this->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(15)->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<string, mixed>
     */
    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(15)->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;
        }
    }
}