Contract-freeze · UX approval

Account security — Two-factor authentication · Approval Frames

These frames demonstrate the MFA management experience before any feature UI is implemented: enrolling an authenticator app (TOTP), adding a phone for text-message codes, one-time recovery codes, removing a method, and the step-up challenge shown mid sign-in. Approving them freezes the visual + interaction contract the implementation and frontend-test-engineer's Playwright checks are verified against. Built in the fuse-seam design system (tokens only). No provider name appears anywhere in the UI.

Reached from the Account security hub (sibling feature) → Two-factor authentication. Route base /account/security/mfa. The sign-in step-up lives on the login path /login/mfa.

Walk the flow

Frame 01 · /account/security/mfa

Two-factor overview

Enrolled methods list, add-a-method menu, recovery-codes status. Authenticator is available; text-message codes are offered only when the channel can deliver.

populatedempty (no MFA)loadingload error
Frame 02 · …/mfa/add/authenticator

Add an authenticator app

Scan the QR (or copy the setup key), then confirm with a 6-digit code. Fail-closed on a wrong or expired code.

scan + verifyinvalid codeexpired
Frame 03 · …/mfa/add/phone

Add a phone (text message)

Enter a phone, receive a code, confirm. Honest degrade: when text messages can't be delivered the option is disabled with a reason — never offered as if it works.

enter + confirmchannel unavailableinvalid code
Frame 04 · …/mfa/recovery-codes

Recovery codes

Shown once, never retrievable again. Returning visits show counts only (GET /mfa/recovery-codes → {remaining, total}) — no code tiles, because the API never returns codes again.

shown onceloadingN of M leftrunning lowall usednever generatedcount load error + retryregenerate confirm375px
Frame 05 · /login/mfa

Step-up challenge (mid sign-in)

Login returned mfa_required. Pick a factor, enter the code, complete sign-in. Fail-closed on bad/expired code; delivery failure is surfaced.

TOTP promptchoose factorrecovery not offeredinvalid / expiredtoo many attempts (429)delivery failed
Frame 06 · /account/security/mfa

Remove a method

Confirm removal. Fail-closed: removing your only remaining factor while two-factor is required is blocked (409) — you can't lock yourself out.

confirmremove-last blocked (409)
Frame 07 · /login/mfa (from frame 05)

Recovery-code entry (step-up)

One text field for an opaque recovery code (not OTP cells), sent as {challengeId, recoveryCode}. Offered only when recoveryAvailable is true.

entryverifyingcode didn't work (401)too many attempts (429)sign-in expired (NOT_ACTIVE)couldn't check

Build inventory — what approving these frames approves

Flows
MfaOverviewFlow · /account/security/mfa TotpEnrollFlow · /account/security/mfa/add/authenticator SmsEnrollFlow · /account/security/mfa/add/phone RecoveryCodesFlow · /account/security/mfa/recovery-codes MfaStepUpFlow · /login/mfa
Components
MfaOverviewPanelMfaFactorListMfaFactorRow AddFactorMenuTotpEnrollPanelQrProvisioning OtpInputSmsEnrollPanelChannelUnavailableNotice RecoveryCodesPanelRemoveFactorDialogMfaChallengeCard FactorPicker
Packages
@fuzefront/account-security-ui @fuzefront/security-client (existing)
Revision note
Recovery-code redemption (contract 1.3.0, #1660) does not change this inventory. Recovery entry is a state of MfaChallengeCard inside MfaStepUpFlow, built from the existing DS CodeField + StatusCallout; the count states belong to RecoveryCodesPanel (DS Skeleton + StatusCallout). No new component, package or DS primitive.
Missing DS primitive
OtpInput — a segmented one-time-code field is not yet in @fuzefront/design-system. Named here for frontend-engineer to add as a foundation primitive; not styled one-off.

Recovery-code revision — open question for the owner

After a recovery sign-in
Not framed: a “you used a recovery code — N left” prompt after signing in. Assumed not in scope. The count is already visible under Security → Recovery codes.
● Awaiting approval

Design system: @fuzefront/design-system (fuse-seam) · Contract: packages/security/openapi.yaml → @fuzefront/security-client · Package: @fuzefront/account-security-ui · Flag: fuzefront.security.mfa-management (default OFF).
Honest-degrade signal: GET /v1/security/methods → mfa.types / verification.sms gate whether text-message codes are offered. Per-flow approval lives in manifest.json; approve by setting a flow's approved: true (+ approver + date) or reply @claude approve / @claude reject: <reason>.

Review · mfa-management approve or reject each flow ↓
mfa-overviewapproved · izzywdev
totp-enrollapproved · izzywdev
sms-enrollapproved · izzywdev
recovery-codesapproved · izzywdev
mfa-step-upapproved · izzywdev