Skip to content

feat(oauth-server-kit): optional Cloudflare Turnstile on the hosted sign-in form - #20

Merged
s3rot0n1n merged 1 commit into
mainfrom
feat/oauth-kit-turnstile
Nov 1, 2026
Merged

s3rot0n1n merged 1 commit into
mainfrom
feat/oauth-kit-turnstile

Conversation

@clbotdev

@clbotdev clbotdev commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

What

createOAuthPagesRouter takes an optional captcha: { provider: "cloudflare-turnstile", siteKey }. When it's set, the email form on /login and /oauth/login renders a Turnstile widget and sends the token in the x-captcha-response header when it asks for a sign-in code (/email-otp/send-verification-otp).

  • If the person submits before the check finishes, the page shows "Complete the security check, then try again." and sends nothing.
  • The widget is reset after every attempt, because each Turnstile token works only once.
  • The router only collects the token. Products verify it with Better Auth's captcha plugin on the same endpoint (documented in integration.md).
  • Without captcha, the pages are unchanged.
  • The site key is validated when the router is created, and the new type HostedLoginCaptcha is exported from /express.

Why

Without a bot check, anyone can script the hosted sign-in to send codes to arbitrary addresses. SendLit needs this first.

Testing

  • bun test in packages/oauth-server-kit: 113 pass. New tests cover widget rendering on both pages, refusing to submit without a token, sending the header, and the unchanged no-captcha pages. Coverage stays above the gate.
  • check-types and Biome are clean.
  • End to end in SendLit, with a packed tarball of this branch and Better Auth's captcha plugin:
    • Cloudflare's always-pass test key: the token reached the header, the server returned 200, and the code form appeared.
    • Always-fail test key: the server returned 403 and the page showed "Captcha verification failed".
    • Real keys in a browser: sign-in succeeded.

Product changes

Listed in the changeset (minor): pass captcha to createOAuthPagesRouter and add Better Auth's captcha plugin with endpoints: ["/email-otp/send-verification-otp"]. Configure both or neither.

…ign-in form

createOAuthPagesRouter takes captcha: { provider: "cloudflare-turnstile",
siteKey }. The email form renders the Turnstile widget and sends its token in
the x-captcha-response header on the send-code request, which Better Auth's
captcha plugin verifies. The token is reset after each attempt because
Turnstile tokens are single-use.
@s3rot0n1n
s3rot0n1n merged commit 4fcb165 into main Nov 1, 2026
1 check 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.

2 participants