Skip to content

fix(auth): ask for the authenticator code on sign-in when 2FA is on - #1456

Merged
iammukeshm merged 2 commits into
mainfrom
fix/two-factor-login-step
Nov 3, 2026
Merged

iammukeshm merged 2 commits into
mainfrom
fix/two-factor-login-step

Conversation

@iammukeshm

@iammukeshm iammukeshm commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Problem

Both React apps let a user turn on two-factor authentication under Settings > Security, but neither login page ever sent the authenticator code. After enabling 2FA, every sign-in failed with 401 two_factor_required, the form showed a generic error, and the user was locked out of the app that offered the feature.

Fix

The API already supported the second factor and already tags the 401 with a stable ProblemDetails code (Identity.TwoFactorRequired for a missing code, Identity.TwoFactorInvalid for a wrong one), so no API change was needed. The clients branch on that code, not on the detail text, which is localized.

In both clients/admin and clients/dashboard:

  • issueToken and login accept an optional twoFactorCode, sent only when present.
  • When the first token call answers Identity.TwoFactorRequired, the login card switches to a code step: a six-digit field (autocomplete="one-time-code", inputmode="numeric", autofocus), a Verify button and a "Back to sign in" link. The page repeats the call with email, password, tenant and twoFactorCode.
  • A wrong code shows an inline error, clears the field and keeps the user on the step.
  • The password lives only in the login page's component state for the retry. The password field is cleared when the code step opens, and the held credentials are dropped on back, on success and on unmount. Nothing goes to localStorage or sessionStorage.
  • Tenant entry stays on the first step, and the retry sends the same tenant header.

All new strings (the code step's heading, label, placeholder, buttons, back link and the wrong-code error) come from the auth catalog in both locales, en-US and pt-BR, so the step follows the language switcher like the rest of the login page.

Both login pages already use controlled inputs (admin's login doesn't use react-hook-form), so the new step follows the same approach. Recovery codes aren't supported by the API (VerifyTwoFactorOrThrowAsync only checks the authenticator provider), so the step accepts authenticator codes only.

On the backend side this is test-only: the integration-test exception handler now mirrors the production handler's code extension, and the two existing 2FA login tests assert both codes, since the clients now depend on them.

Tests

  • New Playwright spec tests/auth/two-factor-login.spec.ts in each app (4 tests each): 2FA user gets the code step and signs in with the second call carrying twoFactorCode (and the password never lands in browser storage); a wrong code shows the error and stays on the step; Back returns to the password step with the password cleared; a non-2FA user still signs in in one step.
  • After merging main (i18n): admin full Playwright suite 183 passed; dashboard 250 passed, plus 2 failures that also fail on main without this change (i18n/switcher.spec.ts impersonation case and system/tenant-deactivated.spec.ts unrelated-403 case, both waitForLoadState timeouts).
  • Integration tests: 790 passed (including the updated TwoFactorAuthTests, 6/6).
  • eslint . (0 errors, including the i18next literal-string rule) and tsc -b --noEmit clean in both apps.

Docs: fullstackhero/docs#276 (changelog entry, new "Signing in from the React apps" section on the two-factor page, and a correction: the page said clients key off the detail prefixes).

Closes #1446

Thanks @marcelo-maciel for the detailed report.

Both React apps let users enable two-factor authentication, but neither login
page could send the code, so every later sign-in failed with 401
two_factor_required and locked the user out.

When POST /token/issue answers with the ProblemDetails code
Identity.TwoFactorRequired, the admin and dashboard login cards now switch to
a six-digit code step (one-time-code autocomplete, numeric input) and repeat
the call with twoFactorCode. A wrong code (Identity.TwoFactorInvalid) shows an
inline error and keeps the user on the step; Back returns to the password
step. The password is held only in component state for the retry and is
cleared on back, success and unmount.

The integration test exception handler now mirrors the production handler's
code extension, and the 2FA login tests assert both codes, since the clients
depend on them.

Closes #1446
… step

Resolve the login page conflicts against main's i18n work: keep main's
translated form, notice banner and demo accounts, and re-apply the
authenticator code step on top. The TwoFactorStep strings and the
invalid-code error now come from the auth catalog (en-US + pt-BR) in
both apps.
@iammukeshm
iammukeshm merged commit 762ae40 into main Nov 3, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two-factor: users who enable 2FA are locked out, the login never asks for the code

1 participant