Email verification (double opt-in), off by default (#31) #34

Closed
Cordy wants to merge 5 commits from feat/email-verification into main
Owner

Implements #31. Ships dormant — enforcement is behind REQUIRE_EMAIL_VERIFICATION (default off), so this is safe to merge and deploy now; it activates only when the env flag is set (after the enter.html companion lands).

Behaviour when REQUIRE_EMAIL_VERIFICATION=true

  • POST /v1/register → creates an unverified account, emails a confirmation link, returns 202 {status:"verify_sent"} with no session.
  • GET /v1/verify?token=… → marks the account verified, redirects to …/enter.html#verified (or #verify-failed). A GET link, so the emailed URL works with no frontend page.
  • POST /v1/verify/resend {email} → always 204 (no existence oracle).
  • POST /v1/login → 403 {reason:"unverified"} until the email is confirmed.
  • Tokens are memory-only, single-use, 24 h TTL (mirrors password reset).

When the flag is off (default, and every test)

Register returns 201 + session and login is ungated exactly as today — the whole existing suite passes untouched.

Grandfather

A one-time marker-file backfill marks all pre-existing accounts verified at first boot, so enabling the gate never locks out a current customer.

Auth allowlist (from #33, recorded here for cross-ref)

The admin allowlist seed is ADMIN_ACCOUNTS=no-reply@swisscairn.ch; the admin panel will manage is_admin thereafter (no Google OAuth). Not in this PR — tracked in #33.

Verification (CI)

  • CGO_ENABLED=0 go build clean, go vet clean, full suite ok.
  • New TestEmailVerificationFlow: register→202 no session, unverified login→403, confirm token→verified, login→200, token single-use. PASS.

Config to turn it on (later, with the frontend)

REQUIRE_EMAIL_VERIFICATION=true, PUBLIC_ORIGIN=https://license.swisscairn.ch, and working SMTP (already used by password reset). Then enter.html needs: show "check your inbox" on the 202, handle #verified / #verify-failed, and a resend button. That frontend companion is the remaining piece before flipping the flag.

Implements #31. **Ships dormant** — enforcement is behind `REQUIRE_EMAIL_VERIFICATION` (default off), so this is safe to merge and deploy now; it activates only when the env flag is set (after the enter.html companion lands). ## Behaviour when `REQUIRE_EMAIL_VERIFICATION=true` - `POST /v1/register` → creates an **unverified** account, emails a confirmation link, returns **202 `{status:"verify_sent"}`** with **no session**. - `GET /v1/verify?token=…` → marks the account verified, redirects to `…/enter.html#verified` (or `#verify-failed`). A GET link, so the emailed URL works with no frontend page. - `POST /v1/verify/resend {email}` → always 204 (no existence oracle). - `POST /v1/login` → 403 `{reason:"unverified"}` until the email is confirmed. - Tokens are memory-only, single-use, 24 h TTL (mirrors password reset). ## When the flag is off (default, and every test) Register returns 201 + session and login is ungated exactly as today — the whole existing suite passes untouched. ## Grandfather A one-time marker-file backfill marks all pre-existing accounts verified at first boot, so enabling the gate never locks out a current customer. ## Auth allowlist (from #33, recorded here for cross-ref) The admin allowlist seed is `ADMIN_ACCOUNTS=no-reply@swisscairn.ch`; the admin panel will manage `is_admin` thereafter (no Google OAuth). Not in this PR — tracked in #33. ## Verification (CI) - `CGO_ENABLED=0 go build` clean, `go vet` clean, full suite ok. - New `TestEmailVerificationFlow`: register→202 no session, unverified login→403, confirm token→verified, login→200, token single-use. PASS. ## Config to turn it on (later, with the frontend) `REQUIRE_EMAIL_VERIFICATION=true`, `PUBLIC_ORIGIN=https://license.swisscairn.ch`, and working SMTP (already used by password reset). Then enter.html needs: show "check your inbox" on the 202, handle `#verified` / `#verify-failed`, and a resend button. That frontend companion is the remaining piece before flipping the flag.
Cordy closed this pull request 2026-08-26 21:06:47 +00:00
Author
Owner

Superseded by #39, which re-applies this onto current main (clean, no conflict, and drops the accidentally-committed binary — now gitignored). Merged. Closing this branch/PR.

Superseded by #39, which re-applies this onto current `main` (clean, no conflict, and drops the accidentally-committed binary — now gitignored). Merged. Closing this branch/PR.
All checks were successful
ci / test-and-build (pull_request) Successful in 20s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#34
No description provided.