Bot checks
Check that a person, not a script, is using your app's sign-up and sign-in forms, with your own Cloudflare Turnstile widget.
A bot check confirms that a person, not a script, is using your app's sign-up and sign-in forms, so nobody can create junk accounts in bulk or have your app mail sign-in links to strangers. Your forms show a Cloudflare Turnstile widget, and Authoric asks Cloudflare whether its token passed before the request runs.
You bring the widget. A Turnstile widget is bound to the domains it was made for, and your app's forms live on your domains, so the site key and secret key are yours.
What it checks
While the check is on, these five requests need a passed token. Every other auth route is unchanged.
| Request | Route |
|---|---|
| Sign up | POST /{appUid}/auth/signup |
| Password sign-in | POST /{appUid}/auth/login |
| Password reset email | POST /{appUid}/auth/request-password-reset |
| Sign-in link email | POST /{appUid}/auth/magic-link/request |
| Sign-in code email | POST /{appUid}/auth/otp/request |
The check is off until you turn it on.
Turn it on
- In the Cloudflare dashboard, open Turnstile and add a widget for your app's domains. Copy its site key and secret key.
- Add the widget to your app's own sign-up and sign-in forms, and send its token with the five requests above (next section). Do this first: once the check is on, a request without a token is refused, sign-in included.
- In the console, open your app, then App settings › Sign-in. Paste both keys, switch on Check for bots, and save.
The secret key is checked with Cloudflare when you save, and a key Cloudflare does not recognize is refused. It is then stored encrypted and is never shown again: the console says only that one is set. Type a new one to replace it. Turning the check off keeps both keys, so you can turn it back on without pasting them again.
Send the token
Your front end learns the site key from GET /{appUid}/auth/methods, so it can render the widget
only when the check is on. botCheck is null while it is off.
{
"password": true,
"magicLink": true,
"emailOtp": true,
"botCheck": { "provider": "turnstile", "siteKey": "0x4AAAAAAA..." }
}Render the widget with that site key, then send the token it produces in the X-Bot-Check header.
A token is good for one request, so reset the widget after each attempt.
curl -X POST "$AUTHORIC_URL/$APP_UID/auth/login" \
-H "Content-Type: application/json" \
-H "X-Bot-Check: $TURNSTILE_TOKEN" \
-d '{ "email": "dana@example.com", "password": "Str0ng!Passphrase" }'The header is the same on all five requests. If your server calls these routes on a user's behalf, forward the token the user's browser produced: the check is on the request, wherever it comes from.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 403 | BOT_CHECK_FAILED | The request has no X-Bot-Check header, or Cloudflare says its token did not pass. | Show the widget again and retry with a fresh token. |
| 503 | BOT_CHECK_UNAVAILABLE | Cloudflare could not be reached to check the token. The request is refused, never let through. | Retry in a moment. |
Saving the keys in the console can answer:
| Status | Code | When |
|---|---|---|
| 400 | BOT_CHECK_SECRET_INVALID | Cloudflare does not recognize the secret key. Copy it again from the widget's settings. |
| 400 | BOT_CHECK_SETTINGS_INVALID | A key is not in Turnstile's format, or the check was switched on without both keys. |
| 503 | BOT_CHECK_UNAVAILABLE | Cloudflare could not be reached to confirm the secret key. Nothing was saved. |
Testing locally
Cloudflare publishes test keys
that work on any domain, localhost included. With the site key 1x00000000000000000000AA and the
secret key 1x0000000000000000000000000000000AA, every check passes; the secret key
2x0000000000000000000000000000000AA makes every check fail. Switch to your real widget's keys
before you go live.