Requirement to add a google reCAPTCHA custom variable on public facing record producers

georgehornsey
Giga Expert

I have a requirement to add a google reCAPTCHA widget that can be called via a custom variable which is added to the record producer.

 

So far I have managed to get the public widget created and loading a google reCAPTCHA but it is only working on authenticated forms.

 

I have provided screenshots for context of where I have got to...

1) Custom variable calling the widget

georgehornsey_0-1791557788601.png

2) REST Message for the siteverify

georgehornsey_1-1791557854002.png

3) script include to verify reCAPTCHA tokens

var GoogleRecaptcha = Class.create();
GoogleRecaptcha.prototype = {
    initialize: function(sitekey, secret) {
        this._gcsitekey = sitekey || gs.getProperty('google.captcha.site_key');
        this._gcsecret = secret || gs.getProperty('google.captcha.secret');
    },

    verify: function(token){        
        try {
            var r = new sn_ws.RESTMessageV2('Google Recaptcha', 'Default');
            r.setStringParameterNoEscape('token', token+""); //forcing the token to be a string
            r.setStringParameterNoEscape('secret', this._gcsecret);
            var response = r.execute();
            var responseBody = response.getBody();
            var httpStatus = response.getStatusCode();
            var responseJson = JSON.parse(responseBody);
            return responseJson && responseJson.success;
        }
        catch(ex) {
            var message = ex.message;
            gs.error("GoogleRecaptcha :: " + message);
            return false;
        }      
    },
   
    getSiteKey : function(){
        return this._gcsitekey;
    },

    type: 'GoogleRecaptcha'
};
 
4) widget code
body html:
<!-- BE SURE TO INCLUDE THE GoogleRecaptcha ANGULAR PROVIDER -->
<div>
<form ng-submit="c.submitV2Request()">
<div id="recaptcha_container"></div>
<br>
<input class="btn btn-primary" type="submit" value="Submit reCAPTCHA">
</form>
</div>

server script:
(function() {
    // You can either put the SITEKEY & SECRET in system properties or pass them in
    var gcSiteKey = gs.getProperty('google.captcha.site_key', ''); //sys property name : google.recaptcha.sitekey
    var gcSecret = gs.getProperty('google.captcha.secret', ''); //sys property name : google.recaptcha.secret
    var googleRecaptcha = new GoogleRecaptcha(gcSiteKey, gcSecret);

    if(input && input.token){  
        //check if the request was captcha verified
        if(googleRecaptcha.verify(input.token)){
            //do your regular processing
            data.success = "V2 SERVERSIDE VERIFICATION SUCCESSFUL!";
        }
        else{
            //return error
            data.error = "V2 SERVERSIDE VERIFICATION FAILED!!";
        }
    }
    else if(!input){        
        data.sitekey = googleRecaptcha.getSiteKey();
    }  
})();


Client controller:

function($scope, spUtil, GoogleRecaptcha) {
    var c = this;  

    /* V2 Checkbox Support */
    var recaptchaV2WidgetId = null, recaptchaV2Result = null;

    function getV2Result(result){
        recaptchaV2Result = result;
    }

    function resetV2Recaptcha(){
        recaptchaV2Result = null;
    }

    c.submitV2Request = function(){
        if(recaptchaV2WidgetId === null && recaptchaV2WidgetId === false){
            spUtil.addErrorMessage("Recaptcha not setup!")        
        }
        else if(recaptchaV2Result === null){
            spUtil.addErrorMessage("Recaptcha not solved!")        
        }
        else{
            //looks like we are good to go
            c.server.get({v2: true, token: recaptchaV2Result}).then(function(r){
                //reset the captcha
                GoogleRecaptcha.reset(recaptchaV2WidgetId);
                //check the response
                if(r.data.success){
                    spUtil.addInfoMessage(r.data.success);
                }
                else if(r.data.error){
                    spUtil.addErrorMessage(r.data.error);
                }
            });
        }
    };

    // We will call this from the link function so that the element has been rendered on page
    // For more info about render function visit https://developers.google.com/recaptcha/docs/display#render_param
    $scope.renderCaptcha = function(){
        GoogleRecaptcha.initializeV2({key: c.data.sitekey}).then(function(){
            recaptchaV2WidgetId = GoogleRecaptcha.render('recaptcha_container', {
                'callback' : getV2Result,
                'expired-callback' : resetV2Recaptcha
            });        
        });
    }
    /* End V2 Checkbox Support */
}

Link:
function link(scope, element, attrs, controller) {
    scope.renderCaptcha();
}

5) Angular Provider
function GoogleRecaptcha($window, $q, $document, $interval, spUtil) {
    var key = null;
    var onLoadFunctionName = 'onGoogleRecaptchaScriptLoaded';
    var defered = $q.defer();
    var initializing = false;
    var initialized = false;

    function writeScript () {
        var tag = document.createElement('script');
        tag.src=url + '?render=' + key + '&onload=' + onLoadFunctionName + '&render=explicit';
        $document.find('body')[0].appendChild(tag);
    }

    function writeScriptV2 () {
        var tag = document.createElement('script');
        tag.src=url + '?onload=' + onLoadFunctionName + '&render=explicit';
        $document.find('body')[0].appendChild(tag);
    }

    function isExecuteAvailable () {
        return angular.isFunction(($window.grecaptcha || {}).execute);
    }

    function scriptExists () {
        return $window.document.querySelector('script[src^="' + url + '"]');
    }

    function execute (params) {
        if (isInitializing()) {
            return defered
                .promise
                .then(function () {
                return execute(params);
            });
        }
        return $window.grecaptcha.execute(key, params);
    }

    function isInitialized () {
        return !!initialized;
    }

    function isInitializing () {
        return !!initializing;
    }

    function reset (id) {
        return $window.grecaptcha.reset(id);
    }

    function extractParametersForRender(obj, params){
        var result = {};
        obj = obj || {};
        params = params || [];
        for(var i in params){
            var param = params[i];          
            if(obj[param]){
                //only set size if it is set to compact otherwise let it be default
                if(param == 'size' && obj[param] === 'compact'){
                    result[param] = obj[param];
                }
                else{
                    result[param] = obj[param];
                }
            }
        }
        return result;
    }

    function render(element_id, options){
        if(initialized && $window.grecaptcha){          
            var opts = extractParametersForRender(options, ['callback', 'theme', 'size', 'tabindex', 'expired-callback','error-callback']);
            opts.sitekey = key;

            return $window.grecaptcha.render(element_id, opts);
        }
        else{
            spUtil.addErrorMessage("GoogleRecaptcha not initialized!");
            return false;
        }
    }

    function initialize (params) {
        setTimeout(function () {
            try {
                if (!params || !params.key) {
                    throw new Error('Missing required public key for reCaptcha.');
                }

                key = params.key + '';

                if (isExecuteAvailable()) {
                    initialized = true;
                    initializing = false;
                    defered.resolve();
                } else if (scriptExists()) {
                    initializing = true;
                    var wait = $interval(function () {
                        if (isExecuteAvailable()) {
                            $interval.cancel(wait);

                            initialized = true;
                            initializing = false;
                            defered.resolve();
                        }
                    }, 50);
                } else {
                    initializing = true;
                    $window[onLoadFunctionName] = function () {
                        initialized = true;
                        initializing = false;
                        defered.resolve();
                    };

                    writeScript();
                }


            } catch (e) {
                defered.reject(e);
            }
        });

        return defered.promise;
    }

    function initializeV2 (params) {
        setTimeout(function () {
            try {
                if (!params || !params.key) {
                    throw new Error('Missing required public key for reCaptcha.');
                }

                key = params.key + '';

                if (isExecuteAvailable()) {
                    initialized = true;
                    initializing = false;
                    defered.resolve();
                } else if (scriptExists()) {
                    initializing = true;
                    var wait = $interval(function () {
                        if (isExecuteAvailable()) {
                            $interval.cancel(wait);

                            initialized = true;
                            initializing = false;
                            defered.resolve();
                        }
                    }, 50);
                } else {
                    initializing = true;
                    $window[onLoadFunctionName] = function () {
                        initialized = true;
                        initializing = false;
                        defered.resolve();
                    };

                    writeScriptV2();
                }


            } catch (e) {
                defered.reject(e);
            }
        });

        return defered.promise;
    }

    return {
        initialize: initialize,
        initializeV2 : initializeV2,
        execute: execute,
        render : render,
        reset: reset,
        isInitialized: isInitialized,
        isInitializing: isInitializing
    };
}


1 ACCEPTED SOLUTION

georgehornsey
Giga Expert

After reviewing my development with ServiceNow.

We'd found that we needed to swap the variable type to something else, i.e., multi-line text this will then display the write and read role permissions fields, you need to add 'public' role to those fields save the record and then change the variable type back to custom and re save the record. This will keep the permissions added.  I also changed my code to be similar to the following post - https://www.google.com/url?q=https://www.servicenow.com/community/user-group-info/how-to-add-recaptc...

As this was the exact way I wanted my reCAPTCHA widget to work, rather than having a submit button and also preventing the submission of the public record producers without having the reCAPTCHA confirmed.

View solution in original post

2 REPLIES 2

sanjaygupta
Kilo Guru

You've done most of the hard work already. Your widget and REST call work for logged-in users, so the problem is almost certainly about guest access. Start by finding out exactly what breaks for a guest, because each cause has a different fix.

  • Open your public portal in a private browser window so you're truly logged out.
  • Open the record producer and press F12 to see the browser console.
  • Note whether the reCAPTCHA box doesn't appear at all, or appears but fails when you verify.
  • Go to System Logs > All and search for messages starting with "GoogleRecaptcha ::", which your script include writes when the call fails.

If the box doesn't appear at all, the guest probably can't load the widget, the page, or the item.

  • Go to Service Portal > Widgets, open your reCAPTCHA widget, and confirm the Public checkbox is ticked.
  • Go to Service Portal > Pages, open the page your form uses (usually sc_cat_item), and confirm it's marked Public too.
  • Open the record producer and check its Available For list. Guests need user criteria that lets them see the item.
  • Open the custom variable and check the Read, Write, and Create roles on the Permissions tab. Clear any roles there, since guests have none.
  • Check the Angular Provider is in the widget's Angular Providers related list. A missing one shows an "Unknown provider" error in the console.

If the box shows "Invalid domain for site key," Google is rejecting your public URL. This happens when your public portal uses a different domain from the logged-in one.

  • Open your Google reCAPTCHA admin console.
  • Add the exact domain your public portal uses to the allowed domains list.

If the box shows but verification fails, the guest session may not be reading your keys.

  • Add gs.info('site key: ' + gcSiteKey); to your widget server script and test as a guest.
  • If the key comes back empty, open the properties google.captcha.site_key and google.captcha.secret and clear any Read roles.

I'm not fully certain property read roles affect server-side reads in every release, so test this rather than assume it.

There's also a small bug in your client controller. Your check says recaptchaV2WidgetId === null && recaptchaV2WidgetId === false, which can never be true.

  • Change the && to || so your "Recaptcha not setup" message can fire.
  • Remove the <form> tag from your widget HTML. Your widget sits inside the catalog form, and a form inside a form causes odd submits.
  • Replace the submit input with a button using ng-click="c.submitV2Request()".

The biggest gap is that your captcha isn't tied to the record producer's Submit button yet. A guest can skip your widget button and just submit the form, so the captcha protects nothing right now.

  • Add a hidden single line text variable to the record producer, named something like recaptcha_token.
  • In your getV2Result callback, store the token with $scope.page.g_form.setValue('recaptcha_token', result);.
  • Clear that variable in your expired callback so an old token doesn't linger.
  • Add an onSubmit catalog client script that blocks submit if recaptcha_token is empty.
  • In the record producer's Script field, call new GoogleRecaptcha().verify(producer.recaptcha_token).
  • If it returns false, call current.setAbortAction(true) and add an error message so no record is created.
  • Remove the separate verify call from your widget button, because Google tokens work only once.
  • Test the full flow as a guest and as a logged-in user in a sub-production instance before moving your update set.

If none of this fixes it, share your release name and the exact console error, and that should point straight to the cause.

georgehornsey
Giga Expert

After reviewing my development with ServiceNow.

We'd found that we needed to swap the variable type to something else, i.e., multi-line text this will then display the write and read role permissions fields, you need to add 'public' role to those fields save the record and then change the variable type back to custom and re save the record. This will keep the permissions added.  I also changed my code to be similar to the following post - https://www.google.com/url?q=https://www.servicenow.com/community/user-group-info/how-to-add-recaptc...

As this was the exact way I wanted my reCAPTCHA widget to work, rather than having a submit button and also preventing the submission of the public record producers without having the reCAPTCHA confirmed.