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