How to Implement Cloudflare's Turnstile with React and Cloudflare Workers

This application demonstrates integration of Cloudflare Turnstile CAPTCHA service with a Remix-React application.

Getting Started

Prerequisites

Widget Props (v1.0)

  • theme (dark || light || auto)

Demo Keys (will always verify)

  • Secret Key: 1x0000000000000000000000000000000AA

  • Public Key: 1x00000000000000000000AA

Widget Test Installation

  1. Clone the repository

  2. Install dependencies:

npm install

Set Variables and Deploy Cloudflare Worker

  1. Insert your Turnstile public key into keys.json.

  2. Deploy worker.js on Cloudflare and insert the URL into keys.json.

    {
      "cft_public_key": "ENTER_PUBLIC_KEY",
      "worker_url": "ENTER_WORKER_URL"
    }
    
  3. Set your Turnstile secret key as an environment variable or secret as “CFT_SECRET_KEY” for the Worker.

Test Deployment

Note: Unless you’re using test keys, the widget will not pass the challenge or verify correctly in a local environment.

Local Dev:

npm run dev

Then run the app in production mode:

npm start

Production build for deployment:

npm run build

DIY

If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of npm run build

  • build/server

  • build/client

Detailed Widget Implementation

  1. Input your Cloudflare Turnstile public key into keys.json

    {
      "cft_public_key": "ENTER_PUBLIC_KEY",
      "worker_url": "ENTER_WORKER_URL"
    }
    
  2. Import the turnstile component (turnstile.tsx) and token verification utility (turnstile.ts) into your relevant page. Ensure your paths are correct.

    import { Turnstile } from '~/turnstile/turnstile';
    import { verifyTurnstileToken } from '~/utils/turnstile';
    
  3. Create a Cloudflare Worker under your Cloudflare account.

  4. In your new Worker settings, add an environment secret, your Turnstile secret key as “CFT_SECRET_KEY”.

  5. In your new Worker code, copy and paste worker.js and deploy your worker. Make note of the worker URL, whether you’re using the default address or a custom domain.

  6. For increased security, change the allowed origin in the CORS headers to your domain.

    const CORS_HEADERS = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*', //Change this to your domain
      'Access-Control-Allow-Methods': 'POST',
      'Access-Control-Allow-Headers': 'Content-Type'
    };
    
  7. In keys.json, insert your Worker URL.

    {
      "cft_public_key": "ENTER_PUBLIC_KEY",
      "worker_url": "ENTER_WORKER_URL"
    }
    
  8. Render the Turnstile widget in your form. Retain the success prop if you want the widget to be removed after successful verification.

    return (
    /* YOUR FORM CODE 
    ...*/
            <Turnstile
              theme="dark"
              success={status === 'success'}
              {/* OTHER CUSTOM PROPS */}
            />
    /* YOUR FORM CODE
    ... */
    
  9. Ensure you include server-side token verification using verifyTurnstileToken.

    Do not modify “cf-turnstile-response” - This is the token response from Cloudflare required for token verification.

    /* TOKEN VERIFICATION SHOULD BE FIRST ACTION UPON SUBMIT */
    const token = formData.get('cf-turnstile-response') as string;
        try {
          const result = await verifyTurnstileToken(token);
          if ('success' in result && result.success) {
            /* ENTER SUCCESS LOGIC */
            setStatus('success');
          } 
            /* ENTER FAILURE LOGIC */
            else {
            setStatus('error');
            if ('message' in result) {
              setErrorMessage(result.message || 'Verification failed');
            } else {
              setErrorMessage('Verification failed');
            }
          }
        } catch (error) {
          setStatus('error');
          setErrorMessage('Verification failed');
        }
    /* OTHER FORM LOGIC/SUBMISSION LOGIC... */
    

Questions/Issues

I welcome pull requests and issues. You can also contact me directly.

Updated on