mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-18 17:45:09 +00:00
6e47d76ba6
Client file sharing, rebuilt from the ground up: a private area per client, resumable uploads, folders, groups and categories, sharing with expiry dates and download limits, comments, file versions, an activity log, a REST API, and sixteen languages. This repository begins here. ProjectSend 2 was developed privately, and that development history is not published — the previous generation remains available, with its own history, at projectsend/legacy. Free software under the GNU General Public License v2, or (at your option) any later version.
102 lines
3.6 KiB
PHP
102 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Identity\Social;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* An OpenID Connect server describing itself.
|
|
*
|
|
* The reason this application can speak to Keycloak, Authentik, Authelia,
|
|
* Okta, Auth0 and Entra without a class for each: every one of them
|
|
* publishes its endpoints at a well-known path, so the entire
|
|
* configuration of an unknown server is its issuer URL.
|
|
*
|
|
* HTTPS only, and not negotiable. The document names the URL we will send
|
|
* a person's browser to and the URL we will post a client secret to; over
|
|
* plaintext, anyone on the path chooses both.
|
|
*/
|
|
class OidcDiscovery
|
|
{
|
|
/**
|
|
* Endpoints change on the order of never, but an administrator who
|
|
* has just fixed a typo should not wait a day to find out. A short
|
|
* life plus an explicit flush when settings are saved covers both.
|
|
*/
|
|
private const TTL_SECONDS = 3600;
|
|
|
|
/**
|
|
* @return array{authorization_endpoint: string, token_endpoint: string, userinfo_endpoint: ?string, supports_pkce: bool}
|
|
*/
|
|
public function for(string $issuerUrl): array
|
|
{
|
|
$issuer = rtrim(trim($issuerUrl), '/');
|
|
|
|
if (! str_starts_with($issuer, 'https://')) {
|
|
throw new RuntimeException('An OpenID Connect issuer must be an https:// URL.');
|
|
}
|
|
|
|
/** @var array{authorization_endpoint: string, token_endpoint: string, userinfo_endpoint: ?string, supports_pkce: bool} */
|
|
return Cache::remember(
|
|
self::cacheKey($issuer),
|
|
self::TTL_SECONDS,
|
|
fn (): array => $this->fetch($issuer),
|
|
);
|
|
}
|
|
|
|
public function forget(string $issuerUrl): void
|
|
{
|
|
Cache::forget(self::cacheKey(rtrim(trim($issuerUrl), '/')));
|
|
}
|
|
|
|
private static function cacheKey(string $issuer): string
|
|
{
|
|
return 'identity.oidc.discovery.v1.'.sha1($issuer);
|
|
}
|
|
|
|
/**
|
|
* @return array{authorization_endpoint: string, token_endpoint: string, userinfo_endpoint: ?string, supports_pkce: bool}
|
|
*/
|
|
private function fetch(string $issuer): array
|
|
{
|
|
$url = $issuer.'/.well-known/openid-configuration';
|
|
|
|
// Short, because this runs inside a person's click on "Sign in".
|
|
// A provider that has stopped answering must fail the login, not
|
|
// hold the worker — the same reasoning as LdapAuthenticator's
|
|
// circuit breaker.
|
|
$response = Http::timeout(5)->connectTimeout(5)->acceptJson()->get($url);
|
|
|
|
if (! $response->successful()) {
|
|
throw new RuntimeException("The OpenID Connect discovery document at {$url} could not be read.");
|
|
}
|
|
|
|
$document = $response->json();
|
|
|
|
if (! is_array($document)) {
|
|
throw new RuntimeException("The OpenID Connect discovery document at {$url} is not JSON.");
|
|
}
|
|
|
|
foreach (['authorization_endpoint', 'token_endpoint'] as $required) {
|
|
if (! is_string($document[$required] ?? null) || $document[$required] === '') {
|
|
throw new RuntimeException("The OpenID Connect discovery document at {$url} has no {$required}.");
|
|
}
|
|
}
|
|
|
|
$methods = $document['code_challenge_methods_supported'] ?? [];
|
|
|
|
return [
|
|
'authorization_endpoint' => $document['authorization_endpoint'],
|
|
'token_endpoint' => $document['token_endpoint'],
|
|
'userinfo_endpoint' => is_string($document['userinfo_endpoint'] ?? null)
|
|
? $document['userinfo_endpoint']
|
|
: null,
|
|
'supports_pkce' => is_array($methods) && in_array('S256', $methods, true),
|
|
];
|
|
}
|
|
}
|