Files
minecraft-account-manager/apps/web/src/lib/auth/admin-api-auth.ts
T
dmg c2ac2ad16b
CI / validate (push) Successful in 6m49s
Release / release (push) Successful in 11m24s
feat(auth): verify machine tokens for admin read APIs
2026-09-10 14:53:47 -04:00

95 lines
4.9 KiB
TypeScript

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<typeof createRemoteJWKSet> } | 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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function authorizeBearer(request: Request): Promise<Authorization> {
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<typeof bearerConfiguration>;
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<Authorization> {
// 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);
}
}