Cloudflare Turnstile on Auth Pages

Why it's the right call, what it looks like, and how to wire it up via API

Why Put Turnstile on Auth Pages?
Auth pages are the most attacked surfaces on any web app. Turnstile blocks bots without annoying real users.
💥

Credential Stuffing

Bots try leaked email/password combos from data breaches. Login pages are the #1 target. Turnstile stops automated attempts before they hit your auth backend.

🤖

Fake Account Creation

Signup forms without protection get flooded with bot accounts used for spam, abuse, or trial farming. Turnstile ensures only real humans register.

🔐

Brute Force Attacks

Bots systematically guess passwords. Rate limiting helps, but Turnstile adds a layer that stops the attempts before they even reach your rate limiter.

📨

Password Reset Abuse

Bots hammer the "forgot password" endpoint to flood inboxes with reset emails or enumerate valid accounts. Turnstile on reset pages blocks this.

Verdict: Yes, this is a great idea

Auth pages are high-value, low-traffic surfaces — perfect for Turnstile. The UX impact is minimal (often invisible), and the security benefit is massive.

Where to Place Turnstile

Login Page

Critical — #1 target for credential stuffing

Signup Page

Critical — prevents fake account creation

Password Reset

Important — blocks email flooding & enumeration
~

MFA Verification

Optional — already gated, but adds depth
~

Email Verification

Optional — low risk if token is single-use

OAuth Callback

Recommended — prevents token replay bots
Auth Page Mockups with Turnstile
Here's exactly how the Turnstile widget fits into standard login and signup forms.
Login Page
yourapp.com/login

Welcome back

Sign in to your account
user@example.com
•••••••••••
Success!
Cloudflare
or
Signup Page
yourapp.com/signup

Create account

Start your free trial
Jane Doe
jane@example.com
••••••••••••
Verifying you are human...
Cloudflare
or
Password Reset
yourapp.com/forgot-password

Reset password

We'll send you a reset link
user@example.com
Success!
Cloudflare
Invisible Mode (no widget visible)
yourapp.com/login

Welcome back

Sign in to your account
user@example.com
•••••••••••
Turnstile runs invisibly in the background.
No widget shown to the user.
or
UX Best Practices

Place After Last Input

Put the widget between the last form field and the submit button. Users naturally scan downward — this feels seamless.

Disable Submit Until Verified

Grey out the submit button until Turnstile passes. This prevents form submission errors and gives clear feedback.

Use Managed Mode

Let Cloudflare decide when to show a challenge. Most users pass silently. Only suspicious traffic sees the widget.

Handle Expiry Gracefully

Tokens expire after 300s. If the user idles, re-render the widget automatically instead of showing a cryptic error.

Widget Modes Compared
Choose the right mode for your auth pages based on your security vs. UX tradeoff.
Alternative

Non-Interactive

Always shows a widget, but the user never needs to interact. It runs browser checks automatically and shows a spinner then a checkmark.

User sees:
Loading spinner → checkmark
No clicks required
Alternative

Invisible

Completely hidden. No UI element at all. Runs entirely in the background. If it fails, you need to handle the fallback yourself.

User sees:
Nothing at all
Zero visual footprint

For Auth Pages: Use Managed

It gives the best balance — invisible for most users, but challenges bots. Invisible mode works too, but you lose the visual trust signal.

Integration Flow
Step-by-step: create a widget via API, embed it in your auth page, verify on your backend.
📧
Auth
Email
🔑
Auth
Global API Key
🏢
Identity
Account ID

Create Turnstile Widget

API • POST

Register your domain and choose widget mode.

POST /accounts/{account_id}/challenges/widgets { "name": "Auth Pages", "domains": ["yourapp.com"], "mode": "managed" }
1
2

Store Keys Securely

Environment Variables

Save the sitekey (public) and secret (private) as env vars. Never commit the secret to git.

# .env TURNSTILE_SITE_KEY=0x4AAAAAAA... TURNSTILE_SECRET_KEY=0x4AAAAAAB...

Add Widget to Auth Forms

Frontend • HTML

Place the widget between the last input field and the submit button on login, signup, and reset forms.

<!-- After password field, before submit --> <div class="cf-turnstile" data-sitekey="0x4AAAAAAA..." data-callback="onTurnstilePass" data-expired-callback="onExpire" ></div>
3
4

Disable Submit Until Verified

Frontend • JavaScript

Keep the submit button disabled until Turnstile callback fires. Re-disable on token expiry.

function onTurnstilePass(token) { submitBtn.disabled = false; hiddenInput.value = token; } function onExpire() { submitBtn.disabled = true; turnstile.reset(); }

Verify on Backend

Server-side • POST

Before processing login/signup, verify the Turnstile token with Cloudflare. Reject if it fails.

POST https://challenges.cloudflare.com /turnstile/v0/siteverify { "secret": env.TURNSTILE_SECRET_KEY, "response": req.body.turnstileToken, "remoteip": req.ip // optional }
5
6

Proceed or Block

Application Logic

Turnstile passed? Continue with auth. Failed? Return 403, log the attempt, don't reveal why.

if (!turnstileResult.success) { return res.status(403).json({ error: "Verification failed" }); } // Continue with auth logic... authenticateUser(req.body);
API Reference
All Turnstile endpoints. Auth: X-Auth-Email + X-Auth-Key headers on every request.

Turnstile Widget Management

ActionMethodEndpoint
Create widget POST /accounts/{id}/challenges/widgets
List widgets GET /accounts/{id}/challenges/widgets
Get widget GET /accounts/{id}/challenges/widgets/{sitekey}
Update widget PUT /accounts/{id}/challenges/widgets/{sitekey}
Delete widget DELETE /accounts/{id}/challenges/widgets/{sitekey}
Rotate secret POST /accounts/{id}/challenges/widgets/{sitekey}/rotate_secret

Token Verification (Server-side)

ActionMethodEndpoint
Verify token POST https://challenges.cloudflare.com/turnstile/v0/siteverify

Create Widget — Request Body

FieldTypeDescription
namestringHuman-readable widget name
domainsstring[]Allowed domains (e.g. ["yourapp.com"])
modestring"managed" | "non-interactive" | "invisible"
bot_fight_modebooleanEnable bot fight mode (optional)
offlabelbooleanRemove Cloudflare branding (Enterprise only)

Verify Token — Request Body

FieldTypeDescription
secretstringYour Turnstile secret key (required)
responsestringToken from cf-turnstile-response (required)
remoteipstringUser's IP address (optional, improves accuracy)
idempotency_keystringPrevent replay attacks (optional, recommended)