// MFA ticket binding tests — the second login step's tickets are bound to the // issuing request's IP + User-Agent (hashed) so a stolen ticket replayed from // a different origin can't complete the second factor. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { issueTicket, redeemTicket } from '../../src/auth/mfa-tickets.js'; test('ticket round-trips when ip + userAgent match', () => { const id = issueTicket('user-1', { ip: '1.2.3.4', userAgent: 'curl/8' }); assert.equal(redeemTicket(id, { ip: '1.2.3.4', userAgent: 'curl/8' }), 'user-1'); }); test('ticket rejects redemption from a different IP', () => { const id = issueTicket('user-1', { ip: '1.2.3.4', userAgent: 'curl/8' }); assert.equal(redeemTicket(id, { ip: '9.9.9.9', userAgent: 'curl/8' }), null); }); test('ticket rejects redemption with a different User-Agent', () => { const id = issueTicket('user-1', { ip: '1.2.3.4', userAgent: 'curl/8' }); assert.equal(redeemTicket(id, { ip: '1.2.3.4', userAgent: 'Mozilla/5.0' }), null); }); test('ticket is single-use even on binding mismatch', () => { // A wrong-binding probe must still burn the ticket — otherwise an attacker // could try multiple IPs/UAs against the same ticket. const id = issueTicket('user-1', { ip: '1.2.3.4', userAgent: 'curl/8' }); assert.equal(redeemTicket(id, { ip: '9.9.9.9', userAgent: 'curl/8' }), null); // Same ticket with correct bindings now also fails — it was consumed. assert.equal(redeemTicket(id, { ip: '1.2.3.4', userAgent: 'curl/8' }), null); }); test('redeemTicket returns null for missing or unknown id', () => { assert.equal(redeemTicket(null), null); assert.equal(redeemTicket(undefined), null); assert.equal(redeemTicket(''), null); assert.equal(redeemTicket('not-a-real-id', { ip: 'x', userAgent: 'y' }), null); }); test('ticket is single-use on success', () => { const id = issueTicket('user-1', { ip: '1.2.3.4', userAgent: 'curl/8' }); assert.equal(redeemTicket(id, { ip: '1.2.3.4', userAgent: 'curl/8' }), 'user-1'); assert.equal(redeemTicket(id, { ip: '1.2.3.4', userAgent: 'curl/8' }), null); }); test('issueTicket without bindings still works (back-compat / tests)', () => { const id = issueTicket('user-1'); // No bindings on redeem either — both sides skip the check. assert.equal(redeemTicket(id), 'user-1'); });