Introduction
This blog post is a tutorial how to build an Episerver Forms validator using the built-in functionality of Episerver Forms combined with Google ReCaptcha v3.
Episerver Forms comes with a ReCaptcha v2 validator which you can read more about on the Episerver website.
The components
We will create a custom Episerver Forms element constisting of a model, view, scripts and validators.
Model
The model is a custom Episerver Forms element. It inherits ValidatableElementBlockBase to make use of Episervers Forms built-in validation functionality. IExcludeInSubmission is used to prevent any validation data from being stored in the the form submission.
[ContentType(GUID = "9758434E-DAFE-48E3-87FE-292B5BC3A313", GroupName = "Custom Elements", DisplayName = "Custom ReCaptcha", Description ="Uses Google ReCaptcha version 3.")]
public class CustomRecaptchaElement : ValidatableElementBlockBase, IExcludeInSubmission
{
[ScaffoldColumn(false)]
[Display(GroupName = SystemTabNames.Content, Order = -5000)]
public override string Validators
{
get
{
var customValidator = typeof(RecaptchaValidator).FullName;
var validators = this.GetPropertyValue(content => content.Validators);
if (string.IsNullOrEmpty(validators))
{
return customValidator;
}
else
{
return string.Concat(validators, EPiServer.Forms.Constants.RecordSeparator, customValidator);
}
}
set
{
this.SetPropertyValue(content => content.Validators, value);
}
}
}
View
The markup and CSS classes derives from the standard Episerver Forms code.
We start by rendering the ReCaptcha script using ClientResources.RequireScript. This ensures the script only loads once regardless of how many instances of the element is being used on a page.
The form element markup is rendered with Html.BeginElement and will remain hidden for the user. Using the built-in Episerver helper will handle validation message when needed.
The script initialization is triggered and unique for each instance of the form element.
@using EPiServer.Forms.Helpers.Internal
@model ExampleWebsite.Models.Forms.Elements.CustomRecaptchaElement
@{
EPiServer.Framework.Web.Resources.ClientResources.RequireScript($"https://www.google.com/recaptcha/api.js?render={ConfigurationManager.AppSettings["GoogleRecaptchaSiteKey"]}").AtHeader();
}
@using (Html.BeginElement(Model, new { @class = "FormHidden" }))
{
<input class="Form__Element" type="hidden" id="@Model.FormElement.Guid" name="@Model.FormElement.ElementName" />
@Html.ValidationMessageFor(Model)
}
<script>
window.addEventListener("load", function () {
if (window.jQuery) {
initializeRecaptcha("@Model.FormElement.Guid", "@ConfigurationManager.AppSettings["GoogleRecaptchaSiteKey"]");
}
});
</script>
<div class="Form__Element">
<p>This form uses Google ReCaptcha v3.</p>
</div>
Script
The ReCaptcha ready() function extends the built-in Episerver Forms validation. The client-side validation will always return true since real validation will happen server-side in the element validator. Otherwise it would go out of sync with revalidation triggered at a two minute interval.
Token timeout and validation
The initializeRecaptcha() function is a work-around for the ReCaptcha v3 timeout on the token. Once created, the token will only be valid for two minutes. This creates a problem when a user takes longer than two minutes to submit the form since loading the page.
Our solution is to regenerate the token every two minutes (spare a couple miliseconds) with the recursive function.
if (typeof grecaptcha !== "undefined") {
grecaptcha.ready(function () {
if (epi !== undefined && epi.EPiServer !== undefined && epi.EPiServer.Forms !== undefined) {
$.extend(true, epi.EPiServer.Forms,
{
Validators:
{
"ExampleWebsite.Business.Forms.Validation.RecaptchaValidator": function (fieldName, fieldValue, validatorMetaData) {
//Recaptcha token has a two minute time out, and we cannot generate the token before the form submission. Real validadation happen server-side.
//Consider valid clientside as this would otherwise go out of sync with revalidation triggered at a 2 minute interval.
return { isValid: true };
},
},
});
}
});
}
function initializeRecaptcha(fieldId, siteKey) {
function revalidateCaptcha(delayMs) {
setTimeout(function () {
grecaptcha.execute(siteKey, { action: 'homepage' }).then(function (token) {
document.getElementById(fieldId).value = token;
//Recaptcha token has a two minute time out, and we cannot generate the token before the form submission. Thus we re-generate the token every other minute.
revalidateCaptcha((1000 * 60 * 2) - 5);
});
}, delayMs);
}
revalidateCaptcha(0);
}
Episerver Forms element validator
The form element validator is very basic and utilizes the built-in Episerver Forms validator functionality with the base class ElementValidatorBase. In the Validate() method the token is passed to the ReCaptcha validator.
public class RecaptchaValidator : ElementValidatorBase
{
public override bool? Validate(IElementValidatable targetElement)
{
var recaptchaElment = targetElement as CustomRecaptchaElement;
if (recaptchaElment == null)
{
return false;
}
var token = recaptchaElment.GetSubmittedValue() as string;
return RecaptchaHelper.Validate(token);
}
public override bool AvailableInEditView => false;
public override IValidationModel BuildValidationModel(IElementValidatable targetElement)
{
var model = base.BuildValidationModel(targetElement);
if (model != null)
{
model.Message = "Failed captcha validation";
}
return model;
}
}
ReCaptcha validator
The ReCaptcha validator posts the token and secret to the Google service for validation. The validation is based on the returned score which passes true or false back to the Episerver Forms element validator.
public static class RecaptchaHelper
{
private static readonly ILogger Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private static string Secret => ConfigurationManager.AppSettings["GoogleRecaptchaSecret"];
public static bool Validate(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return false;
}
try
{
var httpClient = new HttpClient();
var res = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={Secret}&response={token}").Result;
if (res.StatusCode != HttpStatusCode.OK)
{
return false;
}
string JSONres = res.Content.ReadAsStringAsync().Result;
dynamic JSONdata = JObject.Parse(JSONres);
if (JSONdata.success.ToString().ToLower() != "true")
{
return false;
}
if (float.TryParse(JSONdata.score.ToString(), out float score) && score >= 0.5)
{
return true;
}
}
catch (Exception ex)
{
Log.Error("Recaptcha validation threw an error!", ex);
}
return false;
}
}
Final words
This tutorial highlights the strengths and extendability if the built-in Episerver Forms functionality. By inheriting base classes and extending functions we are able to build a robust editor- and user-friendly solution for implementing ReCaptcha in Episerver Forms.