2

How do we add a Google reCaptcha v3 to a Flow Screen without hard coding the site key in either the LWC or the Digital Experience?

By not hard coding the site key within these components (ie making it data driven) the LWC can become a re-usable component that doesn't need to be forked every time we use a different site key.

This is different to the other questions on here about reCaptcha as it's specifically for flow screen components, and separating out the site key from the LWC/Site code.

1 Answer 1

2

Google ReCaptcha v3 as LWC Flow Screen Component in Digital Experience

The reCaptcha docs say we need to include the site key in the request for the js file.

<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>

When working on a digital experience, we have to load the js in the header, which can't have variables passed into it.

The trick was to set up some js in the digital experience header to listen for events from the LWC, and vice versa.

Working Solution

There's a bit to it, but this solution results in the configuration of the reCaptcha all existing in apex code, so you can configure it however you'd like, such as custom settings, named credentials, etc.

Digital Experience Setup

Go to Builder > Settings > Advanced > Edit Head Markup and enter the following:

<!--reCaptcha v3-->
<script>
    document.addEventListener('grecaptchaSiteKey', (event) => {
    
        let script = document.createElement('script');
        script.addEventListener('load', () => {
            document.dispatchEvent(new CustomEvent('grecaptchaJsLoaded'));
        });
        
        script.src = 'https://www.google.com/recaptcha/api.js?render=' + event.detail.siteKey;
        
        let s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(script, s);
    });
    
    document.addEventListener('grecaptchaExecute', (event) => {
        grecaptcha.ready(function() {
          grecaptcha.execute(event.detail.siteKey, {action: event.detail.action}).then(function(token) {
              document.dispatchEvent(new CustomEvent('grecaptchaVerified', {detail: {response: token, action: event.detail.action}}));
          });
        });
    });
    
    document.addEventListener('grecaptchaReset', function() {
        grecaptcha.reset();
    }); 
</script>

Then Builder > Settings > Security & Privacy > Trusted Sites for Scripts

Add https://www.google.com/ and https://www.gstatic.com/

Apex Class

public with sharing class CtsGoogleReCaptcha {

    // I've added these variables here for simplicity, but they can be anywhere in Salesforce
    private static final String ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';
    private static final String SECRET_KEY = 'MY_SECRET_KEY';
    private static final String PUBLIC_KEY = 'MY_PUBLIC_KEY';
    

    @AuraEnabled
    public static String verifyReCaptcha(String recaptchaResponse) {

        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint(ENDPOINT);
        request.setMethod('POST');
        request.setBody('secret=' + SECRET_KEY + '&response=' + recaptchaResponse);
        HttpResponse response = http.send(request);

        if (response.getStatusCode() != 200) {
            throw new AuraHandledException('There was an error processing reCaptcha. HTTP Status Code: ' + response.getStatusCode());
        }

        return response.getBody();
    }

    @AuraEnabled(cacheable=true)
    public static String getSiteKey() {
        return PUBLIC_KEY;
    }
}

LWC

js file

import { LightningElement, wire, api } from 'lwc';
import { FlowAttributeChangeEvent } from 'lightning/flowSupport';

import verifyRecaptcha from '@salesforce/apex/CtsGoogleReCaptcha.verifyReCaptcha';
import getSiteKey from '@salesforce/apex/CtsGoogleReCaptcha.getSiteKey';

/**
 * Taken from https://developers.google.com/recaptcha/docs/v3
 *
 * @typedef {Object} ReCaptchaVerifyResponse
 * @property {boolean}  success         whether this request was a valid reCAPTCHA token for your site
 * @property {number}   score           the score for this request (0.0 - 1.0)
 * @property {string}   action          the action name for this request (important to verify)
 * @property {string}   challenge_ts    timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
 * @property {string}   hostname        the hostname of the site where the reCAPTCHA was solved
 * @property {string[]} error-codes     any errors that occurred loading the challenge
 */

export default class CtsGoogleReCaptcha extends LightningElement {

    _threshold = 0.5;
    @api get threshold() { return this._threshold; }
    set threshold(value) {
        this._threshold = Number(value);
    }


    score;

    @api verificationPassed;
    @api verificationFailed;

    isVerifyInProgress = false;
    isVerifyComplete = false;

    _initialized = false;
    
    // Turn this on to see the score
    showDebug = false;

    siteKey;

    @wire(getSiteKey)
    wiredSiteKey(result) {
        if(result.data) {
            this.siteKey = result.data;
            this.initiliaze();
        }
        else if(result.error) {
            console.dir(result.error);
        }
    }

    connectedCallback() {

        document.addEventListener('grecaptchaJsLoaded', () => {
            this.isVerifyInProgress = true;
            document.dispatchEvent(new CustomEvent("grecaptchaExecute", {
                detail: {
                    action:     'view_site',
                    siteKey:     this.siteKey,
                }
            }));
        })


        document.addEventListener("grecaptchaVerified", (event) => {
            verifyRecaptcha({recaptchaResponse: event.detail.response})
                .then(result => {
                    /** @type {ReCaptchaVerifyResponse} */
                    const parsedResult = JSON.parse(result);

                    // reset the reCaptcha so it can be called again for the current user
                    // - ie. it will be called again if the form submission fails and we'll get a failed response "timeout-or-duplicate"
                    document.dispatchEvent(new CustomEvent("grecaptchaReset"));
                    this.isVerifyComplete = true;

                    if(parsedResult.success) {
                        this.score = parsedResult.score;
                        this.dispatchEvent(new FlowAttributeChangeEvent('verificationPassed', parsedResult.score >= this.threshold));
                        this.dispatchEvent(new FlowAttributeChangeEvent('verificationFailed', parsedResult.score < this.threshold));
                    }
                })
                .catch(error => {
                    console.dir(error);
                    console.log(error.body.message);
                })
                .finally( () => {
                    this.isVerifyInProgress = false;
                })
        });
    }

    renderedCallback() {
        this.initiliaze();
    }

    initiliaze() {
        if(this._initialized) { return; }

        let divElement = this.template.querySelector('div.recaptchaV3');

        if(this.siteKey && divElement) {

            this._initialized = true;

            document.dispatchEvent(new CustomEvent("grecaptchaSiteKey", {
                detail: {
                    siteKey:    this.siteKey
                }
            }));
        }

    }

    @api
    validate() {
        if(this.score > this.threshold) {
            return {
                isValid: true
            };
        }

        return {
            isValid: false,
            errorMessage: 'reCaptcha failed'
        }

    }
}

html file

<template>

    <div class="recaptchaV3"></div>
    
    <template lwc:if={showDebug}>
        <div lwc:if={isVerifyInProgress}>
            Verifying...
        </div>

        <div lwc:elseif={isVerifyComplete}>
            Verified with a score of: {score}
        </div>
    </template>

</template>

meta.xml file

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>61.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">

            <property name="threshold"  type="String"  label="Threshold (0.0 - 1.0)" default="0.5" role="inputOnly" />

            <property name="verificationPassed" type="Boolean"  label="Verification Passed" role="outputOnly" />
            <property name="verificationFailed" type="Boolean"  label="Verification Failed" role="outputOnly" />

        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Remote Site Settings

Add entry for https://www.google.com Potentially not required if you're using name credentials

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .