import { recoveryApp } from "./recovery.ts"; import { Hono } from "jsr:@hono/hono@4"; import type { Context } from "jsr:@hono/hono@4"; import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from "jsr:@simplewebauthn/server@13"; import type { AuthenticationResponseJSON, RegistrationResponseJSON, } from "jsr:@simplewebauthn/server@13"; import { deleteCookie, getCookie, setCookie } from "jsr:@hono/hono@4/cookie"; import { decodeBase64Url, encodeBase64Url, } from "jsr:@std/encoding@1/base64url"; import { MetadataService } from "jsr:@simplewebauthn/server@13"; import { initDb, sqlWrapper } from "./db.ts"; import { pingValkey, valkey } from "./valkey.ts"; import { rateLimitWrapper } from "./ratelimit.ts"; import { auditWrapper } from "./audit.ts"; import { spireWrapper } from "./spire_ffi.ts"; import { AuthService } from "../sdk/gen/auth_connect.ts"; import { universalServerRequestFromFetch, universalServerResponseToFetch, } from "npm:@connectrpc/connect@^1.4.0/protocol"; import type { ConnectRouter } from "npm:@connectrpc/connect@^1.4.0"; import { uiApp } from "../ui/mod.ts"; type Variables = { userId: string; }; export const app: Hono<{ Variables: Variables }> = new Hono< { Variables: Variables } >(); // Mount UI Routes app.route("/", uiApp); const rpName = "Auth-Yes Identity Provider"; const rpID = Deno.env.get("RP_ID") || (import.meta.main ? undefined : "localhost"); const origin = Deno.env.get("ORIGIN") || (import.meta.main ? undefined : "http://localhost"); const requireHardwareToken = Deno.env.get("REQUIRE_HARDWARE_TOKEN") === "true"; if (!rpID || !origin) { throw new Error( "Missing critical environment variables: RP_ID and ORIGIN must be set.", ); } // Auth API Gateway for Zero-Trust Identity Provider // Exposes REST and gRPC endpoints exclusively to the internal Docker network. if (import.meta.main) { console.log("[Auth API] Initializing FIDO MDS3 Metadata Blob..."); try { // SIDE EFFECT: Fetch MDS3 metadata dynamically on startup await MetadataService.initialize(); console.log("[Auth API] FIDO MDS3 Metadata Blob successfully loaded."); } catch (error) { console.error( "[Auth API] Fatal Error: Failed to initialize FIDO MDS3 Metadata Blob:", ); console.error(error); Deno.exit(1); } try { // SIDE EFFECT: Initialize database schema on startup await initDb(); } catch (error) { console.error( "[Auth API] Fatal Error: Failed to initialize Database Schema:", error, ); console.error(error); Deno.exit(1); } } function generateSessionId() { return crypto.randomUUID(); } // --------------------------------------------------------- // Middleware // --------------------------------------------------------- app.use("*", async (_c, next) => { // TODO: Add mTLS verification or internal network constraints await next(); }); // Middleware helper to get IP address function getClientIp(c: Context): string { // Check for X-Real-IP first const realIp = c.req.header("x-real-ip"); if (realIp) { return realIp.trim(); } // Fallback to X-Forwarded-For, bounded to prevent memory exhaustion let forwardedFor = c.req.header("x-forwarded-for"); if (forwardedFor) { // Truncate to a max of 256 characters if (forwardedFor.length > 256) { forwardedFor = forwardedFor.substring(0, 256); } const parts = forwardedFor.split(","); // Extract the last untrusted hop (right-most IP) return parts[parts.length - 1].trim(); } // Fallback (might not be accurate behind proxy without X-Forwarded-For) return "unknown-ip"; } // Rate Limiting Middlewares // Public Endpoints (/api/login/*, /api/register/*): 10 requests per minute by IP app.use("/api/login/*", async (c, next) => { const ip = getClientIp(c); const key = `ratelimit:public:${ip}`; const allowed = await rateLimitWrapper.checkRateLimit(key, 10, 60000); if (!allowed) { return c.json({ error: "Too Many Requests" }, 429); } await next(); }); app.use("/api/register/*", async (c, next) => { const ip = getClientIp(c); const key = `ratelimit:public:${ip}`; const allowed = await rateLimitWrapper.checkRateLimit(key, 10, 60000); if (!allowed) { return c.json({ error: "Too Many Requests" }, 429); } await next(); }); // Admin Endpoints (/api/admin/*): 60 requests per minute by user_id app.use("/api/admin/*", async (c, next) => { const auth = await getAuthenticatedUser(c); if (!auth) { return c.json({ error: "Missing or invalid session" }, 401); } const key = `ratelimit:admin:${auth.userId}`; const allowed = await rateLimitWrapper.checkRateLimit(key, 60, 60000); if (!allowed) { return c.json({ error: "Too Many Requests" }, 429); } // Stash userId in context so downstream routes can use it c.set("userId", auth.userId); await next(); }); // --------------------------------------------------------- // Provisioning & Registration (Use Cases 1, 2, 3) // --------------------------------------------------------- app.post("/api/admin/invites/create", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden: Global admin access required" }, 403); } const { appId, role, expiresInDays, customCode, usageLimitType, maxUses, autoActivate, } = await c.req.json(); const assignedRole = (typeof role === "string" && role.trim()) ? role.trim() : "user"; let validatedAppId = null; if (appId) { const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; if (typeof appId !== "string" || !uuidRegex.test(appId)) { return c.json({ error: "appId must be a valid UUID string" }, 400); } const appExists = await sqlWrapper .sql`SELECT id FROM apps WHERE id = ${appId}`.then( (res: any) => res[0], ); if (!appExists) { return c.json({ error: "Target application does not exist" }, 404); } validatedAppId = appId; } let parsedMaxUses: number | null = 1; if (usageLimitType === "unlimited") { parsedMaxUses = null; } else if (usageLimitType === "limited") { const n = parseInt(maxUses); parsedMaxUses = (!isNaN(n) && n > 0) ? n : 5; } else { parsedMaxUses = 1; // single-use default } const shouldAutoActivate = autoActivate !== false; const inviteCode = (customCode && typeof customCode === "string" && customCode.trim()) ? customCode.trim() : encodeBase64Url(crypto.getRandomValues(new Uint8Array(24))); const days = Number(expiresInDays) || 7; if (!Number.isInteger(days) || days < 1 || days > 30) { return c.json({ error: "expiresInDays must be an integer between 1 and 30", }, 400); } const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + days); try { await sqlWrapper.sql` INSERT INTO invites (code, app_id, role, created_by, max_uses, uses_count, auto_activate, expires_at) VALUES (${inviteCode}, ${validatedAppId}, ${assignedRole}, ${auth.userId}, ${parsedMaxUses}, 0, ${shouldAutoActivate}, ${expiresAt}) `; } catch (err: any) { if (err.code === "23505") { return c.json({ error: "An invite with this code already exists" }, 409); } return c.json({ error: "Failed to generate invite" }, 500); } auditWrapper.auditLog( auth.userId, "invite_created", validatedAppId, { code: inviteCode, role: assignedRole, max_uses: parsedMaxUses, auto_activate: shouldAutoActivate, expiresInDays: days, }, getClientIp(c), ); return c.json({ success: true, inviteCode, expiresAt, maxUses: parsedMaxUses, autoActivate: shouldAutoActivate, }); }); app.get("/.well-known/webauthn", (c) => { if (!origin) { return c.json({ origins: [] }); } return c.json({ origins: [origin] }); }); // Start a WebAuthn registration ceremony app.post("/api/register/challenge", async (c) => { const { username, inviteCode } = await c.req.json(); if (!username || !inviteCode) { return c.json({ error: "Username and inviteCode required" }, 400); } // Validate invite code early (checks expiration and max_uses bounds) const invite = await sqlWrapper .sql`SELECT id, max_uses, uses_count, auto_activate FROM invites WHERE code = ${inviteCode} AND (max_uses IS NULL OR uses_count < max_uses) AND expires_at > NOW()` .then((res: any) => res[0]); if (!invite) { return c.json( { error: "Invalid, expired, or fully claimed invite code" }, 400, ); } // Prevent hijacking an existing user's account if they already exist const existingUser = await sqlWrapper .sql`SELECT id FROM users WHERE username = ${username}`.then((res: any) => res[0] ); if (existingUser) { return c.json({ error: "Username already exists" }, 409); } const newUserId = crypto.randomUUID(); const userIdBytes = new TextEncoder().encode(newUserId); const options = await generateRegistrationOptions({ rpName, rpID, userName: username, userID: userIdBytes, attestationType: "direct", authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "preferred", }, timeout: 60000, extensions: { ["prf" as string]: {}, } as any, }); setCookie(c, "expected_registration_challenge", options.challenge, { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 300, }); setCookie(c, "registration_user_id", newUserId, { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 300, }); return c.json({ options, username }); }); // Verify registration and create UUID/session app.post("/api/register/verify", async (c) => { try { const { response, username, inviteCode } = await c.req.json(); if (!inviteCode) { return c.json({ error: "inviteCode required" }, 400); } const expectedChallenge = getCookie(c, "expected_registration_challenge"); const registrationUserId = getCookie(c, "registration_user_id"); if (!expectedChallenge || !registrationUserId) { return c.json({ error: "Missing or expired registration challenge/user ID", }, 400); } // Prevent race condition account hijacking let user = await sqlWrapper .sql`SELECT id FROM users WHERE username = ${username}` .then( (res: any) => res[0], ); if (user) { return c.json({ error: "Username already exists" }, 409); } let verification; try { verification = await verifyRegistrationResponse({ response: response as RegistrationResponseJSON, expectedChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: false, }); } catch (error: any) { return c.json({ error: error.message }, 400); } const { verified, registrationInfo } = verification; if (!verified || !registrationInfo) { return c.json({ error: "Verification failed" }, 400); } // Enterprise Allow-List Verification const allowlistCount = await sqlWrapper .sql`SELECT COUNT(*) as count FROM aaguid_allowlist`.then((res: any) => Number(res[0].count) ); if (allowlistCount > 0 && registrationInfo.aaguid) { const isAllowed = await sqlWrapper .sql`SELECT id FROM aaguid_allowlist WHERE aaguid = ${registrationInfo.aaguid}` .then((res: any) => res[0]); if (!isAllowed) { auditWrapper.auditLog(null, "failed_attestation_allowlist", null, { aaguid: registrationInfo.aaguid, }, getClientIp(c)); return c.json({ error: "Authenticator AAGUID is not in the enterprise allow-list.", }, 403); } } // Optional Strict Hardware Attestation (e.g. YubiKey-only) if (requireHardwareToken) { if ( !registrationInfo.aaguid || registrationInfo.aaguid === "00000000-0000-0000-0000-000000000000" ) { auditWrapper.auditLog(null, "registration_failed_attestation", null, { username, reason: "No AAGUID provided", }, getClientIp(c)); return c.json({ error: "Hardware attestation failed: No AAGUID provided. Only certified hardware security keys are permitted.", }, 403); } let mdsStatement; try { mdsStatement = await MetadataService.getStatement( registrationInfo.aaguid, ); } catch (mdsError) { console.warn("[Auth API] MetadataService lookup error:", mdsError); } if (!mdsStatement) { auditWrapper.auditLog(null, "registration_failed_attestation", null, { username, aaguid: registrationInfo.aaguid, reason: "AAGUID not found in MDS3", }, getClientIp(c)); return c.json({ error: `Hardware attestation failed: Authenticator AAGUID (${registrationInfo.aaguid}) not found in FIDO MDS3 blob. Only certified hardware security keys are permitted.`, }, 403); } // @ts-ignore: TypeScript definition might be out of date for FIDO MDS3 (1) if (mdsStatement.keyProtection?.includes(0x0001)) { auditWrapper.auditLog(null, "registration_failed_attestation", null, { username, aaguid: registrationInfo.aaguid, reason: "Software passkey detected", }, getClientIp(c)); return c.json({ error: "Hardware attestation failed: Authenticator is flagged as a software-based passkey. Only certified hardware security keys are permitted.", }, 403); } } const credentialID = registrationInfo.credential.id; const credentialPublicKey = registrationInfo.credential.publicKey; const counter = registrationInfo.credential.counter; const base64CredentialID = typeof credentialID === "string" ? credentialID : encodeBase64Url(new Uint8Array(credentialID as unknown as ArrayBuffer)); const base64PublicKey = encodeBase64Url( new Uint8Array(credentialPublicKey as unknown as ArrayBuffer), ); const prfEnabled = (response.clientExtensionResults as any)?.prf?.enabled === true; let prfSalt = null; if (prfEnabled) { const saltBytes = crypto.getRandomValues(new Uint8Array(32)); prfSalt = encodeBase64Url(saltBytes); } // Validate invite code at verification time to prevent race conditions const invite = await sqlWrapper .sql`SELECT id, app_id, role, max_uses, uses_count, auto_activate FROM invites WHERE code = ${inviteCode} AND (max_uses IS NULL OR uses_count < max_uses) AND expires_at > NOW()` .then((res: any) => res[0]); if (!invite) { return c.json( { error: "Invalid, expired, or fully claimed invite code" }, 400, ); } const initialStatus = invite.auto_activate === false ? "pending" : "active"; const insertRes = await sqlWrapper .sql`INSERT INTO users (id, username, account_status) VALUES (${registrationUserId}, ${username}, ${initialStatus}) RETURNING id`; user = insertRes[0]; await sqlWrapper.sql` INSERT INTO passkeys (user_id, credential_id, public_key, counter, prf_enabled, prf_salt) VALUES (${user.id}, ${base64CredentialID}, ${base64PublicKey}, ${counter}, ${prfEnabled}, ${prfSalt}) `; await sqlWrapper.sql` UPDATE invites SET uses_count = uses_count + 1, used_at = NOW(), used_by = ${user.id} WHERE id = ${invite.id} `; await sqlWrapper.sql` INSERT INTO invite_redemptions (invite_id, user_id) VALUES (${invite.id}, ${user.id}) `; if (invite.app_id) { await sqlWrapper.sql` INSERT INTO grants (user_id, app_id, role) VALUES (${user.id}, ${invite.app_id}, ${invite.role}) `; } else if (invite.role === "admin") { const adminApp = await sqlWrapper .sql`SELECT id FROM apps WHERE name = 'Auth-Yes Management Console'` .then((res: any) => res[0]); if (adminApp) { await sqlWrapper.sql` INSERT INTO grants (user_id, app_id, role) VALUES (${user.id}, ${adminApp.id}, 'admin') ON CONFLICT (user_id, app_id) DO UPDATE SET role = 'admin' `; } } auditWrapper.auditLog( user.id, "user_registered", null, { username, inviteCode }, getClientIp(c), ); // Set response cookie to clear out the challenge setCookie(c, "expected_registration_challenge", "", { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 0, }); setCookie(c, "registration_user_id", "", { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 0, }); const cookieDomain = Deno.env.get("COOKIE_DOMAIN") || (rpID && rpID.includes(".") ? `.${rpID}` : undefined); if (cookieDomain) { deleteCookie(c, "session_id", { domain: cookieDomain, path: "/" }); } deleteCookie(c, "session_id", { path: "/" }); return c.json({ success: true }); } catch (error: any) { console.error( "[Auth API] Uncaught Exception in /api/register/verify:", error, ); return c.json({ error: error.message || "Internal server error" }, 500); } }); // --------------------------------------------------------- // Authentication (Login) // --------------------------------------------------------- // Start a WebAuthn authentication ceremony app.post("/api/login/challenge", async (c) => { let body; try { body = await c.req.json(); } catch (_err) { body = {}; } const username = body.username; let extensions: any = undefined; if (username) { const user = await sqlWrapper.sql`SELECT id FROM users WHERE username = ${username}`.then((res: any) => res[0]); if (user) { const passkeys = await sqlWrapper.sql`SELECT credential_id, prf_enabled, prf_salt FROM passkeys WHERE user_id = ${user.id} AND prf_enabled = true AND prf_salt IS NOT NULL`; if (passkeys.length > 0) { extensions = { ["prf" as string]: { evalByCredential: {} } }; for (const pk of passkeys) { const saltBytes = decodeBase64Url(pk.prf_salt); extensions["prf"]["evalByCredential"][pk.credential_id] = { first: saltBytes, }; } } } } const options = await generateAuthenticationOptions({ rpID, userVerification: "preferred", timeout: 60000, extensions, }); setCookie(c, "expected_authentication_challenge", options.challenge, { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 300, }); return c.json({ options }); }); // Verify login and issue session app.post("/api/login/verify", async (c) => { const { response } = await c.req.json(); const expectedChallenge = getCookie(c, "expected_authentication_challenge"); if (!expectedChallenge) { return c.json( { error: "Missing or expired authentication challenge" }, 400, ); } const base64CredentialID = response.id; const passkey = await sqlWrapper .sql`SELECT * FROM passkeys WHERE credential_id = ${base64CredentialID}` .then((res: any) => res[0]); if (!passkey) { return c.json({ error: "Passkey not found. Please register your passkey first.", }, 404); } const user = await sqlWrapper .sql`SELECT id, username, account_status FROM users WHERE id = ${passkey.user_id}` .then((res: any) => res[0]); if (!user) { return c.json({ error: "User not found" }, 404); } const userId = user.id; if (user.account_status !== "active") { auditWrapper.auditLog( userId, "login_failed", null, { reason: `Account status is ${user.account_status}` }, getClientIp(c), ); return c.json({ error: "Account is not active. Please contact an administrator.", }, 403); } const publicKeyBytes = decodeBase64Url(passkey.public_key); let verification; try { verification = await verifyAuthenticationResponse({ response: response as AuthenticationResponseJSON, expectedChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: false, credential: { id: passkey.credential_id, publicKey: publicKeyBytes, counter: Number(passkey.counter), }, }); } catch (error: any) { return c.json({ error: error.message }, 400); } const { verified, authenticationInfo } = verification; if (!verified || !authenticationInfo) { auditWrapper.auditLog( userId, "login_failed", null, { reason: "verification failed" }, getClientIp(c), ); return c.json({ error: "Verification failed" }, 400); } await sqlWrapper .sql`UPDATE passkeys SET counter = ${authenticationInfo.newCounter} WHERE id = ${passkey.id}`; const sessionId = generateSessionId(); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // Persistence in PostgreSQL await sqlWrapper .sql`INSERT INTO sessions (id, user_id, expires_at) VALUES (${sessionId}, ${user.id}, ${expiresAt})`; // Write session to Valkey with TTL matching expiresAt const ttlSeconds = Math.floor((expiresAt.getTime() - Date.now()) / 1000); try { const sessionData = JSON.stringify({ uuid: user.id, username: user.username, }); await valkey.setex(sessionId, ttlSeconds, sessionData); } catch (_err: unknown) { // If Valkey fails, log and fail closed for security auditWrapper.auditLog( user.id, "login_failed", null, { reason: "Cache write failure" }, getClientIp(c), ); return c.json({ error: "Internal server error" }, 500); } const oldSessionId = getCookie(c, "session_id"); if (oldSessionId) { try { await valkey.del(oldSessionId); await sqlWrapper.sql`DELETE FROM sessions WHERE id = ${oldSessionId}`; } catch (_e) { // Best effort cleanup } } const cookieDomain = Deno.env.get("COOKIE_DOMAIN") || (rpID && rpID.includes(".") ? `.${rpID}` : undefined); // Clear any existing host-scoped cookie to prevent domain duplication deleteCookie(c, "session_id", { path: "/" }); setCookie(c, "session_id", sessionId, { domain: cookieDomain, httpOnly: true, secure: true, sameSite: "Lax", expires: expiresAt, }); setCookie(c, "expected_authentication_challenge", "", { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 0, }); auditWrapper.auditLog(userId, "login_success", null, null, getClientIp(c)); return c.json({ success: true }); }); // --------------------------------------------------------- // gRPC Services (Zero-Trust mTLS internal communication) // --------------------------------------------------------- const connectRoutes = (router: ConnectRouter) => { router.service(AuthService, { async validateSession(req, context) { try { const spiffeId = spireWrapper.extractSpiffeIdFromCert( context.requestHeader.get("x-peer-cert") || "", ); if (!spiffeId) { return { valid: false, uuid: "", scopes: [], error: "Validation failed", // Sanitized }; } // Check if the SPIFFE ID is a recognized application const appRecord = await sqlWrapper .sql`SELECT id FROM apps WHERE spiffe_id = ${spiffeId}`.then(( res: any, ) => res[0]); if (!appRecord) { auditWrapper.auditLog(null, "session_validation_failed", null, { reason: "Unauthorized SPIFFE ID", }, "internal-grpc"); return { valid: false, uuid: "", scopes: [], error: "Validation failed", // Sanitized }; } const token = req.token; if (!token) { return { valid: false, uuid: "", scopes: [], error: "Validation failed", }; // Sanitized } let sessionDataStr; try { sessionDataStr = await valkey.get(token); } catch (err: unknown) { console.error("Valkey error during validateSession:", err); return { valid: false, uuid: "", scopes: [], error: "Internal server error", }; } if (!sessionDataStr) { auditWrapper.auditLog( null, "session_validation_failed", appRecord.id, { reason: "Session invalid or expired", }, "internal-grpc", ); return { valid: false, uuid: "", scopes: [], error: "Validation failed", // Sanitized }; } let sessionData; try { sessionData = JSON.parse(sessionDataStr); } catch (err: unknown) { console.error("JSON parse error during validateSession:", err); return { valid: false, uuid: "", scopes: [], error: "Internal server error", // Sanitized }; } if (!sessionData || !sessionData.uuid) { return { valid: false, uuid: "", scopes: [], error: "Internal server error", // Sanitized }; } const userId = sessionData.uuid; // Check RBAC grant for the user and app const grantRecord = await sqlWrapper .sql`SELECT role FROM grants WHERE user_id = ${userId} AND app_id = ${appRecord.id}` .then((res: any) => res[0]); if (!grantRecord) { auditWrapper.auditLog( userId, "session_validation_failed", appRecord.id, { reason: "Access denied (RBAC)", }, "internal-grpc", ); return { valid: false, uuid: "", scopes: [], error: "Validation failed", }; // Sanitized } return { valid: true, uuid: userId, scopes: [grantRecord.role], error: "", }; } catch (err: unknown) { console.error("Unexpected error in validateSession:", err); return { valid: false, uuid: "", scopes: [], error: "Internal server error", }; } }, }); }; // Deno/Hono adapter using Universal Handlers import { createConnectRouter } from "npm:@connectrpc/connect@^1.4.0"; const router = createConnectRouter(); connectRoutes(router); // `createUniversalHandler` in ConnectRPC is actually exposed via `createConnectRouter().handlers` // which returns an array of `UniversalHandler`. We can map those to standard web fetch. const handlers = router.handlers; app.all("/auth.v1.AuthService/*", async (c) => { const url = new URL(c.req.url); const handler = handlers.find((h) => h.requestPath === url.pathname); if (!handler) { return new Response("Not Found", { status: 404 }); } const uReq = universalServerRequestFromFetch(c.req.raw, {}); const uRes = await handler(uReq); return universalServerResponseToFetch(uRes); }); // --------------------------------------------------------- // Session & Credential Management (Authenticated APIs) // --------------------------------------------------------- import { getAuthenticatedUser, isGlobalAdmin } from "./auth-session.ts"; // --------------------------------------------------------- // Global Admin APIs (For the Management Console) // --------------------------------------------------------- app.get("/api/admin/audit-logs", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden: Global admin access required" }, 403); } const logs = await sqlWrapper.sql` SELECT a.id, a.action, a.resource, a.details, a.ip_address, a.created_at, u.username as user FROM audit_records a LEFT JOIN users u ON a.user_id = u.id ORDER BY a.created_at DESC LIMIT 100 `; return c.json({ logs }); }); app.get("/api/admin/users", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden: Global admin access required" }, 403); } const users = await sqlWrapper.sql` SELECT id, username, display_name, account_status FROM users ORDER BY username ASC `; return c.json({ users }); }); app.post("/api/admin/users/:id/status", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden: Global admin access required" }, 403); } const targetUserId = c.req.param("id"); const { status } = await c.req.json(); if (!["active", "pending", "suspended"].includes(status)) { return c.json({ error: "Invalid status" }, 400); } const targetUser = await sqlWrapper .sql`UPDATE users SET account_status = ${status} WHERE id = ${targetUserId} RETURNING id` .then((res: any) => res[0]); if (!targetUser) { return c.json({ error: "User not found" }, 404); } auditWrapper.auditLog(auth.userId, "user_status_changed", targetUserId, { newStatus: status, }, getClientIp(c)); return c.json({ success: true }); }); // --------------------------------------------------------- // Traefik ForwardAuth Edge Proxy Route (Tier 2) // --------------------------------------------------------- import { getAppByHost, getUserGrant } from "./auth-session.ts"; import { computeJwkThumbprint, verifyHttpSignature, } from "./http_signatures.ts"; app.get("/api/forward-auth", async (c) => { const host = c.req.header("X-Forwarded-Host"); if (!host) { return c.text("Bad Request: Missing X-Forwarded-Host header", 400); } // 1. Resolve Target App (Valkey -> DB) const appRecord = await getAppByHost(host); if (!appRecord) { // Default-Deny if app is not registered return c.text("Forbidden: Application not registered", 403); } // 2. Validate Session OR HTTP Signature const signatureInput = c.req.header("Signature-Input"); const signature = c.req.header("Signature"); if (signatureInput && signature) { // Headless Edge Node Path (RFC 9421) try { const fingerprint = await verifyHttpSignature(c.req.raw); // Look up key name from postgres if needed, but fingerprint string manipulation is fast enough const serviceName = `service-node:${fingerprint.substring(0, 8)}`; const serviceId = fingerprint; const scopes = "edge-node,daemon"; c.header("X-Forwarded-User", serviceName); c.header("X-Forwarded-User-Id", serviceId); c.header("X-Forwarded-Scopes", scopes); c.header("X-Forwarded-App-Id", appRecord.id); return c.text("OK", 200); } catch (err: any) { return c.text(`Unauthorized: ${err.message}`, 401); } } // Standard User Session Path const auth = await getAuthenticatedUser(c); if (!auth) { return c.text("Unauthorized", 401); } // Cache lookup for user status can be added later; hitting DB to be safe for now, // but let's just make sure account is active. const user = await sqlWrapper.sql` SELECT id, username, account_status FROM users WHERE id = ${auth.userId} `.then((res: any) => res[0]); if (!user || user.account_status !== "active") { return c.text("Forbidden: Account inactive", 403); } // 3. Resolve Grants and Roles const globalAdmin = await isGlobalAdmin(auth.userId); const grantRole = await getUserGrant(auth.userId, appRecord.id); if (!globalAdmin && !grantRole) { // Enforce Default-Deny if no app-specific grants and not global admin return c.text("Forbidden: Access denied to this application", 403); } // Combine scopes, ensuring no duplicates and formatting as comma-separated string const scopes = [ ...new Set([grantRole, globalAdmin ? "admin" : null].filter(Boolean)), ].join(","); // 4. Inject Headers c.header("X-Forwarded-User", user.username); c.header("X-Forwarded-User-Id", user.id); c.header("X-Forwarded-Scopes", scopes); c.header("X-Forwarded-App-Id", appRecord.id); return c.text("OK", 200); }); // --------------------------------------------------------- // Admin Application Registry // --------------------------------------------------------- app.get("/api/admin/apps", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const apps = await sqlWrapper.sql` SELECT a.id, a.name, a.spiffe_id, a.description, a.created_at, COUNT(g.id) AS active_grants_count FROM apps a LEFT JOIN grants g ON a.id = g.app_id GROUP BY a.id, a.name, a.spiffe_id, a.description, a.created_at ORDER BY a.created_at ASC `; return c.json({ apps }); }); app.post("/api/admin/apps", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { name, spiffeId, description } = await c.req.json(); if (!name || !spiffeId) { return c.json({ error: "Name and SPIFFE ID are required" }, 400); } try { const newApp = await sqlWrapper.sql` INSERT INTO apps (name, spiffe_id, description) VALUES (${name.trim()}, ${spiffeId.trim()}, ${ description?.trim() || null }) RETURNING id, name, spiffe_id, description, created_at `.then((res: any) => res[0]); auditWrapper.auditLog(auth.userId, "app_registered", newApp.id, { name: newApp.name, spiffe_id: newApp.spiffe_id, }, getClientIp(c)); return c.json({ success: true, app: newApp }); } catch (err: any) { if (err.code === "23505") { return c.json({ error: "An application with this SPIFFE ID already exists", }, 409); } return c.json({ error: "Failed to register application" }, 500); } }); app.delete("/api/admin/apps/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const appId = c.req.param("id"); const app = await sqlWrapper .sql`DELETE FROM apps WHERE id = ${appId} RETURNING id, name` .then((res: any) => res[0]); if (app) { auditWrapper.auditLog( auth.userId, "app_deleted", appId, { name: app.name }, getClientIp(c), ); return c.json({ success: true }); } return c.json({ error: "Application not found" }, 404); }); // --------------------------------------------------------- // Admin Role Catalog Management // --------------------------------------------------------- app.get("/api/admin/roles", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const appId = c.req.query("appId"); let roles; if (appId) { roles = await sqlWrapper.sql` SELECT r.id, r.name, r.description, r.app_id, r.created_at, a.name AS app_name FROM roles r LEFT JOIN apps a ON r.app_id = a.id WHERE r.app_id IS NULL OR r.app_id = ${appId} ORDER BY r.app_id NULLS FIRST, r.name ASC `; } else { roles = await sqlWrapper.sql` SELECT r.id, r.name, r.description, r.app_id, r.created_at, a.name AS app_name FROM roles r LEFT JOIN apps a ON r.app_id = a.id ORDER BY r.app_id NULLS FIRST, r.name ASC `; } return c.json({ roles }); }); app.post("/api/admin/roles", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { name, description, appId } = await c.req.json(); if ( !name || typeof name !== "string" || name.trim().length < 2 || name.trim().length > 32 ) { return c.json( { error: "Role name must be between 2 and 32 characters" }, 400, ); } const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_"); let validatedAppId = null; if (appId && typeof appId === "string" && appId.trim()) { const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; if (!uuidRegex.test(appId)) { return c.json({ error: "Invalid App UUID" }, 400); } const appExists = await sqlWrapper .sql`SELECT id, name FROM apps WHERE id = ${appId}` .then((res: any) => res[0]); if (!appExists) { return c.json({ error: "Selected application does not exist" }, 404); } validatedAppId = appId; } try { const newRole = await sqlWrapper.sql` INSERT INTO roles (name, description, app_id) VALUES (${normalizedName}, ${ description?.trim() || null }, ${validatedAppId}) RETURNING id, name, description, app_id, created_at `.then((res: any) => res[0]); auditWrapper.auditLog(auth.userId, "role_created", validatedAppId, { role_name: newRole.name, scope: validatedAppId ? "app-specific" : "global", }, getClientIp(c)); return c.json({ success: true, role: newRole }); } catch (err: any) { if (err.code === "23505") { return c.json({ error: "This role already exists for the selected scope", }, 409); } return c.json({ error: "Failed to create role" }, 500); } }); app.delete("/api/admin/roles/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const roleId = c.req.param("id"); const role = await sqlWrapper .sql`SELECT id, name, app_id FROM roles WHERE id = ${roleId}`.then( (res: any) => res[0], ); if (!role) { return c.json({ error: "Role not found" }, 404); } if (role.name === "admin" && role.app_id === null) { return c.json({ error: "The global 'admin' role cannot be deleted" }, 400); } await sqlWrapper.sql`DELETE FROM roles WHERE id = ${roleId}`; auditWrapper.auditLog( auth.userId, "role_deleted", role.app_id, { role_name: role.name }, getClientIp(c), ); return c.json({ success: true }); }); // --------------------------------------------------------- // Admin Invites / Registration Tokens // --------------------------------------------------------- app.get("/api/admin/invites", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const invites = await sqlWrapper.sql` SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at, a.name AS app_name, a.id AS app_id, u.username AS used_by_username FROM invites i LEFT JOIN apps a ON i.app_id = a.id LEFT JOIN users u ON i.used_by = u.id ORDER BY i.created_at DESC `; return c.json({ invites }); }); app.get("/api/admin/invites/:id/redemptions", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const inviteId = c.req.param("id"); const redemptions = await sqlWrapper.sql` SELECT ir.id, ir.redeemed_at, u.id AS user_id, u.username, u.display_name, u.account_status FROM invite_redemptions ir JOIN users u ON ir.user_id = u.id WHERE ir.invite_id = ${inviteId} ORDER BY ir.redeemed_at DESC `; return c.json({ redemptions }); }); app.delete("/api/admin/invites/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const inviteId = c.req.param("id"); const invite = await sqlWrapper .sql`DELETE FROM invites WHERE id = ${inviteId} RETURNING id, code` .then((res: any) => res[0]); if (invite) { auditWrapper.auditLog( auth.userId, "invite_revoked", inviteId, { code: invite.code }, getClientIp(c), ); return c.json({ success: true }); } return c.json({ error: "Invite not found" }, 404); }); // --------------------------------------------------------- // Admin User RBAC Grants Management // --------------------------------------------------------- app.get("/api/admin/users/:id/grants", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const targetUserId = c.req.param("id"); const grants = await sqlWrapper.sql` SELECT g.id, g.app_id, g.role, g.created_at, a.name AS app_name, a.spiffe_id FROM grants g JOIN apps a ON g.app_id = a.id WHERE g.user_id = ${targetUserId} ORDER BY a.name ASC `; return c.json({ grants }); }); app.post("/api/admin/users/:id/grants", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const targetUserId = c.req.param("id"); const { appId, role } = await c.req.json(); if (!appId || !role) { return c.json({ error: "appId and role are required" }, 400); } const app = await sqlWrapper .sql`SELECT id, name FROM apps WHERE id = ${appId}`.then( (res: any) => res[0], ); if (!app) return c.json({ error: "Application not found" }, 404); const targetUser = await sqlWrapper .sql`SELECT id, username FROM users WHERE id = ${targetUserId}`.then( (res: any) => res[0], ); if (!targetUser) return c.json({ error: "User not found" }, 404); await sqlWrapper.sql` INSERT INTO grants (user_id, app_id, role) VALUES (${targetUserId}, ${appId}, ${role}) ON CONFLICT (user_id, app_id) DO UPDATE SET role = ${role} `; auditWrapper.auditLog( auth.userId, "user_grant_assigned", targetUserId, { app_id: appId, app_name: app.name, role }, getClientIp(c), ); return c.json({ success: true }); }); app.delete("/api/admin/users/:id/grants/:appId", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { id: targetUserId, appId } = c.req.param(); const grant = await sqlWrapper.sql` DELETE FROM grants WHERE user_id = ${targetUserId} AND app_id = ${appId} RETURNING id `.then((res: any) => res[0]); if (grant) { auditWrapper.auditLog( auth.userId, "user_grant_revoked", targetUserId, { app_id: appId }, getClientIp(c), ); return c.json({ success: true }); } return c.json({ error: "Grant not found" }, 404); }); // --------------------------------------------------------- // Admin AAGUID Management // --------------------------------------------------------- app.get("/api/admin/aaguid", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const allowlist = await sqlWrapper .sql`SELECT id, aaguid, description, created_at FROM aaguid_allowlist ORDER BY created_at DESC`; return c.json({ allowlist }); }); app.post("/api/admin/aaguid", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { aaguid, description } = await c.req.json(); const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i; if (!aaguid || !uuidRegex.test(aaguid)) { return c.json({ error: "Valid AAGUID (UUID) is required" }, 400); } try { await sqlWrapper .sql`INSERT INTO aaguid_allowlist (aaguid, description) VALUES (${aaguid.toLowerCase()}, ${ description || null })`; auditWrapper.auditLog( auth.userId, "aaguid_added", null, { aaguid }, getClientIp(c), ); return c.json({ success: true }); } catch (err: any) { if (err.code === "23505") { return c.json({ error: "AAGUID already exists" }, 409); } return c.json({ error: "Internal server error" }, 500); } }); app.delete("/api/admin/aaguid/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const id = c.req.param("id"); const record = await sqlWrapper .sql`DELETE FROM aaguid_allowlist WHERE id = ${id} RETURNING aaguid` .then((res: any) => res[0]); if (record) { auditWrapper.auditLog( auth.userId, "aaguid_removed", null, { aaguid: record.aaguid }, getClientIp(c), ); } return c.json({ success: true }); }); // --------------------------------------------------------- // Admin HWK (Header Web Key) Management // --------------------------------------------------------- app.post("/api/admin/hwk", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { jwk, name } = await c.req.json(); if (!jwk || !name || typeof name !== "string") { return c.json({ error: "Missing required fields: jwk, name" }, 400); } if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) { return c.json({ error: "Invalid JWK: Must be an Ed25519 OKP key" }, 400); } try { const fingerprint = await computeJwkThumbprint(jwk); // 1. Dual Storage: PostgreSQL (Durability) await sqlWrapper.sql` INSERT INTO hwk_keys (fingerprint, public_key, name) VALUES (${fingerprint}, ${JSON.stringify(jwk)}, ${name}) `; // 2. Dual Storage: Valkey (O(1) Verification) await valkey.sadd("auth:hwk:fingerprints", fingerprint); auditWrapper.auditLog( auth.userId, "hwk_added", null, { fingerprint, name }, getClientIp(c), ); return c.json({ success: true, fingerprint }, 201); } catch (err: any) { if (err.code === "23505") { return c.json({ error: "This key has already been registered" }, 409); } console.error("Failed to add HWK:", err); return c.json({ error: "Internal server error" }, 500); } }); app.delete("/api/admin/hwk/:fingerprint", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const fingerprint = c.req.param("fingerprint"); // 1. Remove from PostgreSQL const record = await sqlWrapper.sql` DELETE FROM hwk_keys WHERE fingerprint = ${fingerprint} RETURNING id, name `.then((res: any) => res[0]); if (record) { // 2. Remove from Valkey try { await valkey.srem("auth:hwk:fingerprints", fingerprint); } catch (_err) {} auditWrapper.auditLog(auth.userId, "hwk_removed", null, { fingerprint, name: record.name, }, getClientIp(c)); return c.json({ success: true }); } return c.json({ error: "Key not found" }, 404); }); // --------------------------------------------------------- // Global Session and Device Revocation (Admin) // --------------------------------------------------------- app.get("/api/admin/users/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const targetUserId = c.req.param("id"); const user = await sqlWrapper .sql`SELECT id, username, display_name, account_status FROM users WHERE id = ${targetUserId}` .then((res: any) => res[0]); if (!user) return c.json({ error: "User not found" }, 404); const sessions = await sqlWrapper .sql`SELECT id, created_at, expires_at FROM sessions WHERE user_id = ${targetUserId} ORDER BY created_at DESC`; const passkeys = await sqlWrapper .sql`SELECT id, credential_id, counter FROM passkeys WHERE user_id = ${targetUserId}`; return c.json({ user, sessions, passkeys }); }); app.delete("/api/admin/sessions/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const sessionId = c.req.param("id"); const session = await sqlWrapper .sql`DELETE FROM sessions WHERE id = ${sessionId} RETURNING user_id` .then((res: any) => res[0]); if (session) { try { await valkey.del(sessionId); } catch (_err) {} auditWrapper.auditLog( auth.userId, "admin_session_revoked", session.user_id, { revoked_session_id: sessionId, }, getClientIp(c), ); } return c.json({ success: true }); }); app.delete("/api/admin/users/:id/sessions", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const targetUserId = c.req.param("id"); const sessions = await sqlWrapper .sql`DELETE FROM sessions WHERE user_id = ${targetUserId} RETURNING id`; for (const session of sessions) { try { await valkey.del(session.id); } catch (_err) {} } auditWrapper.auditLog( auth.userId, "admin_all_sessions_revoked", targetUserId, null, getClientIp(c), ); return c.json({ success: true }); }); app.delete("/api/admin/users/:userId/passkeys/:passkeyId", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const { userId, passkeyId } = c.req.param(); const passkey = await sqlWrapper .sql`DELETE FROM passkeys WHERE id = ${passkeyId} AND user_id = ${userId} RETURNING id` .then((res: any) => res[0]); if (passkey) { auditWrapper.auditLog(auth.userId, "admin_passkey_revoked", userId, { passkey_id: passkey.id, }, getClientIp(c)); return c.json({ success: true }); } return c.json({ error: "Passkey not found" }, 404); }); // --------------------------------------------------------- // Out-of-Band Account Recovery (Use Case 12) // --------------------------------------------------------- app.post("/api/admin/users/:id/recovery", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); if (!(await isGlobalAdmin(auth.userId))) { return c.json({ error: "Forbidden" }, 403); } const targetUserId = c.req.param("id"); const targetUser = await sqlWrapper .sql`SELECT id FROM users WHERE id = ${targetUserId}` .then((res: any) => res[0]); if (!targetUser) return c.json({ error: "User not found" }, 404); const recoveryCode = encodeBase64Url( crypto.getRandomValues(new Uint8Array(24)), ); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 1); await sqlWrapper .sql`INSERT INTO recovery_links (code, user_id, created_by, expires_at) VALUES (${recoveryCode}, ${targetUserId}, ${auth.userId}, ${expiresAt})`; auditWrapper.auditLog( auth.userId, "recovery_link_created", targetUserId, null, getClientIp(c), ); return c.json({ success: true, recoveryCode, expiresAt }); }); app.route("/api/recovery", recoveryApp); app.get("/api/admin/check", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const isAdmin = await isGlobalAdmin(auth.userId); return c.json({ isAdmin }); }); // Get user's active sessions app.get("/api/sessions", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const sessions = await sqlWrapper.sql` SELECT id, created_at, expires_at FROM sessions WHERE user_id = ${auth.userId} AND expires_at > NOW() ORDER BY created_at DESC `; return c.json({ sessions, currentSessionId: auth.sessionId }); }); // Revoke a specific session app.delete("/api/sessions/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetSessionId = c.req.param("id"); // Verify the session belongs to the user const session = await sqlWrapper.sql` SELECT id FROM sessions WHERE id = ${targetSessionId} AND user_id = ${auth.userId} `.then((res: any) => res[0]); if (!session) { return c.json({ error: "Session not found or access denied" }, 404); } // Remove from Valkey try { await valkey.del(targetSessionId); } catch (err) { console.error("Failed to delete session from cache:", err); } // Remove from DB (or expire it immediately) await sqlWrapper.sql`DELETE FROM sessions WHERE id = ${targetSessionId}`; auditWrapper.auditLog(auth.userId, "session_revoked", null, { revoked_session_id: targetSessionId, }, getClientIp(c)); return c.json({ success: true }); }); // --------------------------------------------------------- // Authenticated Passkey Registration (Adding a new device) // --------------------------------------------------------- app.post("/api/passkeys/register/challenge", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const user = await sqlWrapper .sql`SELECT username FROM users WHERE id = ${auth.userId}` .then((res: any) => res[0]); if (!user) return c.json({ error: "User not found" }, 404); const userIdBytes = new TextEncoder().encode(auth.userId); const options = await generateRegistrationOptions({ rpName, rpID, userName: user.username, userID: userIdBytes, attestationType: "direct", authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "preferred", }, timeout: 60000, }); setCookie(c, "expected_add_passkey_challenge", options.challenge, { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 300, }); return c.json({ options }); }); app.post("/api/passkeys/register/verify", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const { response } = await c.req.json(); const expectedChallenge = getCookie(c, "expected_add_passkey_challenge"); if (!expectedChallenge) { return c.json({ error: "Missing or expired registration challenge" }, 400); } let verification; try { verification = await verifyRegistrationResponse({ response: response as RegistrationResponseJSON, expectedChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: false, }); } catch (error: any) { return c.json({ error: error.message }, 400); } const { verified, registrationInfo } = verification; if (!verified || !registrationInfo) { return c.json({ error: "Verification failed" }, 400); } // Enterprise Allow-List Verification const allowlistCount = await sqlWrapper .sql`SELECT COUNT(*) as count FROM aaguid_allowlist`.then((res: any) => Number(res[0].count) ); if (allowlistCount > 0 && registrationInfo.aaguid) { const isAllowed = await sqlWrapper .sql`SELECT id FROM aaguid_allowlist WHERE aaguid = ${registrationInfo.aaguid}` .then((res: any) => res[0]); if (!isAllowed) { auditWrapper.auditLog(auth.userId, "failed_attestation_allowlist", null, { aaguid: registrationInfo.aaguid, }, getClientIp(c)); return c.json({ error: "Authenticator AAGUID is not in the enterprise allow-list.", }, 403); } } // Optional Strict Hardware Attestation if (requireHardwareToken) { if ( !registrationInfo.aaguid || registrationInfo.aaguid === "00000000-0000-0000-0000-000000000000" ) { auditWrapper.auditLog( auth.userId, "add_passkey_failed_attestation", null, { reason: "No AAGUID provided", }, getClientIp(c), ); return c.json( { error: "Hardware attestation failed: No AAGUID provided." }, 403, ); } let mdsStatement; try { mdsStatement = await MetadataService.getStatement( registrationInfo.aaguid, ); } catch (mdsError) { console.warn("[Auth API] MetadataService lookup error:", mdsError); } if (!mdsStatement) { auditWrapper.auditLog( auth.userId, "add_passkey_failed_attestation", null, { aaguid: registrationInfo.aaguid, reason: "AAGUID not found in MDS3", }, getClientIp(c), ); return c.json({ error: `Hardware attestation failed: Authenticator AAGUID (${registrationInfo.aaguid}) not found in FIDO MDS3 blob.`, }, 403); } // @ts-ignore: FIDO MDS3 missing type if (mdsStatement.keyProtection?.includes(0x0001)) { auditWrapper.auditLog( auth.userId, "add_passkey_failed_attestation", null, { aaguid: registrationInfo.aaguid, reason: "Software passkey detected", }, getClientIp(c), ); return c.json({ error: "Hardware attestation failed: Authenticator is flagged as a software-based passkey.", }, 403); } } const credentialID = registrationInfo.credential.id; const credentialPublicKey = registrationInfo.credential.publicKey; const counter = registrationInfo.credential.counter; const base64CredentialID = typeof credentialID === "string" ? credentialID : encodeBase64Url(new Uint8Array(credentialID as unknown as ArrayBuffer)); const base64PublicKey = encodeBase64Url( new Uint8Array(credentialPublicKey as unknown as ArrayBuffer), ); await sqlWrapper.sql` INSERT INTO passkeys (user_id, credential_id, public_key, counter) VALUES (${auth.userId}, ${base64CredentialID}, ${base64PublicKey}, ${counter}) `; auditWrapper.auditLog( auth.userId, "passkey_added", null, null, getClientIp(c), ); setCookie(c, "expected_add_passkey_challenge", "", { httpOnly: true, secure: true, sameSite: "Lax", maxAge: 0, }); return c.json({ success: true }); }); // Get user's registered passkeys app.get("/api/passkeys", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const passkeys = await sqlWrapper.sql` SELECT id, counter FROM passkeys WHERE user_id = ${auth.userId} `; return c.json({ passkeys }); }); // Revoke a specific passkey app.delete("/api/passkeys/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const targetPasskeyId = c.req.param("id"); // Verify the passkey belongs to the user const passkey = await sqlWrapper.sql` SELECT id FROM passkeys WHERE id = ${targetPasskeyId} AND user_id = ${auth.userId} `.then((res: any) => res[0]); if (!passkey) { return c.json({ error: "Passkey not found or access denied" }, 404); } // Prevent deleting the very last passkey to avoid locking out the user const passkeyCount = await sqlWrapper.sql` SELECT count(*) as count FROM passkeys WHERE user_id = ${auth.userId} `.then((res: any) => Number(res[0].count)); if (passkeyCount <= 1) { return c.json({ error: "Cannot delete your last passkey. Register another one first.", }, 400); } await sqlWrapper.sql`DELETE FROM passkeys WHERE id = ${targetPasskeyId}`; auditWrapper.auditLog(auth.userId, "passkey_revoked", null, { revoked_passkey_id: targetPasskeyId, }, getClientIp(c)); return c.json({ success: true }); }); // --------------------------------------------------------- // Lifecycle & Revocation // --------------------------------------------------------- // Revoke a session manually (used by layout logout) app.post("/api/revoke", async (c) => { // Try Authorization header first (SDK) let token = ""; const authHeader = c.req.header("Authorization"); if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.split(" ")[1]; } else { // Fallback to session cookie (Web UI) token = getCookie(c, "session_id") || ""; } if (!token) { return c.json({ error: "Missing or invalid token" }, 401); } // SIDE EFFECT: Deletes the key in Valkey cache instantly try { await valkey.del(token); } catch (_err: unknown) { return c.json({ error: "Failed to revoke session from cache" }, 500); } // Best effort delete from postgres if it's a UUID style session id try { await sqlWrapper.sql`DELETE FROM sessions WHERE id = ${token}`; } catch (_e) { // ignore } const cookieDomain = Deno.env.get("COOKIE_DOMAIN") || (rpID && rpID.includes(".") ? `.${rpID}` : undefined); if (cookieDomain) { deleteCookie(c, "session_id", { domain: cookieDomain, path: "/", httpOnly: true, secure: true, sameSite: "Lax", }); } deleteCookie(c, "session_id", { path: "/", httpOnly: true, secure: true, sameSite: "Lax", }); return c.json({ success: true }); }); if (import.meta.main) { const PORT = parseInt(Deno.env.get("PORT") || "8000"); // Fail fast if connection to Valkey fails during startup await pingValkey(); console.log(`Auth API Server running on port ${PORT}`); Deno.serve({ port: PORT }, app.fetch); }