xtablo-source/apps/api/src/helpers/adminTokens.ts
2026-04-24 15:31:34 +02:00

201 lines
5.4 KiB
TypeScript

import { createHmac, timingSafeEqual } from "node:crypto";
import type { AppConfig } from "../config.js";
export type AdminRole = "viewer" | "operator" | "superadmin";
type TokenKind = "admin_access" | "admin_session";
type AdminTokenClaims = {
aud: string;
email: string;
exp: number;
role: AdminRole;
sub: string;
type: TokenKind;
};
export type AdminSessionClaims = {
aud: string;
exp: number;
operatorEmail: string;
operatorId: string;
role: AdminRole;
};
type AdminTokenErrorCode =
| "ADMIN_SESSION_REQUIRED"
| "INVALID_ADMIN_ACCESS_TOKEN"
| "INVALID_ADMIN_SESSION";
type AdminTokenFailure = {
code: AdminTokenErrorCode;
error: string;
statusCode: 401;
success: false;
};
type AdminTokenSuccess<T> = {
success: true;
value: T;
};
export type AdminTokenResult<T> = AdminTokenFailure | AdminTokenSuccess<T>;
type ExchangeResult = {
expiresAt: string;
operatorEmail: string;
operatorId: string;
role: AdminRole;
sessionToken: string;
};
function encodeSegment(value: unknown) {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
function decodeSegment<T>(segment: string): T | null {
try {
return JSON.parse(Buffer.from(segment, "base64url").toString("utf8")) as T;
} catch {
return null;
}
}
function signToken(claims: AdminTokenClaims, secret: string) {
const header = encodeSegment({ alg: "HS256", typ: "JWT" });
const payload = encodeSegment(claims);
const signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
return `${header}.${payload}.${signature}`;
}
function invalidToken(error: string, code: AdminTokenErrorCode): AdminTokenFailure {
return {
code,
error,
statusCode: 401,
success: false,
};
}
function isFailure<T>(result: AdminTokenResult<T>): result is AdminTokenFailure {
return !result.success;
}
function verifyToken(
token: string,
config: AppConfig,
expectedType: TokenKind
): AdminTokenResult<AdminTokenClaims> {
const segments = token.split(".");
if (segments.length !== 3) {
return invalidToken(
expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session",
expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION"
);
}
const [header, payload, signature] = segments;
const expectedSignature = createHmac("sha256", config.ADMIN_TOKEN_SIGNING_SECRET)
.update(`${header}.${payload}`)
.digest();
const receivedSignature = Buffer.from(signature, "base64url");
if (
expectedSignature.length !== receivedSignature.length ||
!timingSafeEqual(expectedSignature, receivedSignature)
) {
return invalidToken(
expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session",
expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION"
);
}
const claims = decodeSegment<AdminTokenClaims>(payload);
if (!claims) {
return invalidToken(
expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session",
expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION"
);
}
if (claims.type !== expectedType || claims.aud !== config.ADMIN_TOKEN_AUDIENCE) {
return invalidToken(
expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session",
expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION"
);
}
if (claims.exp <= Math.floor(Date.now() / 1000)) {
return invalidToken(
expectedType === "admin_access" ? "Invalid privileged access token" : "Invalid admin session",
expectedType === "admin_access" ? "INVALID_ADMIN_ACCESS_TOKEN" : "INVALID_ADMIN_SESSION"
);
}
return { success: true, value: claims };
}
export function exchangePrivilegedToken(
token: string,
config: AppConfig
): AdminTokenResult<ExchangeResult> {
const verifiedAccessToken = verifyToken(token, config, "admin_access");
if (isFailure(verifiedAccessToken)) {
return verifiedAccessToken;
}
const accessClaims = verifiedAccessToken.value;
const sessionExpiry = Math.floor(Date.now() / 1000) + 15 * 60;
const sessionToken = signToken(
{
aud: config.ADMIN_TOKEN_AUDIENCE,
email: accessClaims.email,
exp: sessionExpiry,
role: accessClaims.role,
sub: accessClaims.sub,
type: "admin_session",
},
config.ADMIN_TOKEN_SIGNING_SECRET
);
return {
success: true,
value: {
expiresAt: new Date(sessionExpiry * 1000).toISOString(),
operatorEmail: accessClaims.email,
operatorId: accessClaims.sub,
role: accessClaims.role,
sessionToken,
},
};
}
export function verifyAdminSession(
token: string | undefined,
config: AppConfig
): AdminTokenResult<AdminSessionClaims> {
if (!token) {
return invalidToken("Admin session required", "ADMIN_SESSION_REQUIRED");
}
const verifiedSession = verifyToken(token, config, "admin_session");
if (isFailure(verifiedSession)) {
return {
...verifiedSession,
code: "ADMIN_SESSION_REQUIRED",
error: "Admin session required",
};
}
return {
success: true,
value: {
aud: verifiedSession.value.aud,
exp: verifiedSession.value.exp,
operatorEmail: verifiedSession.value.email,
operatorId: verifiedSession.value.sub,
role: verifiedSession.value.role,
},
};
}