Skip to content

feat(auth): enforce the TOTP requirement - #55

Merged
fylorn merged 1 commit into
devfrom
feat/enforce-totp-required
Sep 24, 2026
Merged

fylorn merged 1 commit into
devfrom
feat/enforce-totp-required

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Behaviour

security.totp_required is now enforced server-side instead of only driving a notice.

When the setting is on and a user who has not enrolled TOTP signs in to the console (password login or SSO/OIDC callback), they still get a session, but that session can only reach:

  • GET /api/auth/me
  • POST /api/auth/register-key (the signing key every signed write depends on, including the enrollment calls)
  • POST /api/auth/logout
  • GET /api/auth/totp/status
  • POST /api/auth/totp/setup
  • POST /api/auth/totp/verify-setup

Every other console/admin endpoint answers:

403 {"error": {"type": "totp_enrollment_required", "message": "Two-factor authentication must be set up before continuing"}}

(new AppError::TotpEnrollmentRequired variant, same body shape as the other AppErrors).

  • The gate is decided per request in require_auth, reusing the per-request users lookup that already checks is_active (it now also reads totp_enabled; no extra query). Switching the setting on applies to existing sessions; enrolling lifts the restriction on the same session, no re-login or token refresh.
  • No exemptions, super-admins included. (password_change_required turned out to have no server-side session gate to mirror; it is only a hint in the login response, so this is its own mechanism.)
  • API keys are unaffected: gateway / MCP traffic never passes through require_auth, and console tw- API keys take the API-key branch before the gate.
  • Setting off: no change.
  • POST /api/auth/totp/disable is refused (400) while the setting is on; disabling would only put the session straight back at enrollment.
  • GET /api/auth/me gains totp_enrollment_required: bool.

Console

  • While me.totp_enrollment_required is true, the root renders a standalone enrollment screen in place of the app shell (no navigation; the account email and a logout button are the only other things on it). After verify-setup succeeds the user is reloaded and the console appears.
  • The TOTP setup flow (QR, recovery codes, verify) is extracted from the profile page into components/auth/totp-enrollment.tsx and shared by both places.
  • The API client fires an event on any totp_enrollment_required 403; the auth hook reloads the user, so a session whose setting flips mid-session is sent to enrollment.
  • Profile page: with the setting on, an enrolled user sees that TOTP is required and no disable button.
  • New en/zh strings; errors.byType.totp_enrollment_required added to the i18n checker's dynamic list.

Tests

  • crates/test-support/tests/totp_required.rs
    • unenrolled super-admin via password login: /me and /totp/status work; /api/keys, /api/dashboard/stats, /api/health, /api/admin/settings, /api/admin/users, POST /api/keys, POST /api/auth/password → 403 totp_enrollment_required; real setup + verify-setup with a computed code lifts it on the same session; disable is then refused; logout works for an unenrolled session
    • switching the setting on holds an existing session, switching it off releases it
    • enrolled user unaffected
    • setting off → unenrolled user unrestricted
    • AI gateway API key and console API key of an unenrolled owner still work
  • admin_access.rs::an_unenrolled_sso_user_is_held_at_totp_enrollment — SSO sign-in against the mock IdP is held the same way and released by enrolling
  • Web: api.test.ts (event fired only for this type), use-auth.test.tsx (user reloaded on the event and after enrolling), totp-enrollment.test.tsx (screen offers only enrollment and logout)

Local: cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings, cargo clippy --workspace --lib -D warnings, cargo nextest run --workspace --lib --bins --tests, the full integration suite (--run-ignored only --profile ci), and pnpm check:i18n && pnpm test && pnpm build all pass. The enrollment screen and the mid-session switch were also checked in a browser against a mock API.

🤖 Generated with Claude Code

With security.totp_required on, a console session whose user has not
enrolled TOTP (password or SSO sign-in) only reaches /api/auth/me,
register-key, logout and the TOTP status/setup/verify-setup endpoints.
Everything else answers 403 with error type totp_enrollment_required.
The gate is decided per request in require_auth, so switching the
setting on covers existing sessions and enrolling lifts it on the same
session. API keys are unaffected.

/api/auth/me reports totp_enrollment_required; the console replaces
itself with an enrollment screen while it is set and reloads the user
when any request is refused with that type. Disabling TOTP is refused
while the setting is on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@fylorn
fylorn merged commit 367df3c into dev Sep 24, 2026
6 checks passed
@fylorn
fylorn deleted the feat/enforce-totp-required branch September 24, 2026 12:06
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.

1 participant