// Short-lived MFA tickets bridging the two login steps. // // When a user with TOTP enabled passes password auth, we don't create a session // yet — we hand back an opaque ticket. The second request (code or recovery // code) redeems the ticket to finish login. Tickets are single-use and expire // fast so a stolen ticket is near-useless. // // In-memory + single-instance, matching the existing login rate-limiter // (auth/rate-limit.js). Documented limitation: in a multi-instance deployment // the second step must hit the same node. Acceptable for Dragonflight's // one-mam-api-per-node shape; revisit if that changes. import { randomBytes } from 'node:crypto'; const TTL_MS = 5 * 60 * 1000; // 5 minutes to enter a code const tickets = new Map(); // id -> { userId, expiresAt } function sweep() { const now = Date.now(); for (const [id, t] of tickets) if (t.expiresAt <= now) tickets.delete(id); } export function issueTicket(userId) { sweep(); const id = randomBytes(32).toString('hex'); tickets.set(id, { userId, expiresAt: Date.now() + TTL_MS }); return id; } // Redeem (and consume) a ticket. Returns the userId, or null if missing/expired. export function redeemTicket(id) { if (!id) return null; const t = tickets.get(id); if (!t) return null; tickets.delete(id); // single-use if (t.expiresAt <= Date.now()) return null; return t.userId; }