Skip to content

fix(auth): an unfinished MFA enrolment locks a privileged account out for good — setup answers 400 forever #889

Description

@alex-dembele

Problem

A member whose role requires MFA, and whose enrolment was ever left unfinished (tab closed, or the 15-minute enrolment token expired), can never sign in again. Every later login lands on enrolment, and enrolment fails with "Impossible de préparer la double authentification … rechargez la page". Reloading changes nothing.

Reproduced (2026-10-02, local instance on master + #888)

  1. Invite an admin and set the MFA policy to 0 grace days. Their login answers mfa_enrollment_required.
  2. The enrolment screen calls POST /auth/mfa/setup → 200. A row is stored in mfa_secrets with is_verified = false.
  3. Let the token expire. A valid code then gets 401 TOKEN_EXPIRED, and the user is sent back to the password (fix(frontend): the MFA code screen does not tell users to sign in again or wait after too many wrong codes #872).
  4. Sign in again → enrolment → POST /auth/mfa/setup → 400, every time.
[12:18:42] 200 - POST /api/v1/auth/mfa/setup
[12:34:53] 400 - POST /api/v1/auth/mfa/setup
mfa_secrets: enrol872@example.test | is_verified=f | 2026-10-02 13:18:42

Cause

SetupMFAUseCase.Execute (backend/internal/application/auth/mfa_usecase.go) refuses only a verified existing secret, then always calls CreateMFASecret. mfa_secrets.user_id is unique, so an unverified row left by an earlier attempt makes every insert fail.

The same defect is why the frontend carries a "setup is NOT idempotent" workaround in AuthScreen.tsx.

Acceptance criteria

  1. When an unverified secret exists, setup replaces its key material and answers 200 with a fresh secret, QR code and backup codes. Only the new secret verifies.
  2. When a verified secret exists, setup still answers 409 already_enabled, and the stored secret is untouched.
  3. Tests: TestSetupMFA_ReplacesAnUnverifiedSecret, TestSetupMFA_VerifiedSecretIsKept.
  4. Live: the reproduction above ends with the user enrolled and signed in.

Definition of Done

  • go test ./internal/application/auth/ -race plus the full backend suite, with output pasted here.
  • Live reproduction re-run.
  • Progress comment in the CLAUDE.md format; PR with Closes this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions