Desired Behaviour
Incorporate reCaptcha JavaScript code in my webpack bundled js file, rather than via inline script tags.
Actual Behaviour
I am getting this error in Chrome dev tools:
Uncaught ReferenceError: grecaptcha is not defined
What I've Tried
The following inline
implementation works and I have been using these docs for reference.
However I had to add unsafe-inline
to my script-src
Content Security Policy in order to allow the inline script to run. More specifically, this was required to implement explicit rendering via the onLoadCallback
function.
Google has an FAQ about CSP and reCaptcha, but it only applies to automatic rendering, where there is no callback function or defined parameters.
I'd prefer not to have to use inline scripts.
index.html
<head>
<script type="text/javascript">
var onloadCallback = function() {
grecaptcha.render('g-recaptcha', {
'sitekey': '******',
'size': 'compact'
});
};
</script>
</head>
<body>
<div id="g-recaptcha"></div>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
</body>
</html>
However, when I try adding the JS to my entry.js
file like this (to stop using inline script):
index.html
<head>
<script type="module" src="/js/bundle.js"></script>
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script>
</head>
<body>
<div id="g-recaptcha"></div>
</body>
</html>
entry.js
const onloadCallback = () => {
grecaptcha.render('g-recaptcha', {
'sitekey': '******',
'size': 'compact',
'data-callback': 'ok-you-can-submit-the-form',
'data-expired-callback': 'you-have-to-click-recaptcha-again',
'data-error-callback': 'something-went-wrong-please-try-again'
});
}
$(document).ready(function() {
onloadCallback();
}
I get the error in Chrome dev tools:
Uncaught ReferenceError: grecaptcha is not defined
So I am guessing this is because bundle.js
does not have any knowledge of the recaptcha script, or its related variables, defined in the <head>
section.
How can I implement google reCaptcha without using the inline script paradigm?
Edit
I think Google's suggestion to use a nonce-based approach (also suggested in this SO answer) only works if you are doing automatic rendering (where only a <script src="****">
tag is required).
If you are using explicit rendering as I am, which requires definition of a callback function inline, then i don't think the nonce approach works.
Just posting what worked for me in the end.
Use reCAPTCHA v3.
index.html
head:
<script src="https://www.google.com/recaptcha/api.js?render=*******"></script>
on click event:
grecaptcha.ready(function() {
grecaptcha.execute('*******', { action: 'submit_entry' }).then(function(token) {
parameters.token = token;
ajax_api_entries_post(parameters);
});
});
verification of google token from server:
var token = req.body.token;
var url = `https://www.google.com/recaptcha/api/siteverify?secret=${secret_key}&response=${token}`;
var response = await fetch(url);
var response_json = await response.json();
var score = response_json.score;
// see: https://developers.google.com/recaptcha/docs/v3#interpreting_the_score
if (score >= 0.5) { ...
helmet configuration:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://maps.googleapis.com", "https://www.google.com", "https://www.gstatic.com"],
connectSrc: ["'self'", "https://some-domain.com", "https://some.other.domain.com"],
styleSrc: ["'self'", "fonts.googleapis.com", "'unsafe-inline'"],
fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "https://maps.gstatic.com", "https://maps.googleapis.com", "data:", "https://another-domain.com"],
frameSrc: ["'self'", "https://www.google.com"]
}
},
})
);