diff --git a/src/content/docs/turnstile/tutorials/conditionally-enforcing-turnstile.mdx b/src/content/docs/turnstile/tutorials/conditionally-enforcing-turnstile.mdx new file mode 100644 index 000000000000000..7b25ab214d9d073 --- /dev/null +++ b/src/content/docs/turnstile/tutorials/conditionally-enforcing-turnstile.mdx @@ -0,0 +1,108 @@ +--- +title: Conditionally enforce Turnstile +pcx_content_type: tutorial +updated: 2025-04-01 +difficulty: Intermediate +languages: + - TypeScript +tags: + - Node.js +sidebar: + order: 6 +--- + +This tutorial explains how to conditionally enforce Turnstile based on the incoming request, such as a pre-shared secret in a header or a specific IP address. + +## Overview + +You may have setups such as automation that cannot load or run the Turnstile challenge. Using [`HTMLRewriter`](/workers/runtime-apis/html-rewriter/), this tutorial will demonstrate how to conditionally handle the [client-side widget](/turnstile/get-started/client-side-rendering/) and [siteverify API](/turnstile/get-started/server-side-validation/) when specific criteria are met. + +:::note +While this tutorial removes Turnstile client-side elements when specific criteria are met, you could instead conditionally insert them. +::: + +:::caution +It is critical to make sure you are validating tokens with the siteverify API when your criteria for enforcing Turnstile are not met. + +It is not sufficient to only remove the client-side widget from the page, as an attacker can forge the request to your API. +::: + +## Implementation + +This tutorial will modify the existing [Turnstile demo](https://github.com/cloudflare/turnstile-demo-workers/blob/main/src/) to conditionally remove the existing `script` and widget container elements. + +```diff title="src/index.mjs" lang="js" +export default { + async fetch(request) { + // ... + ++ if (request.headers.get("x-bypass-turnstile") === "VerySecretValue") { ++ class RemoveHandler { ++ element(element) { ++ element.remove(); ++ } ++ } ++ ++ return new HTMLRewriter() ++ // Remove the script tag ++ .on( ++ 'script[src="https://challenges.cloudflare.com/turnstile/v0/api.js"]', ++ new RemoveHandler(), ++ ) ++ // Remove the container used in implicit rendering ++ .on( ++ '.cf-turnstile', ++ new RemoveHandler(), ++ ) ++ // Remove the container used in explicit rendering ++ .on( ++ '#myWidget', ++ new RemoveHandler(), ++ ) ++ .transform(body); ++ } + + return new Response(body, { + headers: { + "Content-Type": "text/html", + }, + }); + }, +}; +``` + +## Server-side integration + +We will exit early in our validation if the same logic we used to remove the client-side elements is present. + +:::caution +The same logic must be used in both the client-side and the server-side implementations. +::: + +```diff lang="js" title="src/index.mjs" +async function handlePost(request) { ++ if (request.headers.get("x-bypass-turnstile") === "VerySecretValue") { ++ return new Response('Turnstile not enforced on this request') ++ } + // Proceed with validation as normal! + const body = await request.formData(); + // Turnstile injects a token in "cf-turnstile-response". + const token = body.get('cf-turnstile-response'); + const ip = request.headers.get('CF-Connecting-IP'); + // ... +} +``` + +With these changes, Turnstile will not be enforced on requests with the header `x-bypass-turnstile: VerySecretValue` present. + +## Demonstration + +After running `npm run dev` in the project folder, you can test the changes by running the following command: + +```sh +curl -X POST http://localhost:8787/handler -H "x-bypass-turnstile: VerySecretValue" +``` + +```txt output +Turnstile not enforced on this request +```