import "server-only"; import { SignJWT, jwtVerify } from "jose"; import { isUuid, isExternalUserId, parseAuthIdentity, type AuthIdentity, type AuthTokenIdentity, } from "@/lib/auth/types"; import type { DatabaseUser } from "@/lib/db/users"; export type VerifiedAuthToken = AuthTokenIdentity & { iat: number; exp: number; }; export class AuthError extends Error { readonly status = 401; constructor() { super("Unauthorized"); this.name = "AuthError"; } } type AuthTokenInput = Pick; export async function signAuthToken(user: AuthTokenInput): Promise { const config = jwtConfig(); return new SignJWT({ email: user.email, tenant_id: config.tenantId, external_user_id: user.externalUserId, }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setSubject(user.id) .setIssuedAt() .setIssuer(config.issuer) .setAudience(config.audience) .setExpirationTime("24h") .sign(config.secret); } export async function verifyAuthToken(token: string): Promise { try { const config = jwtConfig(); const { payload } = await jwtVerify(token, config.secret, { algorithms: ["HS256"], issuer: config.issuer, audience: config.audience, requiredClaims: [ "sub", "email", "iat", "exp", "tenant_id", "external_user_id", ], }); if ( !isUuid(payload.sub) || typeof payload.email !== "string" || payload.email.length === 0 || payload.tenant_id !== config.tenantId || !isExternalUserId(payload.external_user_id) || payload.aud !== config.audience || typeof payload.iat !== "number" || typeof payload.exp !== "number" ) { throw new Error("Invalid token claims"); } return { id: payload.sub, email: payload.email, externalUserId: payload.external_user_id, tenantId: payload.tenant_id, iat: payload.iat, exp: payload.exp, }; } catch { throw unauthorized(); } } export function toAuthIdentity(user: DatabaseUser): AuthIdentity { const identity = parseAuthIdentity({ id: user.id, externalUserId: user.externalUserId, login: user.login, fullName: user.fullName, position: user.position, email: user.email, firstName: user.firstName, lastName: user.lastName, middleName: user.middleName, roles: user.roles, lastModelId: user.lastModelId, }); if (!identity) { throw new Error("External user profile is incomplete"); } return identity; } export async function authenticateRequest(request: Request): Promise { const authorization = request.headers.get("Authorization"); const match = authorization?.match(/^Bearer\s+(\S+)$/i); if (!match) { throw unauthorized(); } return verifyAuthToken(match[1]); } function jwtConfig(): { secret: Uint8Array; issuer: string; audience: string; tenantId: string; } { const secret = new TextEncoder().encode(requireEnv("JWT_SECRET")); if (secret.byteLength < 32) { throw new Error("Invalid required environment variable: JWT_SECRET"); } const issuer = requireEnv("JWT_ISSUER"); const audience = requireEnv("JWT_AUDIENCE"); const tenantId = requireEnv("AEGIDA_TENANT_ID"); if (!validTenantId(tenantId)) { throw new Error("Invalid required environment variable: AEGIDA_TENANT_ID"); } return { secret, issuer, audience, tenantId }; } function requireEnv(name: string): string { const value = process.env[name]; if (!value || value !== value.trim()) { throw new Error(`Missing required environment variable: ${name}`); } return value; } function validTenantId(value: string): boolean { return /^[A-Za-z0-9_.-]{1,128}$/.test(value); } function unauthorized(): AuthError { return new AuthError(); }