// Journey: TOTP full lifecycle (enroll → verify → backup-codes → disable) // // Endpoints exercised: // GET /api/v1/members/me/totp (before) // POST /api/v1/members/me/totp/enroll-start // POST /api/v1/members/me/totp/enroll-confirm // POST /api/v1/members/me/totp/verify // POST /api/v1/members/me/totp/backup-codes (regenerate) // GET /api/v1/members/me/totp (mid; enrolled=true) // DELETE /api/v1/members/me/totp // GET /api/v1/members/me/totp (after; enrolled=false) // // TOTP code generation is local (lib/totp.js) — no external authenticator. import { sleep } from 'k6'; import { get, post, del, checkEnvelope } from '../lib/http.js'; import { registerAndConfirm } from '../lib/auth.js'; import { generateTOTP, parseOTPAuth } from '../lib/totp.js'; export const options = { vus: 1, iterations: 1, thresholds: { checks: ['rate==1.0'] }, }; export default function () { const { tokens } = registerAndConfirm(); const bearer = { Authorization: `Bearer ${tokens.access_token}` }; // 1. status before enroll const before = checkEnvelope(get('/api/v1/members/me/totp', bearer), 'GET /me/totp (before)').data; if (before.enrolled !== false) throw new Error('totp should be disabled initially'); // 2. enroll-start → get otpauth URL const enroll = checkEnvelope(post('/api/v1/members/me/totp/enroll-start', null, bearer), 'POST /me/totp/enroll-start').data; parseOTPAuth(enroll.otpauth_url); // validate it parses const code = generateTOTP(enroll.otpauth_url); // 3. enroll-confirm const confirm = checkEnvelope( post('/api/v1/members/me/totp/enroll-confirm', { code }, bearer), 'POST /me/totp/enroll-confirm', ).data; if (!Array.isArray(confirm.backup_codes) || confirm.backup_codes.length === 0) { throw new Error('enroll-confirm: backup_codes missing'); } // 4. status mid (enrolled=true) const mid = checkEnvelope(get('/api/v1/members/me/totp', bearer), 'GET /me/totp (mid)').data; if (mid.enrolled !== true) throw new Error('totp should be enrolled after confirm'); // 5. verify — wait until current TOTP differs from the one just consumed // so the replay guard (Member.TOTP.ReplayTTLSeconds) does not reject it. let verifyCode = generateTOTP(enroll.otpauth_url); for (let i = 0; verifyCode === code && i < 30; i++) { sleep(1.1); verifyCode = generateTOTP(enroll.otpauth_url); } if (verifyCode === code) { throw new Error('TOTP window did not advance; cannot avoid replay'); } checkEnvelope( post('/api/v1/members/me/totp/verify', { code: verifyCode }, bearer), 'POST /me/totp/verify', ); // 6. backup-codes (regenerate) const regen = checkEnvelope( post('/api/v1/members/me/totp/backup-codes', null, bearer), 'POST /me/totp/backup-codes', ).data; if (!Array.isArray(regen.backup_codes) || regen.backup_codes.length === 0) { throw new Error('backup-codes: missing codes'); } // 7. disable checkEnvelope(del('/api/v1/members/me/totp', null, bearer), 'DELETE /me/totp'); // 8. status after disable const after = checkEnvelope(get('/api/v1/members/me/totp', bearer), 'GET /me/totp (after)').data; if (after.enrolled !== false) throw new Error('totp should be disabled after DELETE'); }