mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
98 lines
3.5 KiB
PHP
98 lines
3.5 KiB
PHP
<?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(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<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(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;
|
|
}
|
|
}
|
|
}
|