Boost Your Security: Implementing reCAPTCHA v3

Boost Your Security: Implementing reCAPTCHA v3

Boost Your Security: Implementing reCAPTCHA v3

Why do we need reCAPTCHA?

Marketing Cloud forms, as any other web forms, can be copied and moved to another domain, where an automated script would submit the form on a fixed schedule, thus creating a huge number of fake records in Marketing Cloud for spamming or hacking purposes.

Google reCAPTCHA prevents this kind of operation and provides an additional layer of security against malicious form submissions.

reCAPTCHA v2:

  1. User Interaction:-

    • v2 typically involves a user-facing challenge, such as clicking a checkbox or solving an image puzzle. Users actively participate in proving they are not robots.
  2. Human-friendly:-

    • The challenges presented in v2 are designed to be more user-friendly and might include straightforward tasks like selecting images with specific objects.
  3. Ease of Implementation:-

    • Implementation of v2 is generally straightforward, and there are different versions available, including the “Invisible reCAPTCHA” that works in the background without explicit user interaction.
  4. Customization:-

    • v2 allows some customization in terms of the difficulty of challenges and the appearance of the reCAPTCHA widget.

 

reCAPTCHA v3:

  1. No User Interaction:-

    • v3 operates in the background without any user-facing challenges. Users do not need to actively interact with a checkbox or solve puzzles.
  2. Score-Based:-

    • v3 provides a score based on user behaviour, allowing you to set custom thresholds for what you consider suspicious or bot-like activity.
  3. Continuous Monitoring:-

    • Since v3 continuously monitors user behaviour throughout the session, it may provide a more comprehensive analysis of user activity.
  4. Adaptive and Contextual:-

    • The adaptive nature of v3 makes it well-suited for dynamic environments, adjusting to different contexts and varying levels of potential threats.

How will it work?

To successfully implement reCAPTCHA v3 in Marketing Cloud, we first need to create a custom flow using the Form Handler technique.

In this flow, the page with the form will submit the data to the Form Handler page, which will process the data.

Before we start:-

As obvious as it seems, to start implementing Google reCAPTCHA, we need to create a Google account.

Once you have it, please proceed to the official reCAPTCHA page and register for a new website.

Add your Marketing Cloud domain to the domain list and choose reCAPTCHA v3 as the reCAPTCHA type.

After the registration is complete, you will be presented with 2 access keys:
site key and secret key.

Please copy these keys and paste them later in the code, whenever you come across these lines of code: {{ RECAPTCHA SITE KEY }} and {{ RECAPTCHA SECRET KEY }}

Implementation methods:-

There are 2 methods for the reCAPTCHA v3 implementation:

  1. Binding reCAPTCHA automatically to the submit button.
  2. Invoking reCAPTCHA manually.

Automatic binding method:-

This method is very simple. All we need is to call the reCAPTCHA script in the head tag and add some custom attributes to the submit button.

Form page:-

When it comes to setting up a simple HTML5 form validation with some basic styles, Bootstrap is the way to go.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link 
        rel="stylesheet" 
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
        crossorigin="anonymous"
    >
    <script src="https://www.google.com/recaptcha/api.js?hl=en"></script>
</head>
<body>
    <div class="container">
        <div class="mx-auto my-5">
                <form id="form" class="needs-validation my-4" action="{{ URL TO FORM HANDLER }}"
                    method="post">
                    <div class="input-group input-group-lg">
                        <input 
                            class="form-control" 
                            type="email" 
                            minlength="3" 
                            maxlength="254" 
                            placeholder="Enter your email..." 
                            pattern="^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,})$"
                            required
                        >
                        <div class="invalid-feedback order-last">
                            Please enter a valid email address.
                        </div>
                        <div class="input-group-append">
                            <button 
                                class="g-recaptcha btn btn-primary" 
                                data-sitekey="{{ RECAPTCHA SITE KEY }}"
                                data-callback='onSubmit' 
                                data-action='submit'
                            >Send</button>
                        </div>
                    </div>
                </form>
        </div>
    </div>
    <script>
        var form = document.getElementById('form');
        function onSubmit() {
            if(form.checkValidity()) form.submit();
            form.classList.add('was-validated');
        }
    </script>
</body>
</html>

When the user clicks on the submit button, reCAPTCHA generates a token and puts it in an invisible form field named g-recaptcha-response.

Then, it executes a function, referenced in the data-callback attribute of the submit button.

In the example below, the data-callback attribute calls the onSubmit function, which verifies the validity of the form and submits the form if all the fields are valid.

Form Handler page:-

The Form Handler page in this example has only one purpose: to verify the validity of the reCAPTCHA token sent by the form page.

This is what we call server-side validation.

<script runat='server'>
    Platform.Load('core', '1');
    try {

        var g_recaptcha_response = Request.GetFormField("g-recaptcha-response");
        var secret = "{{ RECAPTCHA SECRET KEY }}";
        var payload = "secret=" + secret + "&response=" + g_recaptcha_response;
        var req = HTTP.Post('https://www.google.com/recaptcha/api/siteverify', 'application/x-www-form-urlencoded', payload);

        if (req.StatusCode == 200) {
            var resp = Platform.Function.ParseJSON(String(req.Response));
			if(!resp.success) throw "reCAPTCHA request returned an error";
        	if(resp.score < 0.5) throw "reCAPTCHA score is low, probably a SPAM";
        } else {
            throw "reCAPTCHA API error";
        }

        Write(Stringify(resp));

    } catch (error) {
        Write(Stringify({ status: "Error", message: error }));
    }
</script>

 

Invoking reCAPTCHA manually (POST method):-

The invoking method is rather complicated but it allows for a more custom approach regarding the JavaScript code.

Form:-

To make it work, every step of the process needs to be triggered manually in the JavaScript:

  1. Add event listener for the form submission.
  2. Execute reCAPTCHA when ready.
  3. Once executed, add the token to the g-recaptcha-response field.
  4. Submit the form if all the fields are valid.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link 
        rel="stylesheet" 
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
        crossorigin="anonymous"
    >
    <script src="https://www.google.com/recaptcha/api.js?render={{ RECAPTCHA SITE KEY }}&hl=en"></script>
</head>
<body>
    <div class="container">
        <div class="mx-auto my-5">
            <form 
				id="form"
				class="needs-validation my-4"
                action="{{ URL TO FORM HANDLER }}"
				method="post"
			>
                <div class="input-group input-group-lg">
                    <input 
						class="form-control" 
						type="email" 
						minlength="3" 
						maxlength="254"
                        placeholder="Enter your email..." 
						pattern="^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,})$"   
						required
					>
                    <div class="invalid-feedback order-last">
                        Please enter a valid email address.
                    </div>
                    <div class="input-group-append">
                        <button id="send" class="btn btn-primary">Send</button>
                    </div>
                </div>
                <input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response">
            </form>
        </div>
    </div>
    <script>
        var form = document.getElementById('form');
        var recaptcha = document.getElementById('g-recaptcha-response')

        form.addEventListener('invalid', function (e) {
            e.preventDefault();
            this.classList.add('was-validated');
        }, true);

        form.addEventListener('submit', function (e) {
            e.preventDefault();
            grecaptcha.ready(function () {
                grecaptcha.execute(
                    '{{ RECAPTCHA SITE KEY }}',
                    { action: 'submit' }
                ).then(function (token) {
                    recaptcha.value = token;
                    form.submit();
                });
            });
        }, true);
    </script>
</body>
</html>

 

Form Handler page:-

The Form Handler code doesn’t change, but let’s try to use AMPscript this time to make it interesting.

%%[

    SET @Recaptcha = RequestParameter('g-recaptcha-response')
    SET @RecaptchaSecret = "{{ RECAPTCHA SECRET KEY }}"
    SET @RecaptchaURL = "https://www.google.com/recaptcha/api/siteverify"
    SET @RecaptchaPayload = CONCAT("secret=",@RecaptchaSecret,"&response=",@Recaptcha)
    
    SET @RecaptchaRequest = HTTPPost(@RecaptchaURL,"application/x-www-form-urlencoded", @RecaptchaPayload, @RecaptchaResponse)

    SET @SuccessRegEx = '"success": (true)'
    SET @RecaptchaSuccess = RegExMatch(@RecaptchaResponse, @SuccessRegEx, 1)

    SET @ScoreRegEx = '"score": ([-+]?\d*\.?\d*)'
    SET @RecaptchaScore = RegExMatch(@RecaptchaResponse, @ScoreRegEx, 1)

    IF EMPTY(@RecaptchaSuccess) THEN
        OUTPUTLINE(CONCAT("reCAPTCHA request returned an error"))
    ENDIF

    IF NOT EMPTY(@RecaptchaScore) AND ADD(@RecaptchaScore,0) < 0.5 THEN
        OUTPUTLINE(CONCAT("reCAPTCHA score is low, probably a SPAM"))
    ENDIF

]%%

%%=v(@RecaptchaResponse)=%%

 

Invoking reCAPTCHA manually (AJAX method):-

This method showcases how to send the data from the form using AJAX.

Form page:-

All the different steps of method #1 remain the same, except for the last one.

Instead of submitting the form, 2 processes will be triggered:

  1. Collect the field values along with the reCAPTCHA token in a JSON object.
  2. Send the JSON object to the Form Handler using AJAX (Fetch in this case).

To make it simple, the response sent back from the Form Handler will be simply displayed with an alert window.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link 
        rel="stylesheet" 
        href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
        crossorigin="anonymous"
    >
    <script src="https://www.google.com/recaptcha/api.js?render={{ RECAPTCHA SITE KEY }}&hl=en"></script>
</head>
<body>
    <div class="container">
        <div class="mx-auto my-5">
            <form 
                id="form" 
                class="needs-validation my-4" 
                action="{{ URL TO FORM HANDLER }}" 
                method="post"
            >
                <div class="input-group input-group-lg">
                    <input 
                        class="form-control" 
                        type="email" 
                        name="email" 
                        minlength="3" 
                        maxlength="254"
                        placeholder="Enter your email..." 
                        pattern="^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,})$"
                        required
                    >
                    <div class="invalid-feedback order-last">
                        Please enter a valid email address.
                    </div>
                    <div class="input-group-append">
                        <button id="send" class="btn btn-primary">Send</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
    <script>
        var form = document.getElementById('form');
        var recaptcha = document.getElementById('g-recaptcha-response')

        form.addEventListener('invalid', function (e) {
            e.preventDefault();
            this.classList.add('was-validated');
        }, true);

        form.addEventListener('submit', function (e) {
            e.preventDefault();
            grecaptcha.ready(function () {
                grecaptcha.execute(
                    '{{ RECAPTCHA SITE KEY }}',
                    { action: 'submit' }
                ).then(function (token) {
                    sendFormData(token);
                });
            });
        }, true);

        function sendFormData(token) {
            var data = formToJSON(form, token);
            fetch(form.action, {
                method: "POST",
                body: JSON.stringify(data)
            })
            .then(function (res) { return res.json(); })
            .then(function (data) { alert(JSON.stringify(data)) })
        }

        function formToJSON(form, token) {
            var data = {};
            for (var i = 0, ii = form.length; i < ii; ++i) {
                var input = form[i];
                if (input.name) {
                    data[input.name] = input.value;
                }
            }
            data["g-recaptcha-response"] = token;
            return data;
        }
    </script>
</body>
</html>

 

Form Handler page:-

As the Form Handler now needs to process a JSON object instead of a POST request, the script needs to be modified accordingly.

<script runat='server'>
    Platform.Load('core', '1');
    try {

        var postData = Platform.Request.GetPostData();
        var parsedData = Platform.Function.ParseJSON(postData);
        var g_recaptcha_response = parsedData["g-recaptcha-response"];
        var secret = "{{ RECAPTCHA SECRET KEY }}";
        var payload = "secret=" + secret + "&response=" + g_recaptcha_response;

        var req = HTTP.Post('https://www.google.com/recaptcha/api/siteverify', 'application/x-www-form-urlencoded', payload);

        if (req.StatusCode == 200) {
            var resp = Platform.Function.ParseJSON(String(req.Response));
 			if(!resp.success) throw "reCAPTCHA request returned an error";
        	if(resp.score < 0.5) throw "reCAPTCHA score is low";
        } else {
            throw "reCAPTCHA API error";
        }

        Write(Stringify(resp));

    } catch (error) {
        Write(Stringify({ status: "Error", message: error }));
    }
</script>