import { getServerSession } from "next-auth"; import { createRemoteJWKSet, errors, jwtVerify } from "jose"; import { problemDetails } from "@minecraft-account-manager/contracts"; import { problemInstance, problemResponse } from "@/lib/problem-response"; import { adminAuthOptions, requiredAdminRole } from "./admin-auth"; export type AdminApiIdentity = { authenticationMethod: "session" | "bearer"; subject: string | null; name: string | null; email: string | null; }; type Authorization = { identity: AdminApiIdentity; response?: never } | { response: Response; identity?: never }; function failure(request: Request, status: 401 | 403 | 503): Authorization { const problems = { 401: ["unauthorized", "Authentication required", "Supply valid administrator credentials."], 403: ["forbidden", "Administrator role required", "This API is restricted to administrators."], 503: ["admin-auth-unavailable", "Authentication unavailable", "Administrator authentication is temporarily unavailable."], } as const; const [code, title, detail] = problems[status]; const response = problemResponse(problemDetails(`urn:error:${code}`, title, status, detail, problemInstance(request))); if (status === 401) response.headers.set("www-authenticate", 'Bearer realm="admin-api"'); return { response }; } // One bounded, process-local resolver. jose coalesces fetches and refreshes rotated keys. let remote: { issuer: string; keys: ReturnType } | undefined; function bearerConfiguration() { const issuer = process.env.KEYCLOAK_ISSUER_URL?.trim() ?? ""; const audience = process.env.KEYCLOAK_CLIENT_ID?.trim() ?? ""; const url = new URL(issuer); if (!audience || url.protocol !== "https:" || url.username || url.password || url.search || url.hash) { throw new Error("Invalid administrator authentication configuration"); } if (!remote || remote.issuer !== issuer) { // Never discover a key URL from untrusted token claims or headers (jku/x5u/iss). const jwksUrl = new URL(`${issuer.replace(/\/$/, "")}/protocol/openid-connect/certs`); remote = { issuer, keys: createRemoteJWKSet(jwksUrl, { timeoutDuration: 5_000, cooldownDuration: 30_000, cacheMaxAge: 600_000, }) }; } return { issuer, audience, keys: remote.keys }; } function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } async function authorizeBearer(request: Request): Promise { const match = /^Bearer +([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/i.exec(request.headers.get("authorization") ?? ""); if (!match) return failure(request, 401); let configuration: ReturnType; try { configuration = bearerConfiguration(); } catch { return failure(request, 503); } try { const { issuer, audience, keys } = configuration; const { payload } = await jwtVerify(match[1]!, keys, { issuer, audience, algorithms: ["RS256"], requiredClaims: ["exp", "sub"], }); if (typeof payload.sub !== "string" || !payload.sub.trim()) return failure(request, 401); const access = payload.resource_access; const client = record(access) && Object.hasOwn(access, audience) ? access[audience] : undefined; const roles = record(client) ? client.roles : undefined; if (!Array.isArray(roles) || !roles.includes(requiredAdminRole)) return failure(request, 403); return { identity: { authenticationMethod: "bearer", subject: payload.sub, name: null, email: null } }; } catch (error) { // Verification failures are invalid credentials; transport/configuration failures are unavailable. const invalid = error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired || error instanceof errors.JWSInvalid || error instanceof errors.JWTInvalid || error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JOSEAlgNotAllowed || error instanceof errors.JWKSNoMatchingKey || error instanceof errors.JOSENotSupported; return failure(request, invalid ? 401 : 503); } } /** API-only authorization; never use bearer tokens to authorize browser actions. */ export async function authorizeAdminApi(request: Request): Promise { // Presence, including an empty/unsupported header, is authoritative. Never fall back. if (request.headers.has("authorization")) return authorizeBearer(request); try { const session = await getServerSession(adminAuthOptions); if (!session) return failure(request, 401); const roles = (session.user as { roles?: unknown } | undefined)?.roles; if (!Array.isArray(roles) || !roles.includes(requiredAdminRole)) return failure(request, 403); return { identity: { authenticationMethod: "session", subject: null, name: session.user?.name ?? null, email: session.user?.email ?? null, } }; } catch { return failure(request, 503); } }