---
id: w4/m11/t003
title: E2E on mock — TOTP enroll → challenge → recovery codes, scripted
worker: worker4
status: done
estimate: 35m
depends_on: [w4/m11/t002]
---

## Objective

Script the whole second-factor lifecycle against the mock cluster so it's regression-checkable: create a user, enroll TOTP through the settings API (codes generated with an otplib-style library), log out, verify login now requires a valid code (and rejects an invalid one), then burn a recovery code to get in "without the device". Exit 0 = pass, matching the `scripts/auth-e2e.sh` convention.

## Context

- **Precondition (2026-07-11):** the mock-cluster target assumes a schedulable auth substrate — post-m19 the base values pin it to `bex.co/pool=platform`, a label no CAPD mock node carries; blocked on the `w1` inbox note on mock pool-labels, or run the script against prod (`https://auth.bex.co`) with a disposable test identity.
- `scripts/auth-e2e.sh` (w4/m3/m4 era) is the pattern: curl-driven Kratos flows against the mock cluster; extend it or add a sibling `scripts/auth-mfa-e2e.sh`.
- TOTP codes are computable from the enrollment secret (the settings flow exposes it alongside the QR) — no browser needed for the TOTP path; the passkey path stays a manual/browser check (t002) since WebAuthn ceremonies don't script over curl.

## Steps

1. Script: signup → settings flow → enroll TOTP (capture secret, confirm with a generated code).
2. Login flow: password → assert aal2 step → wrong code rejected → right code accepted.
3. Recovery codes: generate, log in using one, assert single-use.

## Files

- `scripts/auth-mfa-e2e.sh` (or extension of `scripts/auth-e2e.sh`)

## Acceptance criteria

- [ ] Script exits 0 on the mock cluster and fails loudly on each broken step (wrong-code acceptance, reusable recovery code).

## Out of scope

- Passkey automation (manual browser check in t002); CI wiring beyond local runnability.
