Skip to content

Add optional TOTP 2FA to admin login#36

Merged
PeterRounce merged 14 commits into
mainfrom
claude/admin-totp-2fa
Jun 16, 2026
Merged

Add optional TOTP 2FA to admin login#36
PeterRounce merged 14 commits into
mainfrom
claude/admin-totp-2fa

Conversation

@PeterRounce

Copy link
Copy Markdown
Member

Summary

Adds optional TOTP-based two-factor authentication to the single-admin login. When enabled, login requires a 6-digit TOTP code (or a one-time recovery code) on top of the existing admin password. Implemented with github.com/pquerna/otp, stored in the existing settings key-value table (no schema migration), and recoverable via backup codes or a docker exec CLI command so a lost authenticator never permanently locks the admin out.

Enforcement is login-only — the session is already 2FA-backed, so the withdraw handler keeps its existing password re-prompt (no 30s-code friction during a payout).

Design spec: docs/superpowers/specs/2026-06-15-admin-totp-2fa-design.md
Implementation plan: docs/superpowers/plans/2026-06-15-admin-totp-2fa.md

What's included

Backend

  • web/totp.go — TOTP generate/validate (±1 step skew) + recovery-code generation (10 codes, 64-bit, bcrypt-hashed, shown once).
  • web/admin_api_2fa.go — settings helpers + GET status, POST setup, POST enable, POST disable endpoints (all session-protected).
  • web/admin_api.goadminApiLogin now requires a valid TOTP or single-use recovery code before issuing a session when 2FA is enabled.
  • web/admin_api_settings.go — settings list redacts the _secret suffix (the TOTP secret never leaks).
  • cli.goDisableAdmin2FA recovery command (docker exec -it card ./app DisableAdmin2FA).
  • Caddyfile/2fa/enable and /2fa/disable added to the auth-endpoint rate-limit zone.

Frontend (admin SPA)

  • hooks/use-auth.tsxlogin(password, code?) + TotpRequiredError; surfaces a rejected-code error.
  • pages/login.tsx — reveals a code field (with an authenticator/recovery-code toggle) when a second factor is required.
  • components/two-factor-card.tsx — Settings-page card driving enrollment (QR + confirm → recovery codes shown once) and disable (password-confirmed).

Storage (settings table, no migration): admin_totp_enabled ("Y"/"N", gates enforcement), admin_totp_secret (base32, redacted), admin_totp_recovery_hash (JSON array of bcrypt-hashed single-use codes, redacted).

Versioning: bumped 0.19.9 → 0.20.0 (SemVer minor — new backward-compatible feature); CLAUDE.md updated to adopt SemVer and document the feature.

Test Plan

Automated (all green): go test -race -count=1 ./... and frontend npm run build.

  • TOTP/recovery helpers; settings-secret redaction; status/setup/enable/disable endpoints (incl. setup-when-already-enabled, enable-without-setup, wrong-code/wrong-password → 400).
  • Login enforcement: password-only → totpRequired (no session issued); valid TOTP → session; invalid code → 401; recovery code single-use (then rejected); wrong password + valid code → still rejected; 2FA-off password-only still works.

Manual (recommended on the test hub after deploy):

  • Settings → Enable 2FA → scan QR → confirm code → recovery codes shown once.
  • Log out / log in: password → code prompt → correct code logs in; wrong code shows an error.
  • "Use a recovery code instead" accepts a code once; reused code rejected; remaining count decrements.
  • docker exec -it card ./app DisableAdmin2FA → next login is password-only.
  • Settings list shows admin_totp_secret as REDACTED.

🤖 Generated with Claude Code

PeterRounce and others added 14 commits June 16, 2026 08:13
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PeterRounce PeterRounce merged commit 4a52b7c into main Jun 16, 2026
7 checks passed
@PeterRounce PeterRounce deleted the claude/admin-totp-2fa branch June 16, 2026 13:57
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