dragonflight/services/mam-api/src/auth/mfa-tickets.js

38 lines
1.4 KiB
JavaScript
Raw Normal View History

// 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;
}