import { SignJWT, jwtVerify } from "jose"; import { getConfig } from "./config"; import { verifyPassword, getUserByUsername, getUserById, type User, type UserRole, } from "./users"; const JWT_ISSUER = "overlord-server"; const JWT_AUDIENCE = "overlord-client"; const JWT_EXPIRATION = "7d"; const tokenBlacklist = new Set(); const tokenCache = new Map< string, { payload: JWTPayload; timestamp: number } >(); const TOKEN_CACHE_TTL = 5000; function getSecretKey(): Uint8Array { const config = getConfig(); return new TextEncoder().encode(config.auth.jwtSecret); } export interface JWTPayload { sub: string; userId: number; role: UserRole; iat: number; exp: number; iss: string; aud: string; } export interface AuthenticatedUser { username: string; userId: number; role: UserRole; } export async function generateToken(user: User): Promise { const token = await new SignJWT({ sub: user.username, userId: user.id, role: user.role, }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setIssuer(JWT_ISSUER) .setAudience(JWT_AUDIENCE) .setExpirationTime(JWT_EXPIRATION) .sign(getSecretKey()); return token; } export async function verifyToken(token: string): Promise { if (tokenBlacklist.has(token)) { return null; } const cached = tokenCache.get(token); if (cached) { const age = Date.now() - cached.timestamp; if (age < TOKEN_CACHE_TTL) { return cached.payload; } else { tokenCache.delete(token); } } try { const { payload } = await jwtVerify(token, getSecretKey(), { issuer: JWT_ISSUER, audience: JWT_AUDIENCE, }); const jwtPayload: JWTPayload = { sub: payload.sub as string, userId: payload.userId as number, role: payload.role as UserRole, iat: payload.iat as number, exp: payload.exp as number, iss: payload.iss as string, aud: payload.aud as string, }; tokenCache.set(token, { payload: jwtPayload, timestamp: Date.now() }); return jwtPayload; } catch (error) { return null; } } export function revokeToken(token: string): void { tokenBlacklist.add(token); tokenCache.delete(token); console.log("[auth] Token revoked"); } export async function cleanupBlacklist(): Promise { const now = Math.floor(Date.now() / 1000); let cleaned = 0; for (const token of tokenBlacklist) { try { const { payload } = await jwtVerify(token, getSecretKey(), { issuer: JWT_ISSUER, audience: JWT_AUDIENCE, }); if (payload.exp && payload.exp < now) { tokenBlacklist.delete(token); cleaned++; } } catch { tokenBlacklist.delete(token); cleaned++; } } const cacheNow = Date.now(); let cacheCleared = 0; for (const [token, entry] of tokenCache.entries()) { if (cacheNow - entry.timestamp > TOKEN_CACHE_TTL * 2) { tokenCache.delete(token); cacheCleared++; } } if (cleaned > 0 || cacheCleared > 0) { console.log( `[auth] Cleaned up ${cleaned} expired tokens from blacklist, ${cacheCleared} from cache`, ); } } setInterval(cleanupBlacklist, 60 * 60 * 1000); export async function authenticateUser( username: string, password: string, ): Promise { return await verifyPassword(username, password); } export function extractTokenFromHeader( authHeader: string | null, ): string | null { if (!authHeader || !authHeader.startsWith("Bearer ")) { return null; } return authHeader.substring(7); } export function extractTokenFromCookie( cookieHeader: string | null, ): string | null { if (!cookieHeader) { return null; } const cookies = cookieHeader.split(/;\s*/); for (const cookie of cookies) { const [name, value] = cookie.split("="); if (name === "overlord_token") { return value; } } return null; } export async function authenticateRequest( req: Request, ): Promise { const authHeader = req.headers.get("Authorization"); let token = extractTokenFromHeader(authHeader); if (!token) { const cookieHeader = req.headers.get("Cookie"); token = extractTokenFromCookie(cookieHeader); } if (!token) { return null; } const payload = await verifyToken(token); if (!payload) { return null; } const user = getUserById(payload.userId); if (!user) { return null; } return { username: payload.sub, userId: payload.userId, role: payload.role, }; } export async function getUserFromRequest( req: Request, ): Promise { return await authenticateRequest(req); } export function extractTokenFromRequest(req: Request): string | null { const authHeader = req.headers.get("Authorization"); let token = extractTokenFromHeader(authHeader); if (!token) { const cookieHeader = req.headers.get("Cookie"); token = extractTokenFromCookie(cookieHeader); } return token; }