# How Does it work? ## CAPTCHA **[CAPTCHA](https://www.cloudflare.com/learning/bots/how-captchas-work/)** stands for the Completely Automated Public Turing test to tell Computers and Humans Apart. **CAPTCHAs** are tools you can use to **differentiate between real users and automated users, such as bots**. CAPTCHAs provide challenges that are difficult for computers to perform but relatively easy for humans. For example, identifying stretched letters, or clicking on specific items. **reCAPTCHA** is a CAPTCHA system owned by [Google](https://www.google.com/recaptcha/about/). It has two major versions: * **V2 (2014)** issues an interactive challenge to verify if the user interaction is legitimate. ![image](https://www.google.com/recaptcha/about/images/timeline-v2@2x.jpg) * **V3 (2018)** does not have an interactive challenge. It is invisible and verifies requests with a score between 0 (bot) and 1 (human) depending on how likely the user is to be a bot. The website developer can determine what action should be taken based on the score. Google says you can use a default threshold score of 0.5. ➨ Ways of determining bots include user behavior tracking, Google account cookies, etc. | | Pros | Cons | | -- | ------------------ | ------------------ | | V2 | Fairly good at blocking bots. | Bad user experience. Users will waste time solving challenges. | | V3 | Better user experience. | Might resolve in false postives (humans that were incorrectly blocked), especially in Incognito mode or using browsers other than Chrome. There are still chances of false negatives (bots that were incorrectly determined to be humans) for both V2 and V3 though. ## Cloudflare Turnstile [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) is a free tool to replace CAPTCHAs. It was publicly released in September 2023. ![image](https://cf-assets.www.cloudflare.com/slt3lc6tev37/2atsfrGgvgOc3DZ91qMlKN/0412afa63e5fac20964377c70c1a9a17/turnstile_gif.gif) In **managed mode** (we will talk about different modes later in the implementation section): * First, [Cloudflare will check user browser information and user behavior](https://blog.cloudflare.com/turnstile-ga/) to determine if it's likely to be a bot. ![image](https://developers.cloudflare.com/assets/light-verifying_huedf21620df8c5151ce35a9e06afd8400_11815_1113x348_resize_q75_box_3-d0d7057a.png) * If Cloudflare is confident that the user is **likely human**, then a checkmark will appear and **no further action needs to be taken**. ![image](https://developers.cloudflare.com/assets/light-success_hu368dc2c5e67cc74c2191fd29410c934a_12471_1113x348_resize_q75_box_3-80edb044.png) * If Cloudflare **suspects the user to be a bot**, a **visual challenge** is issued where the user has to check a checkmark. During the process, Cloudflare will analyze the behavior to check if it's likely to be a bot. ![image](https://developers.cloudflare.com/assets/light-verify_huefda0166b8c4f9ce140f4dc888d4bac7_12249_1113x348_resize_q75_box_3-d058b656.png) * If it's **likely to be a bot**, then the challenge will **fail**. Otherwise the challenge will pass. ![image](https://hackmd.io/_uploads/r1wY_rmET.png) # How to Implement ## Intro ![image](https://developers.cloudflare.com/assets/turnstile-overview_hu857217e6cfe3055a024af7c1505ed0dc_210985_3757x2700_resize_q75_box_3-3bb896c3.png) The [official docs](https://developers.cloudflare.com/turnstile/get-started/) are quite good so let's take a look there. ## Widget Types There are [three widget types](https://developers.cloudflare.com/turnstile/reference/widget-types/): **Managed**, **Non-Interactive**, and **Invisible**. The Widget type can be set in your Cloudflare Turnstile Settings. ## Example In our example, we are using Cloudflare's recommended **Managed Mode**. Ours demos are located in our `iBuypower.NextJS` project, branch `Cloudflare-Turnstile-Demo`. Cloudflare has [Dummy Keys](https://developers.cloudflare.com/turnstile/reference/testing/) that can be used for local testing. ### Client Side Code #### Custom Hook I've created a custom hook in `hooks\useCloudflareTurnstile.ts`: ```typescript import { useEffect, useState } from 'react'; declare const turnstile: any; // *** DUMMY SITE KEYS *** // -- Always passes const SITEKEY = '1x00000000000000000000AA'; // -- Always blocks // const SITEKEY = "2x00000000000000000000AB"; // -- Forces an interactive challenge // const SITEKEY = "3x00000000000000000000FF"; export enum Execution { Render = 'render', Execute = 'execute', } function printColoredMsg(msg: string, color: string) { console.log(`%c${msg}`, `color: ${color};`); } export default function useCloudflareTurnstile( action: string, containerId: string, execution: Execution) { const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileWidgetId, setTurnstileWidgetId] = useState(''); function prepareTurnstileWidget() { const widgetId = turnstile.render(`#${containerId}`, { sitekey: SITEKEY, language: 'en', action, execution, callback: function (token: string) { printColoredMsg(`${widgetId}: Challenge Success ${token}`, 'mediumseagreen'); setTurnstileToken(token); }, 'expired-callback': function () { printColoredMsg('Expired Callback', 'orangered'); // refresh token setTurnstileToken(''); turnstile.reset(`#${containerId}`); turnstile.execute(`#${containerId}`); }, }); setTurnstileWidgetId(widgetId); printColoredMsg(`--- prepareTurnstileWidget #${containerId} ---`, 'steelblue'); printColoredMsg(`new widgetId #${widgetId} ---`, 'steelblue'); } // Remove Cloudflare Turnstile widget when component unmounts useEffect(() => { return () => { printColoredMsg('--- cleanup Turnstile ---', 'salmon'); if (turnstileWidgetId) { printColoredMsg(`Removed Turnstile widget: ${turnstileWidgetId}`, 'salmon'); turnstile.remove(turnstileWidgetId); } }; }, [turnstileWidgetId]); return { turnstileToken, setTurnstileToken, turnstileWidgetId, setTurnstileWidgetId, prepareTurnstileWidget, }; } ``` --- #### Index Page In `pages\temp\index.tsx`, we have a simple page that has links which will take us to the **implcit render** and **explicit render** demo. ```typescript import Link from 'next/link'; export default function Temp() { return ( <div className="mt-5 text-center"> <h1 className="mb-2 text-2xl">Cloudflare Turnstile Demo</h1> <Link className="mr-5 text-blue-600" href="/temp/implicit-render"> Implicit Render </Link> <Link className="text-blue-600" href="/temp/explicit-render"> Explicit Render </Link> </div> ); } ``` --- #### Implicit Render In `pages\temp\implicit-render.tsx`: ```typescript import useCloudflareTurnstile, { Execution } from '@/hooks/useCloudflareTurnstile'; import axios from 'axios'; import { useRouter } from 'next/router'; import Script from 'next/script'; import { useState } from 'react'; export default function ImplicitRender() { const router = useRouter(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); // for Cloudflare Turnstile const { turnstileToken, prepareTurnstileWidget } = useCloudflareTurnstile( 'implicit-demo', 'implicit-container', Execution.Render ); const submitForm = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (turnstileToken === '') { return; } const resp = await axios.post( '/api/member/temp-cloudflare-turnstile', { email, password, turnstileToken } ); console.log('resp.data', resp.data); if (resp.data.success) { alert('Login success'); router.push('/temp'); } else if (resp.data.message) { alert(resp.data.message); } else { alert('Login failed'); } }; return ( <> <Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer onReady={prepareTurnstileWidget} ></Script> <div className="mt-5 w-full"> <div className="mx-auto w-fit"> <h1 className="mb-4 text-3xl">Cloudflare Turnstile - Implicit Render</h1> <form className="flex flex-col" action="POST" onSubmit={submitForm}> <input className="mb-2 p-1" type="email" placeholder="email" required={true} value={email} onChange={(e) => setEmail(e.target.value)} /> <input className="mb-2 p-1" type="password" placeholder="password" required={true} value={password} onChange={(e) => setPassword(e.target.value)} /> <button className={`mt-4 rounded p-3 ${ turnstileToken === '' ? 'cursor-not-allowed bg-gray-400' : 'bg-yellow-400' }`} > Submit </button> {/* The Turnstile widget will be injected in the following div */} <div className="mt-5" id="implicit-container"></div> </form> </div> </div> </> ); } ``` --- #### Explicit Render In `pages\temp\explicit-render.tsx`: ```typescript import useCloudflareTurnstile, { Execution } from '@/hooks/useCloudflareTurnstile'; import axios from 'axios'; import { useRouter } from 'next/router'; import Script from 'next/script'; import { useState } from 'react'; // for Cloudflare Turnstile declare const turnstile: any; export default function ExplicitRender() { const router = useRouter(); const [showForm, setShowForm] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); // for Cloudflare Turnstile const { turnstileToken, prepareTurnstileWidget } = useCloudflareTurnstile( 'explicit-demo', 'explicit-container', Execution.Execute ); const submitForm = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (turnstileToken === '') { return; } const resp = await axios.post( '/api/member/temp-cloudflare-turnstile', { email, password, turnstileToken } ); console.log('resp.data', resp.data); if (resp.data.success) { alert('Login success'); router.push('/temp'); } else if (resp.data.message) { alert(resp.data.message); } else { alert('Login failed'); } }; return ( <> <Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer onReady={prepareTurnstileWidget} ></Script> <div className="mt-5 w-full"> <div className="mx-auto w-fit"> <h1 className="mb-4 text-3xl">Cloudflare Turnstile - Explicit Render</h1> {!showForm ? ( <button className="mx-auto mt-4 rounded bg-blue-400 p-3" onClick={() => { setShowForm(true); turnstile.execute('#explicit-container'); }} > Show form </button> ) : ( <form className="flex flex-col" action="POST" onSubmit={submitForm}> <input className="mb-2 p-1" type="email" placeholder="email" required={true} value={email} onChange={(e) => setEmail(e.target.value)} /> <input className="mb-2 p-1" type="password" placeholder="password" required={true} value={password} onChange={(e) => setPassword(e.target.value)} /> <button className={`mt-4 rounded p-3 ${ turnstileToken === '' ? 'cursor-not-allowed bg-gray-400' : 'bg-yellow-400' }`} > Click </button> </form> )} {/* The Turnstile widget will be injected in the following div */} {/* NOTE: The injected div must always be here and cannot be conditionally rendered, or else the widget fill fail to be injected */} <div className="mt-5" id="explicit-container"></div> </div> </div> </> ); } ``` --- ### Server Side Code In `pages\api\member\temp-cloudflare-turnstile.ts`; ```typescript import withSession from '@/lib/session'; import axios from 'axios'; export default withSession(async (req, resp) => { // Always passes const SECRET_KEY = '1x0000000000000000000000000000000AA'; // Always fails // const SECRET_KEY = '2x0000000000000000000000000000000AA'; // check email & password const { email, password } = req.body; if (email !== 'a@b.com' || password !== '1234') { resp.status(200).json({ success: false, message: 'Invalid email or password' }); } const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; const external = await axios.post(url, { secret: SECRET_KEY, response: req.body.turnstileToken, }); console.log('**************************'); console.log(req.body); console.log(external.data); resp.status(200).json(external.data); }); ``` --- ### Console Logs We are going to run the project and show the demo in the browser. This is what the normal process console logs will look like: &nbsp; ![image](https://hackmd.io/_uploads/Hy7Wi5ENT.png) &nbsp; Since we are using a dummy Site Key, the widget will return a dummy token that never expires. * However, **real tokens expire after 300 seconds**. * This is what the console logs will look like with using a real Site Key and what happens when a token expires. ![image](https://hackmd.io/_uploads/SkNSWcEE6.png) ## Important Considerations ### Use the Next.js `<Script>` tag if using Next.js - When using Next.js, it is important to use the Next.js `<Script>` tag to load the script. This is because when a user navigates to a new route wihtin our site, the **browser doesn't reload the page**. - If we use a normal html `<script>` tag and navigate this way, the window `onloadTurnstileCallback` function will not be called (see Cloudflare Turnstile offical docs for plain html example). ### DO NOT conditionally render the `<div>` widget container - The `<div>` widget container needs to already be there when the Turnstile script loads, or else the widget iframe will not be able to be injected correctly in the `<div>`. ### Make sure to handle expired tokens - Turnstile tokens `expire after 300 seconds`, so make sure to handle that. The widget has a `expired-callback` that you can use to **refresh tokens**. - Calling `turnstile.execute()` will only return the current token. If `turnstile.reset()` has not been called, it will return the current (and possibly expired) token. Thus, when refreshing a widget, make sure to call `reset()` first and then `execute()`. ### Remove Cloudflare Turnstile widget when component unmounts - In React, this can be used in the cleanup function of useEffect # References **CAPTCHA Info** * https://www.cloudflare.com/learning/bots/how-captchas-work/ * https://datadome.co/bot-management-protection/recaptchav2-recaptchav3-efficient-bot-protection/ **reCAPTCHA** * https://developers.google.com/recaptcha/docs/display * https://developers.google.com/recaptcha/docs/v3 **Cloudflare Turnstile** * https://developers.cloudflare.com/turnstile/