Skip to content

feat: two-factor authentication with QR code#7

Merged
mqmalagris merged 2 commits into
mainfrom
dev
May 1, 2026
Merged

feat: two-factor authentication with QR code#7
mqmalagris merged 2 commits into
mainfrom
dev

Conversation

@mqmalagris
Copy link
Copy Markdown
Owner

@mqmalagris mqmalagris commented May 1, 2026

Summary

  • New `twofactor` package implements TOTP (RFC 6238 / SHA-1 / 30s / 6 digits) inline — no extra dependency. Covered by the RFC reference test vectors plus a round-trip on freshly generated secrets.
  • DynamoDB `UserRecord` gains `twofa_secret` and `twofa_enabled`. Pending logins live under the `pending2fa:` sentinel with a 5-minute TTL.
  • Setup endpoints (premium-gated):
    • `POST /user/2fa/setup` returns a fresh secret + `otpauth://` URI without enabling
    • `POST /user/2fa/enable` flips the flag after verifying a code
    • `POST /user/2fa/disable` requires a current code (so a stolen api key can't quietly drop the second factor)
  • Login flow change:
    • `GET /auth/verify` returns `{ two_factor_required, pending_token }` instead of an api key when 2FA is on
    • `POST /auth/2fa/verify` exchanges the pending token + TOTP code for the api key
  • Account page renders three states (off, setup pending, on). Setup shows a 180px QR code (client-side render via `qrcode` npm) next to the secret so users can scan with Google Authenticator, Authy, 1Password, or Bitwarden — or copy the secret manually.
  • `verify.astro` renders an inline TOTP prompt when the magic-link response signals 2FA, then trades the pending token for the api key.
  • All copy translated in EN / PT-BR / ES / FR.

Test plan

  • `go test ./...` passes (twofactor package validates RFC 6238 vectors)
  • `pnpm build` and `pnpm test` succeed
  • `terraform apply` in dev creates the four new routes (`/user/2fa/{setup,enable,disable}` + `/auth/2fa/verify`)
  • As a premium user: Enable 2FA → scan QR → enter code → confirm → /user/me reports `two_factor_enabled: true`
  • Sign out, request a magic link → click link → see TOTP prompt → wrong code = inline error → correct code = login success
  • Disable 2FA without a code → 401; with valid code → cleared, magic link goes back to one-step
  • Enable 2FA as a free user → 403

mqmalagris added 2 commits May 1, 2026 11:59
Premium users can enable 6-digit TOTP (RFC 6238 / SHA-1 / 30s) on their
account. New twofactor package implements generation and verification
inline (no extra dependency) and is covered by the RFC reference test
vectors.

Setup flow:
  POST /user/2fa/setup    → secret + otpauth:// URI (not yet enabled)
  POST /user/2fa/enable   → verify code, flip enabled flag
  POST /user/2fa/disable  → require valid code, clear secret

Login flow change:
  GET  /auth/verify       → if 2FA enabled, returns
                            { two_factor_required: true, pending_token }
                            instead of an api_key
  POST /auth/2fa/verify   → exchange pending_token + TOTP code for the
                            real api_key

Pending tokens are stored under the __pending2fa__ sentinel with a
5-minute server-side TTL. Disable still requires a current code so a
stolen api key can't quietly drop the second factor.

Account page renders three states (off, setup pending with secret +
URI for QR, on with disable form). verify.astro renders a code prompt
when the magic-link response signals 2FA. Four locales translated.
Adds qrcode npm dep and a small QRCode.svelte component that renders the
otpauth:// URI as a 180px canvas. The 2FA setup card shows it side-by-
side with the secret string so users can either scan with their
authenticator app or copy the secret manually.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

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

Project Deployment Actions Updated (UTC)
ephemask-web Ready Ready Preview, Comment May 1, 2026 3:13pm

@mqmalagris mqmalagris merged commit a776a74 into main May 1, 2026
5 checks passed
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