Skip to content

feat(auth): TOTP two-factor authentication (flag-gated rollout) - #22

Merged
Lingz450 merged 1 commit into
mainfrom
feat/2fa-totp
Jun 24, 2026
Merged

feat(auth): TOTP two-factor authentication (flag-gated rollout)#22
Lingz450 merged 1 commit into
mainfrom
feat/2fa-totp

Conversation

@Lingz450

Copy link
Copy Markdown
Collaborator

Summary

Adds authenticator-app two-factor authentication to the local email+password auth. Required for all users via the PB_REQUIRE_2FA flag, which defaults OFF so this deploys safely (enrollment available, not yet enforced).

Sign-in becomes two-step: the password step returns a short-lived MFA challenge token instead of a session, and /auth/2fa/verify exchanges a 6-digit code (or a one-time recovery code) for the real access + refresh tokens.

Backend

  • Migration 019_user_totp.sql: totp_secret / totp_enabled / totp_recovery_codes / totp_enrolled_utc on users. Recovery codes stored as bcrypt hashes, never plaintext.
  • app/core/totp.py: pyotp wrappers, recovery-code gen/verify, and the challenge token (signed with a namespaced secret so it can never be replayed as a session token).
  • Endpoints: POST /auth/2fa/enroll, POST /auth/2fa/verify, GET /auth/2fa/status. Both /auth/signin and /auth/signup gate behind PB_REQUIRE_2FA. An already-enrolled user is always challenged even if the flag is off. The code step has its own brute-force lockout bucket.

Frontend

  • API client: signin/signup now return AuthResult (session or MFA challenge); enroll2fa/verify2fa added.
  • AuthForm: a 2FA step (manual setup key + otpauth:// deep link, chosen over a QR because this is a mobile-first app where you can't scan your own screen) and a one-time recovery-codes screen before routing in.

Rollout (safe by default)

  1. Merge + deploy with PB_REQUIRE_2FA off (this PR's default). Nothing changes for existing users yet.
  2. Enrol your own account, confirm the flow end-to-end.
  3. Set PB_REQUIRE_2FA=true in Render to enforce for everyone.

Note: existing refresh tokens continue to work until they cycle; enforcement applies at each fresh sign-in.

Verification

  • Backend: ruff, mypy (132 files), red-team eval harness (0 failures), 43 existing auth tests + 7 new tests/test_2fa.py all pass.
  • Frontend: typecheck, lint, build, and 132 web tests pass.
  • One unrelated research SSE test (test_streaming_run_emits_progress_sources_and_completion) is order-dependent and flaky in the full run (passes in isolation); not touched by this change.

🤖 Generated with Claude Code

Adds authenticator-app 2FA to the local email+password auth. Sign-in becomes
two-step: the password step returns a short-lived MFA challenge token instead
of a session, and /auth/2fa/verify exchanges a 6-digit code (or a one-time
recovery code) for the real access + refresh tokens.

Backend:
- migration 019_user_totp.sql: totp_secret/enabled/recovery_codes/enrolled_utc
  on users (recovery codes stored as bcrypt hashes, never plaintext).
- app/core/totp.py: pyotp wrappers, recovery codes, and the challenge token
  (signed with a namespaced secret so it can't be replayed as a session token).
- /auth/2fa/enroll, /auth/2fa/verify, GET /auth/2fa/status; signin AND signup
  gate behind PB_REQUIRE_2FA. An already-enrolled user is always challenged,
  even if the flag is off. Code step has its own brute-force lockout bucket.

Frontend:
- api client: signin/signup return AuthResult (session | MFA challenge);
  enroll2fa/verify2fa added.
- AuthForm: 2FA step (manual setup key + otpauth deep link, mobile-friendly)
  and a one-time recovery-codes screen before routing in.

Rollout is safe by default: PB_REQUIRE_2FA defaults OFF, so this deploys with
enrollment available but not enforced. Flip it on after enrolling.

Gates: ruff, mypy (132 files), red-team eval (0), 43 auth + 7 new 2FA tests,
web typecheck/lint/build + 132 web tests all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
petrobrain Ready Ready Preview, Comment Jun 24, 2026 1:54pm

@Lingz450
Lingz450 merged commit d1d2454 into main Jun 24, 2026
7 checks passed
@Lingz450
Lingz450 deleted the feat/2fa-totp branch June 24, 2026 14:01
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