From 1dfaa5113e0a879f37ab04905d7a2c2fb3e94ea5 Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Tue, 3 Nov 2026 22:14:55 +0530 Subject: [PATCH] docs(security): document the two-factor code step on the React sign-in pages The admin and dashboard login pages now ask for the authenticator code when the token call answers Identity.TwoFactorRequired. Describe the step, correct the claim that clients key off the detail prefixes (they use the ProblemDetails code), and add the changelog entry for #1446. --- src/content/docs/changelog/index.mdx | 6 ++++- src/content/docs/frontend/admin.mdx | 4 ++- src/content/docs/security/two-factor.mdx | 32 +++++++++++++++++++++--- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/content/docs/changelog/index.mdx b/src/content/docs/changelog/index.mdx index 62e777be..e9d3f281 100644 --- a/src/content/docs/changelog/index.mdx +++ b/src/content/docs/changelog/index.mdx @@ -1,6 +1,6 @@ --- title: Overview -lastUpdated: 2026-10-09 +lastUpdated: 2026-11-03 description: Release notes and version history for fullstackhero. sidebar: order: 1 @@ -11,6 +11,10 @@ seo: Notable changes to the kit, newest first. +## 2026-11-03 + +- **Admin & dashboard: sign-in now asks for the authenticator code when two-factor is on (fix).** Both apps let a user enable two-factor authentication under Settings > Security, but neither login page could send the code, so the next sign-in failed with `401 two_factor_required` and locked the user out. When `POST /api/v1/identity/token/issue` answers with the ProblemDetails `code` `Identity.TwoFactorRequired`, the login card now switches to a code step (six digits, `autocomplete="one-time-code"` so the browser or password manager can fill it), then repeats the call with `twoFactorCode`. A wrong code (`Identity.TwoFactorInvalid`) shows an inline error and keeps the user on the step; "Back to sign in" returns to the password step. The password is held only in the login page's memory for the retry, never in browser storage, and is dropped on back, success or leaving the page. The clients branch on the `code` extension, not the localized `detail` text. Recovery codes are still not supported. See [Two-factor authentication](/docs/security/two-factor/#signing-in-from-the-react-apps) and [#1446](https://github.com/fullstackhero/dotnet-starter-kit/issues/1446). + ## 2026-10-09 - **CLI & template: scaffolded projects now ship the AI guides `AGENTS.md` points to (fix).** `fsh new` and `dotnet new fsh` included `AGENTS.md` but excluded `CLAUDE.md`, `GEMINI.md` and `.agents/`, so the guide referenced files that did not exist. All four now ship together by default, the scaffolded `AGENTS.md` drops the sections that only apply to the kit's own repo, and `--no-agents` (CLI) / `--agents false` (template) leaves them all out. The internal `superpowers/` folder (specs and audits) no longer ships either. See [CLI](/docs/cli/#fsh-new) and [#1432](https://github.com/fullstackhero/dotnet-starter-kit/pull/1432). diff --git a/src/content/docs/frontend/admin.mdx b/src/content/docs/frontend/admin.mdx index 5d9169ff..6c0a6b5c 100644 --- a/src/content/docs/frontend/admin.mdx +++ b/src/content/docs/frontend/admin.mdx @@ -1,6 +1,6 @@ --- title: Admin console -lastUpdated: 2026-06-11 +lastUpdated: 2026-11-03 description: The operator-facing React + Vite app at clients/admin - users, roles, tenants, impersonation, audits, billing, webhooks, health, notifications. sidebar: label: Admin console @@ -80,6 +80,8 @@ await login({ email, password, tenant }); // stores both tokens, then navigates to the post-login destination. ``` +If the account has two-factor authentication on, the first call answers `401` with `code: "Identity.TwoFactorRequired"` and the login card switches to a six-digit code step; the page then calls `login({ email, password, tenant, twoFactorCode })`. The password stays in component memory only for that retry. The dashboard login works the same way. See [Two-factor authentication](/docs/security/two-factor/#signing-in-from-the-react-apps). + Token refresh is **reactive, not timer-based**: when any API call returns 401, the API client runs a single-flight refresh (`POST /api/v1/identity/token/refresh`) and retries the original request. At boot, if the stored access token is missing or expired but a refresh token exists, the auth context attempts one silent refresh before rendering protected routes. If a refresh fails (refresh token expired / revoked), the user is signed out and redirected to `/login`. ### Inactivity auto sign-out diff --git a/src/content/docs/security/two-factor.mdx b/src/content/docs/security/two-factor.mdx index d20b34fe..91eabc36 100644 --- a/src/content/docs/security/two-factor.mdx +++ b/src/content/docs/security/two-factor.mdx @@ -1,6 +1,6 @@ --- title: Two-factor authentication (TOTP) -lastUpdated: 2026-06-11 +lastUpdated: 2026-11-03 description: Opt-in TOTP enrolment for any user - shared secret + otpauth URI, verify, password-confirmed disable, with TOTP-required token issuance once enrolled. sidebar: label: Two-factor (TOTP) @@ -37,8 +37,8 @@ The kit doesn't force 2FA. A reasonable production posture is to make TOTP **man ↓ 5. From now on, POST /token/issue requires { email, password, twoFactorCode } (the third field is required iff enrolled) - - Missing code → 401 "two_factor_required" - - Wrong code → 401 "two_factor_invalid" + - Missing code → 401, ProblemDetails code "Identity.TwoFactorRequired" + - Wrong code → 401, ProblemDetails code "Identity.TwoFactorInvalid" ↓ 6. To turn off: POST /api/v1/identity/2fa/disable { currentPassword } - Requires the current password, so a stolen access token alone @@ -106,7 +106,31 @@ if (user.TwoFactorEnabled) } ``` -The error prefixes (`two_factor_required` / `two_factor_invalid`) are stable strings the React clients key off to show the code-entry step. The whole login - including 2FA attempts - sits behind the rate-limit `auth` policy, which is the throttle on TOTP brute-forcing. Failed TOTP codes do **not** increment the password-lockout counter; only failed passwords do. +Both exceptions carry a `MessageKey`, which the global exception handler writes to the ProblemDetails body as a machine-readable `code` next to the localized `detail`: + +```json +{ + "title": "Unauthorized", + "status": 401, + "detail": "two_factor_required: An authenticator code is required to complete sign-in.", + "code": "Identity.TwoFactorRequired" +} +``` + +Branch on `code` (`Identity.TwoFactorRequired` / `Identity.TwoFactorInvalid`), not on `detail`: the detail text follows the request culture and changes with `Accept-Language`. The whole login - including 2FA attempts - sits behind the rate-limit `auth` policy, which is the throttle on TOTP brute-forcing. Failed TOTP codes do **not** increment the password-lockout counter; only failed passwords do. + +## Signing in from the React apps + +The admin and dashboard login pages handle the second factor as a two-step card: + +1. The user submits tenant, email and password. For an account without 2FA, `token/issue` returns tokens and the user is in, in one step. +2. If the API answers `401` with `code: "Identity.TwoFactorRequired"`, the card switches to a code step: a six-digit field with `inputmode="numeric"` and `autocomplete="one-time-code"` (so the browser or a password manager can fill it), focused on arrival. +3. On submit, the page repeats `token/issue` with `{ email, password, twoFactorCode }` and the same `tenant` header. +4. A wrong or expired code (`code: "Identity.TwoFactorInvalid"`) shows an inline error, clears the field and keeps the user on the step. "Back to sign in" returns to the password step. + +The password is kept only in the login page's component state for that retry. It never goes to `localStorage` or `sessionStorage`, the password field is cleared when the code step opens, and the held credentials are dropped on back, on success and when the page unmounts. The detection lives in `isTwoFactorRequiredError` / `isTwoFactorInvalidError` in each app's `src/auth/api.ts`, and the step itself is `src/components/auth/two-factor-step.tsx`. + +The code step accepts authenticator codes only. Recovery codes are not part of the kit (see below), so there is no "use a recovery code" link. ## Operational considerations