79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
/**
|
||
* JWKS-Client: lädt die öffentlichen Hub-Schlüssel und cached sie.
|
||
* Tools validieren Hub-Tokens gegen diesen Key (RS256) – kein geteiltes Secret.
|
||
*
|
||
* platform-plan.md §5.2: Der Hub exposet /.well-known/jwks.json.
|
||
*/
|
||
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
|
||
import { getConfig } from "@/lib/config";
|
||
|
||
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 Minuten (passend zum Hub-Cache-Header)
|
||
|
||
interface CachedJwks {
|
||
jwks: ReturnType<typeof createRemoteJWKSet>;
|
||
fetchedAt: number;
|
||
}
|
||
|
||
let cache: CachedJwks | null = null;
|
||
|
||
function getHubUrl(): string {
|
||
const hubUrl = getConfig().hubUrl;
|
||
if (!hubUrl) {
|
||
throw new Error("HUB_URL fehlt – Hub-SSO ist nicht konfiguriert.");
|
||
}
|
||
return hubUrl.replace(/\/$/, "");
|
||
}
|
||
|
||
/** Remote-JWKSet mit einfachem TTL-Cache (jose hat keinen eingebauten). */
|
||
function getJwksSet(): ReturnType<typeof createRemoteJWKSet> {
|
||
const now = Date.now();
|
||
if (cache && now - cache.fetchedAt < JWKS_CACHE_TTL_MS) {
|
||
return cache.jwks;
|
||
}
|
||
const jwks = createRemoteJWKSet(new URL(`${getHubUrl()}/.well-known/jwks.json`));
|
||
cache = { jwks, fetchedAt: now };
|
||
return jwks;
|
||
}
|
||
|
||
export interface HubTokenResult {
|
||
hubId: string;
|
||
email: string;
|
||
name: string;
|
||
isAdmin: boolean;
|
||
}
|
||
|
||
/**
|
||
* Validiert ein Hub-Token (Signatur, TTL, aud, iss).
|
||
* Wirft bei jedem Fehler – der Aufrufer entscheidet über die Response.
|
||
*/
|
||
export async function verifyHubToken(token: string): Promise<HubTokenResult> {
|
||
const config = getConfig();
|
||
const expectedAud = config.hubToolSlug;
|
||
if (!expectedAud) {
|
||
throw new Error("HUB_TOOL_SLUG fehlt – Tool-Slug für Hub-SSO nicht konfiguriert.");
|
||
}
|
||
|
||
const { payload } = await jwtVerify(token, getJwksSet(), {
|
||
algorithms: ["RS256"],
|
||
audience: expectedAud,
|
||
issuer: config.hubIssuer ?? "http://localhost:3001",
|
||
clockTolerance: 5,
|
||
});
|
||
|
||
const hubId = payload.sub;
|
||
const email = payload.email;
|
||
const name = payload.name;
|
||
if (typeof hubId !== "string" || typeof email !== "string" || typeof name !== "string") {
|
||
throw new Error("Hub-Token enthält unvollständige Claims.");
|
||
}
|
||
|
||
return {
|
||
hubId,
|
||
email,
|
||
name,
|
||
isAdmin: payload.isAdmin === true,
|
||
};
|
||
}
|
||
|
||
/** Nur für Typ-Export (jose JWTPayload) – nicht Teil des Contracts. */
|
||
export type { JWTPayload }; |