Files
kalendartool/lib/auth/hub.ts

79 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 };