feat(auth): trusted devices, recovery codes, and admin MFA management #93

Merged
whitlocktech merged 1 commits from feature/trusted-devices-mfa into main 2026-07-22 05:09:06 +00:00
Member

What & why

Reduces 2FA friction without weakening the second-factor boundary, and closes the 2FA-lockout gap. Four deliverables (design: docs/website/TRUSTED_DEVICES_MFA.md, docs PR: RunicGateway/docs#trusted-devices-mfa):

  1. Trusted devices — opt-in "Trust this device" lets a browser/app skip the TOTP step (never the password) for 30 days.
  2. Recovery codes — 10 single-use bcrypt codes generated at 2FA enrollment; a lost-authenticator fallback that needs no admin.
  3. Admin management — staff can view/revoke a user's trusted devices and reset their MFA (backend and front-end).
  4. Password step-up — reuses the existing currentPassword pattern for recovery-code regeneration; disabling TOTP keeps its current-code requirement.

Key design points:

  • Hashing follows the repo's entropy split: trusted-device tokens are sha256 (256-bit random, looked up by a token_hash unique index); recovery codes are bcrypt (human-typed, verified against the user's ≤10 rows like a password).
  • New rg_trust httpOnly cookie is a server-side, per-row-revocable record (never a JWT claim), so the stateless session JWT is unchanged and trust stays revocable. It gates only the 2nd factor, survives logout, and is cleared on untrust / password change / password reset / TOTP disable.
  • Cap of 10 devices/user with NO silent pruning: an over-cap trust returns 409 {error:'trusted_device_limit', devices} (or trustLimitReached at login); the web client shows a TOTP-styled revoke-to-continue / cancel modal.
  • All admin + self MFA/trusted-device actions are audit-logged.

New tables (trusted_devices, recovery_codes) are additive/idempotent. Fully backwards-compatible: with no rg_trust cookie behavior is exactly today's (TOTP every login).

How it was tested

  • cd server && DB_HOST=127.0.0.1 DB_PORT=59999 node --test414 pass / 0 fail (33 new tests: trusted-device login skip, token mint/hash, recovery-code single-use consume + wrong-code backoff, cap 409, self + admin revocation, invalidation on password change / TOTP disable, and permission/ownership scoping).
  • cd client && npm test43 pass; npm run build clean.
  • OpenAPI regenerated (npm run swagger); new endpoints + schemas verified present.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • AI tools were used. Tool(s): Claude Code (Opus 4.8). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Reduces 2FA friction without weakening the second-factor boundary, and closes the 2FA-lockout gap. Four deliverables (design: `docs/website/TRUSTED_DEVICES_MFA.md`, docs PR: RunicGateway/docs#trusted-devices-mfa): 1. **Trusted devices** — opt-in "Trust this device" lets a browser/app **skip the TOTP step** (never the password) for 30 days. 2. **Recovery codes** — 10 single-use bcrypt codes generated at 2FA enrollment; a lost-authenticator fallback that needs no admin. 3. **Admin management** — staff can view/revoke a user's trusted devices and reset their MFA (backend **and** front-end). 4. **Password step-up** — reuses the existing `currentPassword` pattern for recovery-code regeneration; disabling TOTP keeps its current-code requirement. Key design points: - **Hashing follows the repo's entropy split:** trusted-device tokens are **sha256** (256-bit random, looked up by a `token_hash` unique index); recovery codes are **bcrypt** (human-typed, verified against the user's ≤10 rows like a password). - New `rg_trust` httpOnly cookie is a **server-side, per-row-revocable** record (never a JWT claim), so the stateless session JWT is unchanged and trust stays revocable. It gates only the 2nd factor, survives logout, and is cleared on untrust / password change / password reset / TOTP disable. - **Cap of 10 devices/user with NO silent pruning:** an over-cap trust returns `409 {error:'trusted_device_limit', devices}` (or `trustLimitReached` at login); the web client shows a TOTP-styled revoke-to-continue / cancel modal. - All admin + self MFA/trusted-device actions are audit-logged. New tables (`trusted_devices`, `recovery_codes`) are additive/idempotent. Fully backwards-compatible: with no `rg_trust` cookie behavior is exactly today's (TOTP every login). ## How it was tested - `cd server && DB_HOST=127.0.0.1 DB_PORT=59999 node --test` → **414 pass / 0 fail** (33 new tests: trusted-device login skip, token mint/hash, recovery-code single-use consume + wrong-code backoff, cap 409, self + admin revocation, invalidation on password change / TOTP disable, and permission/ownership scoping). - `cd client && npm test` → **43 pass**; `npm run build` clean. - OpenAPI regenerated (`npm run swagger`); new endpoints + schemas verified present. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [x] AI tools were used. Tool(s): `Claude Code (Opus 4.8)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-07-22 04:39:46 +00:00
feat(auth): trusted devices, recovery codes, and admin MFA management
All checks were successful
PR Checks / bot-install (pull_request) Successful in 19s
PR Checks / server-tests (pull_request) Successful in 42s
PR Checks / client-build (pull_request) Successful in 9m24s
60ebacff2c
Add opt-in "Trust this device" so a browser/app skips the TOTP step (never
the password) for 30 days, single-use bcrypt recovery codes as a 2FA-lockout
fallback, and admin trusted-device/MFA-reset management — backend, web UI,
OpenAPI spec, and tests.

- Schema: trusted_devices (sha256 token hash, looked up by unique index) and
  recovery_codes (bcrypt, single-use). Both additive/idempotent.
- Session service: trust-token mint/hash/resolve + cap helpers; new rg_trust
  httpOnly cookie (survives logout, revoked on untrust/password change/reset/
  TOTP disable). JWTs stay stateless — trust is a server-side row, not a claim.
- Web + mobile login accept a trusted-device token / recovery code; login/totp
  gains trustDevice + recoveryCode. Cap of 10/user with NO silent pruning — an
  over-cap trust returns 409/trustLimitReached and the client prompts to revoke.
- Self-service /auth/me/trusted-devices* + recovery-codes*; admin
  /admin/users/:id/trusted-devices* + /mfa/reset. All actions audit-logged.
- Client: "Trust this device" + recovery-code login options, one-time recovery
  code display, Trusted Devices + Recovery Codes account panels, a TOTP-styled
  revoke-to-continue cap modal, and admin per-user security controls.
- OpenAPI regenerated; 33 new server tests (all suites green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-22 04:50:12 +00:00
whitlocktech merged commit 514bc9d23c into main 2026-07-22 05:09:06 +00:00
whitlocktech deleted branch feature/trusted-devices-mfa 2026-07-22 05:09:07 +00:00
wtclaude referenced this issue from a commit 2026-07-22 05:42:10 +00:00
@
Sign in to join this conversation.
No description provided.