feat(mfa): UX hardening — auto-submit, paste tolerance, low-codes warning, dev-mode diagnostics (#620)

* feat(mfa): auto-submit 6-digit TOTPs and normalize pasted backup codes

Match the UX every major MFA prompt has (GitHub, GitLab, 1Password): the
challenge screen and every code-entry dialog now submit automatically once
the sixth TOTP digit lands, and the backup-code input accepts pastes with
smart-dashes, trailing whitespace, or mixed case without silently
truncating the value. Also caps the backup-code input at the correct
11 characters (10 plus a single separator) instead of 12.

Shared normalization helpers live in frontend/src/lib/mfa.ts so the
challenge and the three account-settings dialogs stay in lockstep.

* feat(mfa): warn users when backup codes run low

The Account & Security card silently showed a dim count of backup codes
remaining, which meant users could drift toward zero without noticing
until their phone was already lost. The card now surfaces a warning tone
with an alert icon when 1 or 2 codes remain, and swaps to a dedicated
destructive warning card with a "Regenerate now" action when the user
has used every code.

* feat(mfa): gate diagnostic logs behind developer mode

Reuses the existing isDebugEnabled() gate so operators investigating a
2FA support ticket can flip Developer Mode on to get per-branch
diagnostics (login path taken, replay check outcome, failure counter
after a verify, replay-table purge counts), and flip it back off when
they are done. Standard lifecycle logs stay on by default: enrolment
completed, 2FA disabled, backup codes regenerated, admin reset, SSO
bypass toggled, lockout engaged. Nothing that could reveal a TOTP code,
base32 secret, backup-code cleartext, or partial-auth JWT is ever
logged.

* test(mfa): cover drift, invalid formats, lockout recovery, and paste normalization

Backend: a TOTP generated for a window that has already slid out is
rejected, malformed backup codes (too short, non-alphanumeric, 11-char
alphanumeric that matches no hash) all increment failed_attempts, a
successful verify clears a below-threshold failure streak, a successful
verify after locked_until has passed clears the lockout, a second
enroll/start overwrites the prior pending secret, and the backup-code
normalizer treats en-dash/em-dash/figure-dash with stray whitespace the
same as the canonical form.

E2E: low-backup-codes warning renders in the warning tone and the
exhausted-codes state flips to the dedicated warning card, a 6-digit
TOTP auto-submits without a button click, and a backup code pasted
without the separator still signs in.

* docs(mfa): auto-submit, paste guidance, and expanded troubleshooting

Document that the challenge screen submits automatically on the sixth
digit, that backup codes accept the separator and any case, and that
the Account & Security card nudges at low code counts. Expands the
troubleshooting section with entries for lost or exhausted backup codes
and adds a short note to the admin guide about surfacing auth
diagnostics via Developer Mode.
This commit is contained in:
Anso
2026-04-15 19:51:44 -04:00
committed by GitHub
parent a43c203d7b
commit 4722028904
12 changed files with 600 additions and 53 deletions
+29 -1
View File
@@ -60,14 +60,34 @@ After entering your password, Sencho shows the 2FA challenge screen.
1. Open your authenticator app, find the Sencho entry, read the six-digit code
2. Type it into the **Verification code** field
3. Click **Verify and sign in**
3. Sencho submits automatically once you enter the sixth digit. No click required.
<Note>
If you prefer the explicit route, the **Verify and sign in** button still works. The auto-submit only applies to the six-digit TOTP field; backup codes always require a click to confirm.
</Note>
If your phone is unavailable, click **Use a backup code instead**, enter one of the codes you saved during enrolment, and click **Verify and sign in**. That code is now used up.
### Backup code entry tips
Backup codes are shown grouped as `ABCDE-FGHIJ` to make them easier to read. When signing in, you can enter them any of these ways:
- Paste the code exactly as shown: `ABCDE-FGHIJ`
- Paste without the dash: `ABCDEFGHIJ`
- Paste with extra whitespace or lowercase letters; Sencho normalises the input before sending it to the server
Only letters and digits are significant; dashes, spaces, and case are ignored.
## Regenerate backup codes
If you think your backup codes have been exposed, or you have used most of them, regenerate them from **Settings → Account & Security → Regenerate backup codes**. Sencho asks for a current code, then issues a fresh set of ten and invalidates the old set immediately.
The Account & Security card nudges you about low code counts so you notice before you are locked out:
- **3 or more codes remaining:** a muted count under the status message.
- **1 or 2 codes remaining:** the count turns warning-coloured with an alert icon, inviting you to regenerate a fresh set.
- **0 codes remaining:** a dedicated warning card appears with a **Regenerate now** button. At this point, losing your authenticator app means recovery needs an administrator, so regenerate before that happens.
<Frame>
<img src="/images/two-factor-auth/account-card-enabled.png" alt="Account card showing 2FA enabled with regenerate and disable actions" />
</Frame>
@@ -111,6 +131,14 @@ Click **Can't scan? Show secret key** during enrolment, copy the base32 string,
Contact an administrator. An admin can reset your 2FA from the Users section in **Settings → Users**. See the [admin guide](/operations/two-factor-admin) for the steps. If you are the only admin and have lost access, the administrator can also reset 2FA from the command line on the host running Sencho.
### Lost your backup codes
If you still have your authenticator app, sign in as normal and regenerate the codes from **Settings → Account & Security → Regenerate backup codes**. The previous set stops working immediately. If you no longer have the authenticator app either, follow the "Lost phone and no backup codes left" entry above and ask an administrator to reset 2FA.
### Ran out of backup codes
Each backup code can be used once. As soon as you sign back in with an authenticator code, regenerate a new set from **Settings → Account & Security**. Without codes, losing your phone means recovery requires an administrator.
### SSO sign-in is unexpectedly asking for a code
The **Require 2FA even when signing in via SSO** toggle is on for your account. Open **Settings → Account & Security** and flip it off if SSO alone is enough for your threat model.
+4
View File
@@ -39,6 +39,10 @@ Replace `<username>` with the admin's account name. On success the command print
The command respects the container's `DATA_DIR`, so it always acts on the same SQLite database the application uses. It writes an audit log entry attributed to `cli` so the action is auditable after the fact.
<Note>
Turning on **Developer Mode** in **Settings → Developer** surfaces additional authentication diagnostics in the backend logs. These are helpful when investigating a 2FA support ticket, and can be turned off again once the issue is resolved.
</Note>
## Per-user SSO enforcement
When SSO (LDAP or OIDC) is configured, users with 2FA enabled sign in through SSO without a second factor by default. SSO is already an authenticated flow, and requiring a TOTP on top is extra friction that most teams do not need.