In today's digital landscape, where cyberattacks and spambots are ever-present, securing your web forms is essential. Google reCAPTCHA is one of the most widely used CAPTCHA solutions, especially in Salesforce Marketing Cloud's CloudPages. However, there are other alternatives worth considering, such as Cloudflare Turnstile.
Turnstile verifies that your visitors are real users, effectively preventing abuse without the data privacy concerns or the frustrating user experiences often associated with traditional CAPTCHAs. It’s designed to be more user-friendly and less intrusive than Google's solution.
In this article, we’ll guide you through implementing Cloudflare Turnstile CAPTCHA on your Salesforce Marketing Cloud CloudPages forms.
To begin, create a free account on Cloudflare by visiting their sign-up page. After setting up your account, you'll be directed to the main dashboard. From there, navigate to the "Turnstile" option in the left-hand menu. Register your SFMC CloudPage domain and choose your preferred Widget Mode. Once done, you'll receive a "Site Key" and "Secret Key," which are essential for integrating Turnstile into your form.
For development purposes, Cloudflare also provides "dummy" keys that allow you to test Turnstile without needing an account. These keys are available here. Please note that these test keys have limited functionality and are intended for testing purposes only.
Front-End Interaction
User Interaction: When a user visits a web page with a form protected by Cloudflare Turnstile, the Turnstile widget loads and becomes visible to the user.
Turnstile Widget Display: The user engages with the Turnstile widget, which may involve simple actions to verify they are human. Turnstile conducts background checks to confirm the user’s authenticity without presenting intrusive challenges.
Token Generation: After a successful interaction, Turnstile generates a unique token, which is automatically added to the form as a hidden field.
Form Submission: The user fills out the form and submits it. Along with the form data, the Turnstile token is sent to the backend for further processing.
Back-End Verification
Token Verification: Once the form submission is received, the backend extracts the Turnstile token and sends a verification request to Cloudflare's API, including the token and the secret key.
Cloudflare Verification: Cloudflare processes the request and checks the token’s validity, responding with a result indicating whether the token is valid or invalid.
Form Processing: If the token is valid, the backend processes the form data as usual. If the token is invalid, the backend rejects the submission and may provide feedback to the user.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" defer=""></script>
<form method="post" class="form" id="form" action="">
<label>Email: </label>
<input type="email" name="EmailAddress" required>
<div class="cf-turnstile"
data-sitekey="Site Key"
data-theme="light"
data-callback="handleTurnstileCallback"
data-error-callback="handleTurnstileCallbackError"
data-response-field="true"></div>
<button type="submit" name="submit" disabled>
Send
</button>
</form>
<label for="output">Response: </label>
<input type="text" id="output" name="output">
<script>
/* ------------------------ 1. Global Variables -------------------------- */
const debugging = false;
const button = document.querySelector('form button[type="submit"]');
const output = document.querySelector('#output');
let cfTurnstileResponse;
/* ------------------------ 2. Helper Functions -------------------------- */
/* Unlock submit button if captcha success */
function handleTurnstileCallback(token) {
button.disabled = false;
cfTurnstileResponse = token;
}
function handleTurnstileCallbackError() {
/* Do something here if captcha fails */
alert('CAPTCHA verification failed. Please try again.');
}
/**
* @function request
* @description AJAX handler for POST calls to Code Resource Backend
* @param {Object} payload - request payload with at least action key
*/
function request(payload) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 30 * 1000;
xhr.open('POST', 'YOUR_BACKEND_URL', true);
xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
xhr.onload = () => {
if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 400)) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(xhr.status);
}
};
xhr.onerror = () => {
if (debugging) console.log('Connection Error');
};
xhr.ontimeout = () => {
if (debugging) console.log('Request Timed Out');
};
xhr.send(JSON.stringify(payload));
});
}
/**
* @function pushData
* @description UI handler for POST calls to Code Resource Backend
* @param {Object} payload - prequest payload with at least action key
*/
async function pushData(payload) {
if (debugging) console.log('Processing');
let response = await request(payload);
if (debugging) console.log(`${payload.action} Response: ${JSON.stringify(response)}`);
return response;
}
/* ------------------------- 3. Event Listeners -------------------------- */
document.getElementById('form').addEventListener('submit', function (event) {
event.preventDefault(); // Prevent the form from submitting
pushData({
action: 'captchaTest',
email: document.querySelector("[name='EmailAddress']").value,
cfTurnstileResponse: cfTurnstileResponse
}).then(response => {
if (response.Status === 'OK') {
/* Do something here */
output.value = response.Message;
}
}).catch(error => {
if (debugging) console.log('Error:', error);
});
});
</script>
<script runat="server">
Platform.Load('core', '1');
/* This is the demo secret key */
var cfSecretKey = 'secrectkey';
var result;
/* Capture captcha data from POST body */
if (Platform.Request.Method() == 'POST') {
try {
/* Get the payload from POST call */
var postedData = Platform.Request.GetPostData();
/* Parse Stringified JSON back to Object */
var parsedData = Platform.Function.ParseJSON(postedData);
/* Capture data from POST body */
var action = parsedData.action;
var email = parsedData.email;
var cfToken = parsedData.cfTurnstileResponse;
var ip = Platform.Request.ClientIP();
/* Validate the token by calling the "/siteverify" API endpoint. */
var cfFormData = 'secret=' + cfSecretKey + '&response=' + cfToken + '&remoteip=' + ip;
var cfUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
var cfResult = HTTP.Post(cfUrl, 'application/x-www-form-urlencoded', cfFormData, null, null);
var cfOutcome = Platform.Function.ParseJSON(cfResult.Response[0]);
if (!cfOutcome.success) {
throw { message: 'Invalid Captcha', description: 'Captcha verification failed' };
}
/* Do something here */
result = { Action: 'captchaTest', Status: 'OK', Message: 'Captcha verification succeed'};
} catch (error) {
result = { Action: 'captchaTest', Status: 'Error', Error: error };
}
}
var stringifiedResult = Stringify(result);
</script>
<ctrl:var name=stringifiedResult />