Search code examples
javascriptjqueryrecaptcha

How to implement google reCaptcha without inline script?


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.


Solution

  • 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"]
                }
            },
        })
    );