45
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use Firebase\JWT\JWK;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Valide les jetons d'accès Entra envoyés par l'add-in (obtenus par NAA).
|
||||
* Contrôles : signature (clés publiques Entra), expiration, audience, émetteur,
|
||||
* tenant autorisé et actif, scope attendu.
|
||||
*/
|
||||
class EntraTokenValidator
|
||||
{
|
||||
/** @return array<string, mixed> claims du jeton */
|
||||
public function validate(string $jwt): array
|
||||
{
|
||||
JWT::$leeway = (int) config('signature.addin.leeway_seconds', 120);
|
||||
|
||||
try {
|
||||
$claims = (array) JWT::decode($jwt, $this->keys());
|
||||
} catch (Throwable $e) {
|
||||
// Rotation des clés Entra : un seul nouvel essai avec des clés fraîches.
|
||||
Cache::forget('entra-jwks');
|
||||
try {
|
||||
$claims = (array) JWT::decode($jwt, $this->keys());
|
||||
} catch (Throwable) {
|
||||
throw new RuntimeException('Jeton invalide : '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$tid = $claims['tid'] ?? null;
|
||||
if (! $tid || ! Tenant::where('entra_tenant_id', $tid)->where('active', true)->exists()) {
|
||||
throw new RuntimeException('Tenant non autorisé');
|
||||
}
|
||||
|
||||
$issuers = ["https://login.microsoftonline.com/{$tid}/v2.0", "https://sts.windows.net/{$tid}/"];
|
||||
if (! in_array($claims['iss'] ?? null, $issuers, true)) {
|
||||
throw new RuntimeException('Émetteur inattendu');
|
||||
}
|
||||
|
||||
$audiences = (array) config('signature.addin.audiences');
|
||||
if (! $audiences || ! in_array($claims['aud'] ?? null, $audiences, true)) {
|
||||
throw new RuntimeException('Audience inattendue : reçue « '.($claims['aud'] ?? 'aucune').' », attendue(s) « '.implode(', ', $audiences).' »');
|
||||
}
|
||||
|
||||
$scopes = explode(' ', (string) ($claims['scp'] ?? ''));
|
||||
if (! in_array(config('signature.addin.required_scope'), $scopes, true)) {
|
||||
throw new RuntimeException('Scope manquant : reçu « '.($claims['scp'] ?? 'aucun').' »');
|
||||
}
|
||||
|
||||
if (empty($claims['oid'])) {
|
||||
throw new RuntimeException('Identifiant utilisateur absent');
|
||||
}
|
||||
|
||||
return $claims;
|
||||
}
|
||||
|
||||
private function keys(): array
|
||||
{
|
||||
$jwks = Cache::remember('entra-jwks', config('signature.addin.jwks_cache_seconds', 86400), function () {
|
||||
$res = Http::timeout(10)->get('https://login.microsoftonline.com/common/discovery/v2.0/keys');
|
||||
if (! $res->successful()) {
|
||||
throw new RuntimeException('Clés Entra indisponibles');
|
||||
}
|
||||
|
||||
return $res->json();
|
||||
});
|
||||
|
||||
return JWK::parseKeySet($jwks, 'RS256');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user