Here’s how I managed to integrate Cloudflare Turnstile client-side rendering with Weweb, and server-side validation with Xano.
This took some perseverance to get working so I thought others might find this useful to avoid similar pains. Here’s my first tutorial!
I think Cloudflare has become an extremely reputable company and they offer a nice smart CAPTCHA alternative, with the ability to add a widget that is either;
- A non-interactive challenge.
- A non-intrusive interactive challenge (such as clicking a button), if the visitor is a suspected bot.
- An invisible challenge to the browser.
Important note – “the availability of Turnstile is currently in open beta and available as a free tool for all customers. For the beta, customers are limited to 1 million calls to the siteverify verification endpoint per month per site.”
Here is an example of what the widget looks like.
What is the purpose of this?
- Reduce malicious software from creating accounts or logging into applications by running a managed challenge for all site visitors on signup, login, or pages containing forms.
- If the challenge succeeds, pass the token for server-side validation.
- If the server-side validation passes, toggle the login/signup/submit button to ‘enabled’ so the site visitor can proceed.
- Else, prevent the site-visitor from proceeding and potentially engaging in unwanted activity or actions.
Cloudflare Configuration
-
Before you get started, review the Cloudflare Documentation here;
Get started · Cloudflare Turnstile docs -
You’ll need to add your domain or subdomain to your Cloudflare account, within the Turnstile product.
Weweb Configuration
-
Add a HTML element where you want to render the Cloudflare widget.
-
Add an ‘id’ to the HTML attributes.
You’ll need this id to target the element to render the widget, and to get the response token. (I’ve called this ‘cf-turnstile-container’).
NB: This element doesn’t have any other settings such as height etc or any code within the HTML element itself. I experienced some issues where the widget would render outside of the element and therefore resolved to having it appear empty in the editor view. Once published, the widget will render based on the size parameter within the JavaScript.
-
Create a ‘text’ variable to hold the Turnstile widget response.
Copy the ID for this variable as we will update the variable value when the challenge succeeds. (I’ve called this ‘cf-turnstile-widget-response’).
-
Create an ‘object’ variable to store the response from the server-side validation. (I’ve called this ‘cf-site-verify-response’).
-
Create a Global Workflow to execute the server-side validation.
Copy the workflow UID as we will execute the global workflow from the JavaScript code.
In this workflow, I’ve set a simple condition to prevent the Xano endpoint being called without the variable containing a response.
We pass the ‘text’ variable we created to Xano.
We update the ‘object’ variable with the result.data
-
Back on the page/s where you are rendering the widget, include a ‘text’ element that contains the global workflow UID.
The display for this element can be set to :none.
I’m sure there are other methods to include this, but the UID needs to be somewhere on the page. I resolved to doing it this way.
(see related posts here regarding why you need to include the global workflow UID on the page)
I name this element something obvious eg ‘globalWorkflow-UID-of-xanoCallback’
-
Button Configuration
On your button create a state eg ‘btnDisabled’ so that we get visual feedback for whether the button is enabled.
Here we set the state Condition.
This is the snippet used above
// Check the value of the global variable['cf-site-verify-response'] and return true if it's false or undefined, else return false
const variableValue = variables['e482604c-.......-19bbe3848545']?.['success'];
const result = variableValue === false || variableValue === undefined ? true : false;
// Return the result
return result;
We’re changing the Text within the button for an added cue that something is occurring eg. ‘Verifying’. Otherwise simply set the button colour for enabled/disabled.
This is the snippet used above
// Check the value of the global variable['cf-site-verify-response'] and return the text 'Verifying' or 'Sign in' once a successful response has been received
const variableValue = variables['e482604c-........-19bbe3848545']?.['success'];
// Change the button text
const btnText= variableValue === false || variableValue === undefined ? 'Verifying': 'Sign in';
// Return the result
return btnText;
Within the additional settings, we’re disabling the button if the server-side validation response is false or undefined. Else, the button is enabled. (This is the same code as the code we inserted in the btnDisabled Condition)
- Add the Turnstile script and associated code to the Project Settings – Custom Code, Head.
Here’s some things to observe with the code;
- We’re explicitly rendering the Turnstile widget
- Set parameters ‘theme’, and ‘size’ as per your preferences.
- Set parameter ‘sitekey’ using the key located within your Cloudflare Account.
- Replace all occurrences of #cf-turnstile-container to the id you gave your html element.
- Update the text variable value – we use wwLib.wwVariable.updateValue(“INSERT YOUR VARIABLE ID”, response)
- Execute the global workflow - wwLib.executeWorkflow(“INSERT YOUR GLOBAL WORKFLOW UID”)
- MutationObserver – we need to use a MutationObserver function to check if the target html element is present and then initialize the Turnstile.
This is the full code I’ve added to the Custom Code
<!-- Cloudflare Turnstile explicitly render widget -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" async defer>
</script>
<script>
function onloadTurnstileCallback() {
turnstile.render(
turnstile.execute("#cf-turnstile-container", {
theme: "dark",
sitekey: "0x4AA................vC_Xf",
size: "normal",
appearance: "always",
callback: function (token) {
console.log(`Challenge Success ${token}`);
const retryLimit = 8;
let retryCount = 0;
const handleResponseAndRetry = function () {
const response = turnstile.getResponse("#cf-turnstile-container");
console.log(response);
if (response == "") {
if (retryCount < retryLimit) {
retryCount++;
console.log(
`Empty response. Retrying (${retryCount}/${retryLimit})...`
);
setTimeout(handleResponseAndRetry, 1000); // Retry after 1 second
} else {
console.log("Maximum retry attempts reached. Exiting...");
wwLib.executeWorkflow("6b1605eb-................-0d5ad47cf919");
}
} else {
wwLib.wwVariable.updateValue(
"8a799d83-................-6622265f38d4",
response
);
wwLib.executeWorkflow("6b1605eb-................-0d5ad47cf919");
}
};
handleResponseAndRetry();
},
})
);
}
// Function to check if the target element is present and initialize the Turnstile
function checkAndInitialize() {
const targetElement = document.querySelector("#cf-turnstile-container");
if (targetElement) {
onloadTurnstileCallback();
} else {
// If the target element is not found, observe the DOM changes of the document
const observer = new MutationObserver(function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
// Check if the target element is now present in the added nodes
const targetElement = document.querySelector(
"#cf-turnstile-container"
);
if (targetElement) {
// Disconnect the observer once the target element is found
observer.disconnect();
// Load the external script when the target element is present
onloadTurnstileCallback();
break;
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
}
}
// Call the function to check if the target element is present and initialize Turnstile
checkAndInitialize();
</script>
Xano Implementation
-
Access your Secret Key, within your Cloudflare Turnstile account, and store it as an Environment Variable within Xano.
-
Create a single POST endpoint with a single text input
Configure the External API request
URL https://challenges.cloudflare.com/turnstile/v0/siteverify
Create a variable, and return the response result
If the server-side validation succeeds, we’ll get a key/value pair success: true.
This is the property we are targeting on our frontend to enable the login/signup/submit button.
Publish your application and test
Open DevTools Console
If everything is configured correctly, we expect to see
- the widget render, if the challenge succeeds our button will change states to enabled.
- in the console we’ll see a Challenge Success and token.
- In Xano we’ll get a 200 response with success: true
Hope this is of help to others.
Credit and appreciation to @raydeck and @dorilama for guidance and forum contributions that helped me navigate this implementation!