Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
79
lib/auth/hub.ts
Normal file
79
lib/auth/hub.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user