Skip to content

feat: email sign-in (one-time code + magic link via Cloudflare Email) - #24

Open
JustWallage wants to merge 4 commits into
mainfrom
email-signin
Open

feat: email sign-in (one-time code + magic link via Cloudflare Email)#24
JustWallage wants to merge 4 commits into
mainfrom
email-signin

Conversation

@JustWallage

Copy link
Copy Markdown
Owner

What & why

Lets new users create an account with any email address, not just a Google account. Identity in this app is already email-only (no users table — createSession(email) works for any email), so a verified email is the account. This is purely additive: the Google button is unchanged.

On the "via Google IdP?" question: Google's IdP only authenticates Google accounts — it can't verify ownership of an arbitrary email — so an independent email-verification flow is required alongside it.

How it works

  • POST /auth/email/request — Turnstile-gated; mints a 6-digit one-time code, stores it as a salted hash (sha256Hex(email:code)), and emails it via the Cloudflare Email Sending binding. The email also carries a magic link (/?login_email=…&login_code=…).
  • POST /auth/email/verify — checks the code and mints the same session cookie as the Google callback.
  • SPA: a two-step email form on the landing page, sharing the Turnstile token with the Google button. The magic link autofills the code, auto-submits, and scrubs the URL — "feels like a magic link."

Decisions

  • [user] 6-digit one-time code + a magic link embedding the code that autofills & signs in immediately.
  • [user] Send via Cloudflare Email Service (send_email binding, no third-party key).
  • [AI] Magic link targets the SPA (so the code visibly autofills); params scrubbed after read.
  • [AI] OTP: 10-min TTL, single-use, 5-attempt cap, 60s resend cooldown; code stored hashed; email normalized (trim+lowercase).
  • [AI] Fake-seam + fail-closed pattern (real sender in prod, no-op fake in local/e2e; 503 if EMAIL_FROM/binding absent), mirroring makeGoogleAuth.
  • [AI] devCode returned in the request response only in local/e2e (prod-absent invariant).
  • [AI] Cookie round-trip proven at the unit level (the e2e env authenticates via test headers, not the cookie); e2e asserts only client-observable UI behavior.

Operational prerequisite

Before this works in production, Cloudflare Email Sending must be enabled for the EMAIL_FROM domain (wrangler email sending enable <domain> + SPF/DKIM/DMARC).

Verification

  • pnpm check green (118 unit tests).
  • pnpm test:e2e green (24 tests, incl. 3 new email-signin specs).
  • SA validation: APPROVED (2 rounds). Review: APPROVED (1 round). Docs under docs/specs/email-signin/.

🤖 Generated with Claude Code

JustWallage and others added 4 commits June 26, 2026 07:44
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds passwordless email sign-in alongside Google: POST /auth/email/request
mints a Turnstile-gated 6-digit code (stored only as a salted hash, 10-min TTL,
5-attempt cap, 60s resend cooldown) and emails it through the Cloudflare Email
Sending binding; POST /auth/email/verify checks it and mints the same session
cookie as the Google callback. The email also carries a magic link that the SPA
reads to autofill the code and sign in immediately.

- db: email_login_codes table + migration 0006
- worker/lib/email-login.ts: OTP issue/verify/purge (fake-seam-free, pure D1)
- worker/lib/email.ts: EmailSender seam (real binding in prod, no-op fake in
  local/e2e; fail-closed 503 when unconfigured)
- shared/api.ts: request/verify/result schemas
- LandingPage.tsx: two-step email form + magic-link autofill, shared Turnstile
- maintenance: purge expired codes nightly
- devCode returned only in local/e2e (prod-absent invariant)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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