Repository navigation
fix(frontend): the MFA code screen does not tell users to sign in again or wait after too many wrong codes #872
Description
Problem
When signing in with two-factor authentication, someone who mistypes their code too often gets stuck. The screen keeps saying "invalid code" while the server has already stopped listening.
Since #689 the challenge endpoint refuses in two new ways:
401 {"code":"MFA_CHALLENGE_EXHAUSTED"}: this sign-in attempt used its 5 codes. The token is revoked, and any further code on it answers401 TOKEN_REVOKED. The only way forward is to enter the password again.429 {"code":"MFA_LOCKED","retry_after":<seconds>}with aRetry-Afterheader: the account failed 10 times in 15 minutes and refuses codes for 15 minutes.
AuthScreen.tsx maps every challenge error to copy.mfaInvalid (the catch around challengeMFA). So the user keeps typing codes into a dead token, or into a locked account, with no hint that they should start over or wait.
Acceptance criteria
-
On
MFA_CHALLENGE_EXHAUSTED, or onTOKEN_REVOKEDfrom the challenge, the screen drops the MFA token, returns to the password step, and says why: FR « Trop de codes erronés pour cette connexion. Reconnectez-vous avec votre mot de passe. » / EN "Too many wrong codes for this sign-in. Sign in again with your password." -
On
MFA_LOCKED, the screen says how long to wait, fromretry_after(rounded up to whole minutes), in FR and EN, and disables the code field until then. -
A wrong code that is neither of these still shows the existing "invalid code" message.
-
The strings live in
fr.jsonanden.json. There are no inline literals, and the types have noany. -
Tests in
features/auth/__tests__/cover each of the three responses. -
(Added 2026-10-02 at the owner's request.) Mandated enrolment has the same defect. An expired enrolment token (
MFAEnrollmentTTL, 15 min) made/auth/mfa/verifyreload/loginwith no message. It now returns to the password with the "step expired" reason. The same endpoints called from Settings, with a session, keep the refresh-and-redirect behaviour. -
(Added 2026-10-02.) The expired challenge (criterion 1,
TOKEN_EXPIRED) is checked live, after the real 5 minutes, not only in tests.
Definition of Done
-
npx tsc -b --noEmit,npm run buildandnpx vitest rungreen, with output pasted here. - Live pass against a local backend with an enrolled account: 5 wrong codes, then 10 across sign-ins. Screenshots of both states, FR and EN.
- Progress comment in the CLAUDE.md format; PR with
Closesthis issue.
Follows #689 (backend limits). The owner decided on 2026-10-02 that the copy ships separately.
Activity
fix(frontend): let the MFA challenge handle its own 401s (#872)
fix(frontend): tell users to sign in again or wait after too many MFA…
fix(frontend): bring the auth error banner and footer to 4.5:1 contra…
alex-dembele commentedon Oct 2, 2026
frontend — 2026-10-02
Done
frontend/src/lib/api.ts: requests to/auth/mfa/challengeno longer trigger a session refresh or the/loginreload on a 401. That reload was wiping the explanation after the 6th code (TOKEN_REVOKED).frontend/src/features/auth/challengeRefusal.ts: a pure classifier with three outcomes: invalid, restart (exhausted or expired), or locked (with a duration, or null for the per-IP limit).frontend/src/features/auth/AuthScreen.tsx:frontend/src/features/auth/authStrings.ts: FR/EN copy (mfaExhausted,mfaExpired,mfaLocked,mfaLockedUnknown).frontend/src/features/auth/fields.tsx,AuthLayout.tsx: two serious axe contrast failures fixed (error banner 4.46 → 4.68:1, footer year 3.35:1).Verified
npx tsc -b --noEmitexit 0;npx vite buildOK;npx vitest run: 103 files, 965 tests passed; eslint--max-warnings=0clean on the touched files.Retry-After=900shows "15 minutes", with the field disabled.Criteria
any, but inauthStrings.ts, the auth module's convention, notfr.json/en.jsonas the criterion says. I wrote the criterion without checking. Tell me if you want the module moved.Next: owner review of #880, to merge after #875.
Blocked on: #875 for the codes to appear in production. Nothing for the review.
Not done: the expired-challenge case was only tested, not checked live; expired enrolment tokens (setup/verify) still reload
/loginwith no message, which is outside this issue.fix(frontend): send an expired MFA enrolment back to the password wit…
alex-dembele commentedon Oct 2, 2026
frontend — 2026-10-02 (criteria 6 and 7)
Done
frontend/src/lib/api.tsandfrontend/src/features/auth/authService.ts: the interceptor exemption now follows the credential, through a typedownCredentialconfig flag set when an MFA token is sent, instead of the challenge URL. Mandated enrolment is covered too, and Settings (which runs on the session) keeps its refresh.frontend/src/features/auth/AuthScreen.tsx: an expired enrolment token, at setup or at verify, returns to the password form withmfaExpired.8b0e152bon PR fix(frontend): tell users to sign in again or wait after too many MFA codes (#872) #880.Verified
npx tsc -b --noEmitexit 0;vite buildOK;npx vitest run: 104 files, 972 tests passed.401 TOKEN_EXPIRED→ password form with the reason, no reload;npm run lint:ceilingfails here exactly as on master (3 rules, in files this branch does not touch). That is fixed by chore(lint): make the ESLint ratchet pass on master again, and tighten it to 99 (#627) #891.Criteria: 6 ✅ · 7 ✅ (1 to 5 unchanged, see the earlier comment)
Next: merge #891, bring master into this branch, then owner review of #880.
Blocked on: #891 for a green lint gate.
Found along the way, now separate: #886 (master does not compile, P0, PR #888) and #889 (an unfinished enrolment locked the account out for good, PR #892).
Merge pull request #880 from opendefender/fix/872-mfa-challenge-refusals
fix(frontend): give LoginForm its notice prop back (#893)