Securing forms in Power Pages is critical to prevent spam submissions. Cloudflare Turnstile is a lightweight, privacy-first alternative to traditional CAPTCHA that integrates seamlessly with web applications. By combining client-side and server-side validation, you can ensure that only legitimate users interact with your portal.
For comparison, you can also check out our guide on adding the built-in CAPTCHA in Power Pages to see how Turnstile differs.
Below is the process of how Cloudflare Turnstile works.
As you can see in the diagram above, for the best security, we need both client-side and server-side validation.
Note
To get started, you’ll need a site key and secret key, which can be generated from the Cloudflare dashboard. The site key is used in your client-side code, while the secret key is required for server-side validation.
Client-Side Setup
First, add the Turnstile script to your portal:
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
defer
></script>We are using explicit rendering because in Power Pages, we cannot directly add HTML to the form. This approach lets us initialize the widget dynamically via JavaScript.
Next, add an HTML element for the widget at the bottom of your form:
<div id="turnstile-container"></div>Since we cannot add HTML directly to the form, you can inject this using JavaScript on form load.
Now, initialize the widget in JavaScript using the following code:
$(document).ready(function() {
// Add the HTML div before initializing the widget
const widgetId = turnstile.render("#turnstile-container", {
sitekey: "YOUR_SITE_KEY",
execution: "execute",
callback: function (token) {
console.log("Success:", token);
$('#sm_turnstiletoken').val(token);
},
"error-callback": function (errorCode) {
console.error("Turnstile error:", errorCode);
$('#sm_turnstiletoken').val("");
},
'expired-callback': function () {
/*
* Note that this token expires in 5 minutes,
* so ensure the form is submitted promptly
* or re-initialize the widget if needed.
*/
console.log("Token expired.");
$('#sm_turnstiletoken').val("");
turnstile.reset(widgetId);
turnstile.execute(widgetId);
}
});
// Initialize the widget
turnstile.execute("#turnstile-container");
});Here, sm_turnstiletoken is the hidden field that stores the token generated after a user passes the challenge. You may need to create this field in your table and add it to the form.
Once implemented, the Turnstile widget will appear on your page, as illustrated below:
Server-Side Validation
On the server side, create a synchronous plugin that runs during pre-validation or pre-operation when a record is being created. This ensures the form is validated before submission is processed.
Use the Cloudflare verification endpoint with your secret key and the token from the client. Cloudflare will respond with a validation status.
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Power365Tips
{
public class ValidateToken: IPlugin
{
private static readonly HttpClient httpClient = new HttpClient();
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
if (!context.InputParameters.Contains("Target") || !(context.InputParameters["Target"] is Entity target))
return;
if (!target.Contains("sm_turnstiletoken"))
return; // No token provided, return or throw error
string token = target.GetAttributeValue<string>("sm_turnstiletoken");
if (string.IsNullOrWhiteSpace(token))
throw new InvalidPluginExecutionException("Captcha token missing.");
bool success = VerifyTurnstile(token).GetAwaiter().GetResult();
if (!success)
throw new InvalidPluginExecutionException("Captcha verification failed. Please try again.");
}
private async Task<bool> VerifyTurnstile(string token)
{
var values = new[]
{
new KeyValuePair<string,string>("secret", "YOUR_SECRET_KEY"),
new KeyValuePair<string,string>("response", token)
};
using (var content = new FormUrlEncodedContent(values))
{
var response = await httpClient.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<TurnstileResponse>(json);
return result != null && result.success;
}
}
// Tunrstile Response class
private class TurnstileResponse
{
public bool success { get; set; }
public string[] errorCodes { get; set; }
}
}
}
As you can see in the code, if the response indicates success, we are allowing the record to proceed. If not, we are throwing a validation error to block the submission.
With Cloudflare Turnstile integrated, your Power Pages forms are more secure and user-friendly. Learn more about the Cloudflare Turnstile in the official documentation.