Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
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
```