Creating a reCAPTCHA v3 Custom Form Component in Xperience MVC

Let’s say you want to safeguard your site's forms from spam by utilizing reCAPTCHA v3 so that you don’t have to render the reCAPTCHA challenge in your nicely designed form that you built with Xperience’s Form Builder. Enter custom form components. Custom form components allow you to define new form components that can be used by content administrators when building new forms in Form Builder. In this blog post, I will show you how we can leverage custom form components to add google’s reCAPTCHA v3 via client-side code and validate the key that is returned by the reCAPTCHA API on the server side.

Creating a Custom Form Component

Most of the general setup for creating a custom form component is outlined in the Xperience documentation, so if you are looking to create other custom form components, that is a good place to start. For this blog post, I will be showing specifically how to implement a reCAPTCHA v3 custom form component.

The first step will be to create a folder called “FormComponents” to hold all the classes related to your custom form component(s). This needs to be at the root of your Web project, ~/Models/FormComponents.

Create a Form Component Properties Class

Next, create a “FormComponentProperties” folder within the “FormComponents” (/Models/FormComponents/FormComponentProperties) folder and then create a new InvisibleReCaptchaV3Properties properties class.

A couple of things to note about the code snippet below.

SettingsKeyInfoProvider.GetValue("reCaptchaV3SiteKey", SiteContext.CurrentSiteID) is using CMS.DataEngine to retrieve custom site settings that are defined in the Xperience admin. To create these custom settings, navigate to the Modules application in the Xperience admin. If you don’t already have a Custom module for your site, create one. Edit the Custom module and go to Settings in the left sidebar. Once you are in the Settings, create a “New settings group” titled reCAPTCHA v3 Keys (or whatever you would like to call it) and within that group, create two new settings keys with code names reCaptchaV3SiteKey and reCaptchaV3SecretKey. The code name of these settings keys need to match the first parameter that you pass to the SettingsKeyInfoProvider.GetValue:

SettingsKeyInfoProvider.GetValue("reCaptchaV3SiteKey", SiteContext.CurrentSiteID); SettingsKeyInfoProvider.GetValue("reCaptchaV3SecretKey", SiteContext.CurrentSiteID)

using CMS.DataEngine;
using CMS.SiteProvider;
using Kentico.Forms.Web.Mvc;

namespace DancingGoat.FormBuilder.FormComponents.FormComponentProperties
{
    public class InvisibleReCaptchaV3Properties : FormComponentProperties
    {
        public InvisibleReCaptchaV3Properties()
            : base(FieldDataType.LongText, size: 1000)
        {
            DefaultValue = "";
            GoogleSecretKey = _googleSecretKey;
        }

        static string _googleSiteKey = "";
        static string _googleSecretKey = GoogleSecretKey;


        public static string GoogleSiteKey
        {
            get
            {
                return SettingsKeyInfoProvider.GetValue("reCaptchaV3SiteKey", SiteContext.CurrentSiteID);

            }
            set
            {
                _googleSiteKey = value;
            }
        }

        public static string GoogleSecretKey
        {
            get
            {
                return SettingsKeyInfoProvider.GetValue("reCaptchaV3SecretKey", SiteContext.CurrentSiteID);
            }
            set
            {
                _googleSecretKey = value;
            }
        }

        [DefaultValueEditingComponent(TextAreaComponent.IDENTIFIER)]
        public override string DefaultValue
        {
            get;
            set;
        }
    }
}

Create a Form Component Class

Next, directly within the ~/Models/FormComponents folder, create a InvisibleReCaptchaV3Component.cs form component class. At the top of this file, you will need to register the form component using the registration attribute. Here you can define the display name, description, and the icon that are used to display the component in the Xperience admin.

using CMS.DataEngine;
using CMS.Helpers;
using CMS.SiteProvider;
using DancingGoat.FormBuilder.FormComponents;
using DancingGoat.FormBuilder.FormComponents.FormComponentProperties;
using Kentico.Forms.Web.Mvc;

// Registers a form component for use in the form builder
[assembly: RegisterFormComponent(InvisibleReCaptchaV3Component.IDENTIFIER, typeof(InvisibleReCaptchaV3Component), "Invisible ReCaptcha Component - V3", Description = "This is a custom invisible ReCaptcha v3 component", IconClass = "icon-recaptcha")]

namespace DancingGoat.FormBuilder.FormComponents
{
    public class InvisibleReCaptchaV3Component : FormComponent
    {
        public const string IDENTIFIER = "InvisibleReCaptchaV3Component";

        public string GoogleSiteKey
        {
            get
            {
                var defaultValue = SettingsKeyInfoProvider.GetValue("reCaptchaV3SiteKey", SiteContext.CurrentSiteID);
                return defaultValue;
            }
            set
            {
                GoogleSiteKey = value;
            }
        }

        public string GoogleSecretKey
        {
            get
            {
                return SettingsKeyInfoProvider.GetValue("reCaptchav3SecretKey", SiteContext.CurrentSiteID);
            }
            set
            {
                GoogleSecretKey = value;
            }
        }


        // Specifies the property is used for data binding by the form builder
        [BindableProperty]
        // Used to store the value of the input field of the component
        public string Value { get; set; }

        // Gets the value of the form field instance passed from a view where the instance is rendered
        public override string GetValue()
        {
            return Value;
        }

        // Sets the default value of the form field instance
        public override void SetValue(string value)
        {
            Value = value;
        }
    }
}

Create a Partial View for the Form Component

Next, we will create a partial view, which is responsible for rendering the form component. In our case, we are only using the form input to hold the reCAPTCHA token value. Since the user does not need to see or know about the reCAPTCHA token, we will add a hidden class to hide the input from view.

@{ 
    htmlAttributes["class"] += " hidden";
    htmlAttributes["id"] = @Model.BaseProperties.Guid + "-captchaToken";
    htmlAttributes["data-captchaToken"] = "";
    htmlAttributes["value"] = "";
}

We will then use the value of the input (which will be the reCAPTCHA token) on the server side to validate the token. This is also where we will add the JavaScript that will programmatically invoke the reCAPTCHA challenge. If you would like to read more about reCAPTCHA or need to know how to get a reCAPTCHA site key and secret key, here is a link to Google’s reCAPTCHA documentation.

Next, you will want to create a new JavaScript file that will hold all of our logic for registering the reCAPTCHA. If you follow along in the JavaScript, the first thing we do when the window loads is call the getFormOnSubmitFunction and removeFormOnSubmitFunction functions. Those calls then get the value of the inline onsubmit function that is on the <form> element, sets it to a window object property to be referenced later, and then removes it from the form component. This allows us to programmatically invoke the reCAPTCHA challenge and validate the token before the Xperience form is submitted. We also add an event listener to listen for submit events. When a submit event occurs, the onClick function is called. This function contains the code that programmatically invokes the reCAPTCHA challenge.

Be sure to add this JavaScript file to your partial view that is responsible for rendering all of your site-wide JavaScript bundles in the footer of the HTML.

const siteKey = window.reCaptchaSiteKey;
const reCaptchaElements = document.querySelectorAll('[data-captchaguid]');
const reCaptchaFormComponentGuids = [];
reCaptchaElements.forEach(element => {
  const guid = element.getAttribute('data-captchaguid');
  reCaptchaFormComponentGuids.push(guid);
});

function initReCaptchaV3() {
  main();
}

function onClick(e) {
  e.preventDefault();
  e.stopPropagation();

  const formGuid = getFormGuid(e.submitter.form.id);

  if (document.getElementById(`${formGuid}-captchaToken`).value !== '') {
    return false;
  }

  grecaptcha.ready(() => {
    grecaptcha
      .execute(siteKey, { action: 'submit' })
      .then(token => {
        document.getElementById(`${formGuid}-captchaToken`).value = token;

        // trigger original click event
        if (e.submitter !== undefined) {
          addFormOnSubmitFunction(e.submitter.form.id);
        }
      })
      .catch(error => {
        console.error(error);
      });
  });
}

function getFormGuid(formId) {
  const formElement = document.getElementById(formId);
  const formGuid = formElement
    .querySelectorAll('[data-captchaguid]')[0]
    .getAttribute('data-captchaguid');

  return formGuid;
}

function getFormOnSubmitFunction() {
  reCaptchaFormComponentGuids.forEach(guid => {
    const captchaElement = document.getElementById(`${guid}-captchaToken`);
    const parentForm = captchaElement.closest('form');
    const onSubmitFunction = parentForm.getAttribute('onsubmit');

    window[`${guid}-submitFunction`] = onSubmitFunction;
  });
}

function removeFormOnSubmitFunction() {
  reCaptchaFormComponentGuids.forEach(guid => {
    const captchaElement = document.getElementById(`${guid}-captchaToken`);
    const parentForm = captchaElement.closest('form');

    parentForm.removeAttribute('onsubmit');
  });
}

function addFormOnSubmitFunction(eventSubmitterFormId) {
  const formElement = document.getElementById(eventSubmitterFormId);
  const formGuid = getFormGuid(eventSubmitterFormId);

  const submitFunction = window[`${formGuid}-submitFunction`];
  formElement.setAttribute('onsubmit', submitFunction);

  const submitButton = formElement.querySelectorAll("input[type='submit']")[0];
  submitButton.click();
}

function main() {
  window.addEventListener('load', () => {
    getFormOnSubmitFunction();
    removeFormOnSubmitFunction();
  });

  document.addEventListener('submit', onClick);
}

initReCaptchaV3();

If the grecaptcha.execute() method is successful, it returns a reCAPTCHA token. We need to validate this token on the server side. In order to have access to that token, we set it to the value of the reCAPTCHA custom form component input that we created. Since we created a new validation rule (see “Create a New Validation Rule” section), as long as we select this validation rule in Form Builder, the Validate() method will get called with the input’s value passed in.

One thing to note here is that we are adding a hidden class to the input. You can name your class whatever you would like or use an existing utility class that you have. This hidden class adds display: none to the input so that the element is not displayed and has no effect on the layout of the page.

@using Kentico.Forms.Web.Mvc

@model DancingGoat.FormBuilder.FormComponents.InvisibleReCaptchaV3Component

@{
    // Gets a collection of system HTML attributes necessary for the functionality of form component inputs
    IDictionary htmlAttributes = ViewData.GetEditorHtmlAttributes();

}

@{ 
    htmlAttributes["class"] += " hidden";
    htmlAttributes["id"] = @Model.BaseProperties.Guid + "-captchaToken";
    htmlAttributes["data-captchaToken"] = "";
    htmlAttributes["value"] = "";
}

@Html.TextAreaFor(m => m.Value, htmlAttributes)
@{
    <script src="https://www.google.com/recaptcha/api.js?render=@Model.GoogleSiteKey" type="text/javascript"></script>

    <script type="text/javascript" defer>window.reCaptchaSiteKey = `@Model.GoogleSiteKey`;</script>
}

Create a New Validation Rule

At the root of your Web project, create a ReCaptchaV3Validation.cs file. This file will go in a new ~/FormBuilder/ValidationRules folder. This creates a new validation rule that is available to select for a form component in Form Builder. When the form is submitted, the Validate() method will be called with the input’s value passed in. The value is the reCAPTCHA token that we can pass to the reCAPTCHA API, along with your site secret key. If the validation is successful, google returns a score between 0.0 and 1.0. 0 being that the interaction on your site was very likely a bot, and 1.0 indicating that the interaction was very good and is most likely a human. That’s it! The only step left is to add your new custom form component to a form in Xperience Form Builder.
using System;
using System.Net;
using System.Net.Http;
using CMS.EventLog;
using DancingGoat.FormBuilder.FormComponents.FormComponentProperties;
using DancingGoat.Web.FormBuilder.ValidationRules;
using Kentico.Forms.Web.Mvc;
using Newtonsoft.Json.Linq;

[assembly: RegisterFormValidationRule("ReCaptchaV3Validation", typeof(ReCaptchaV3Validation), "ReCaptcha V3 Validation", Description = "Validate reCaptcha V3 token.")]

namespace DancingGoat.Web.FormBuilder.ValidationRules
{
    [Serializable]
    public class ReCaptchaV3Validation : ValidationRule
    {
        public override string GetTitle()
        {
            return "ReCaptcha V3 Validation";
        }


        class Data
        {
            public string Secret { get; set; }
            public string Response { get; set; }
        };

        protected override bool Validate(string value)
        {
           if ( value == null || String.IsNullOrEmpty(value.ToString()))
            {
                return false;
            }

            var gRecaptchaResponse = value.ToString();

            value = "";

            return ValidateToken(InvisibleReCaptchaV3Properties.GoogleSecretKey, gRecaptchaResponse);

        }

        public static bool ValidateToken(string googleSecretKey, string gRecaptchaResponse)
        {
            HttpClient httpClient = new HttpClient();

            HttpResponseMessage res = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={googleSecretKey}&response={gRecaptchaResponse}").Result;

            if (res.StatusCode != HttpStatusCode.OK)
            {
                return false;
            }
            else
            {
                string JSONres = res.Content.ReadAsStringAsync().Result;

                // This is here so we can detect patterns. If too much spam is getting thru, we might need to go off of score
                EventLogProvider.LogInformation("GoogleReCaptchaV3", "Result", JSONres);

                dynamic JSONdata = JObject.Parse(JSONres);

                if (JSONdata.success != "true")
                {
                    return false;
                }

                return true;
            }
        }
    }
}

Add Your Custom Form Component to a Form

The last step of the process is to add your new form component to a form on your site via Form Builder. Navigate to the Forms application in the Xperience admin, create a new form or edit an existing one, and click on Form Builder. Click on the “+” button to add a new form component. You should see your new form component listed here; select it. Next, click on the Validation tab in the right sidebar, and you should see the custom validation rule you created previously in the dropdown menu. You now have a fully functioning reCAPTCHA v3 implementation that is invisible to the user but protects your form from bots!

image of form components in Kentico

Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Tyler Stirtz

About the author

Tyler began his career in healthcare but quickly realized he was missing out on the creativity and logical aspect that computer programming provides. Shortly after that realization, Tyler decided to pursue his long-running interest in coding as a career, and he loves every second of it! Outside of BizStream, you can find Tyler backpacking, coding up a new project at home, playing video games, or exploring new breweries with friends. 

View other posts by Tyler

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.