Skip to content

feat(auth): Cloudflare Turnstile on the sign-in page - #172

Merged
clbotdev merged 3 commits into
mainfrom
t3/login-turnstile
Nov 1, 2026
Merged

clbotdev merged 3 commits into
mainfrom
t3/login-turnstile

Conversation

@clbotdev

@clbotdev clbotdev commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

What

The hosted sign-in page (/login, which the dashboard redirects to, and /oauth/login for MCP/OAuth clients) can show a Cloudflare Turnstile check before it emails a sign-in code. Better Auth's captcha plugin verifies the token on /email-otp/send-verification-otp.

  • It's on only when both TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY are set. With either unset, sign-in is unchanged. Both are documented in apps/api/.env.example.
  • Only sending a code is gated. Signing in still needs that code, and the account-deletion code is sent from server code, so neither goes through the check.
  • The widget and token handling live in @codelitdev/oauth-server-kit (feat(oauth-server-kit): optional Cloudflare Turnstile on the hosted sign-in form platform#20). This PR bumps every @codelitdev/* package to 0.6.0 and syncs the managed files with platform-cli 0.6.0.

Why

Without a bot check, anyone can script the sign-in page to make SendLit email codes to arbitrary addresses.

Deploying

  1. Create a Turnstile widget in Cloudflare.
  2. Add the API's hostname (the login page is served by the API, not the dashboard) under Hostname Management. The widget fails with error 110200 otherwise.
  3. Set both keys for qa and prod.

Testing

  • Unit tests for the page wiring. The kit has its own tests for the widget and header.
  • Headless Chromium with Cloudflare's test keys:
    • always-pass key: the token is sent, the server returns 200, and the code form appears
    • always-fail key: the server returns 403 "Captcha verification failed", and no code is sent
    • no token: the server returns 400 MISSING_RESPONSE
  • Real keys, signing in from a normal browser over the tailnet stack: works.
  • Full API suite, conformance, drift, lint and typecheck pass; platform-cli doctor is clean.

Turnstile always challenges automated or embedded browsers with real keys, so automated end-to-end tests need Cloudflare's test keys.

0.6.0 adds the optional Turnstile check to oauth-server-kit's hosted
sign-in form. SendLit only imports mcp-server-kit in its parity test, so
0.5.0's move to MCP SDK 2 needs no product change here.
With TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY set, the hosted login
(/login and /oauth/login) renders the Turnstile widget and Better Auth's
captcha plugin verifies its token on /email-otp/send-verification-otp, so
a script can no longer make SendLit email sign-in codes to arbitrary
addresses. With either key unset, sign-in is unchanged.
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.

2 participants