google/recaptcha

Using reCAPTCHA v3 in the frontend

mcdado opened this issue · 7 comments

Hello, while this is not specific to the PHP client library, but I hope to get some answers about the use of the new reCAPTCHA v3.

Basically I have a contact form in PrestaShop, and I want to be able to add reCAPTCHA v3 so that there is no extra interaction for users while making sure I don't get spammed from bots. From the docs page, the idea being brought forward is to add the script tag with the link toapi.js with the render GET parameter ?render=reCAPTCHA_site_key. Doing so you will not need to run grecaptcha.render() manually, which is an important point (more on this later).

Now, I have to add grecaptcha.execute() in the callback of grecaptcha.ready(). Here I can define an action, and finally in the then() promise fulfillment I get a token. I take this token and update the value attribute of a hidden input tag in the form, named g-recaptcha-response. This way it gets sent to the server alongside with the regular form fields and I can make the server verification in ContactController::postProcess() method.

Since I want users to fill the form with several fields and a message, they might take even minutes to submit the form. I'm also using jQuery to run on the form submission, doing some validation checks and in case calling event.preventDefault() and showing an alert if some required fields are empty or invalid.

In my tests I found out that tokens expire rather quickly. Since we don't manually run grecaptcha.render() as mentioned above, we cannot specify an expired-callback to run when the token expires. At this point I resorted to add a setInterval with a 60000 delay (1 minute) to run again grecaptcha.execute().

Is this the right way to use the frontend library? Is it correct to run multiple checks once per minute? Will this affect users' score?

$(document).ready(function () {
  function reCAPTCHA_execute () {
    // grecaptcha instantiated by external script from Google
    // reCAPTCHA_site_key comes from backend
    grecaptcha.execute(reCAPTCHA_site_key, { action: 'contactForm' }).then(function (token) {
      $('[name="g-recaptcha-response"]').val(token);
    }, function (reason) {
      console.log(reason);
    });
  }

  if (typeof grecaptcha !== 'undefined' && typeof reCAPTCHA_site_key !== 'undefined') {
    grecaptcha.ready(reCAPTCHA_execute);
    setInterval(reCAPTCHA_execute, 60000);
  }
});

Initially I thought of running grecaptcha.execute() on form submission, in the same callback function that does data validation, but since grecaptcha.execute() is asynchronous, I cannot stop the form submission (event.preventDefault()) and the resume in the promise fulfillment.

  $('.contact-form-box').on('submit', function (e) {
    // Check if email is empty
    if ($('#email').val() === '') {
      e.preventDefault();
      window.alert(contact_emptyEmail);
      return;
    }

    // Check if email is not valid
    if (!validate_isEmail($('#email').val())) {
      e.preventDefault();
      window.alert(contact_invalidEmail);
      return;
    }

    // etc…

    // here I cannot do grecaptcha.execute() because it's asynchronous,
    // and stopping the form submission cannot be reverted.
  });

I haven't used the V3 API yet, but this flow works on the V2 invisible version.

<div class="g-recaptcha" data-sitekey="XXXXXX" data-size="invisible" data-callback="formSubmit"></div>
var frm = document.forms.item(0);

frm.addEventListener('submit', function (e) {
    e.preventDefault();
    grecaptcha.execute();
});

function formSubmit(response) {
    frm.submit();
}

So your code should probably be something like this

$('.contact-form-box').on('submit', function (e) {
    var frm = this;
    e.preventDefault();
    
    // Check if email is empty
    if ($('#email').val() === '') {
        window.alert(contact_emptyEmail);
        return;
    }
    
    // Check if email is not valid
    if (!validate_isEmail($('#email').val())) {
        window.alert(contact_invalidEmail);
        return;
    }
    
    // etc…
    
    grecaptcha.execute(reCAPTCHA_site_key, { action: 'contactForm' }).then(function (token) {
      $('[name="g-recaptcha-response"]').val(token);
      frm.submit();
    }, function (reason) {
      console.log(reason);
    });
});

Hmm, when you do frm.submit(); wouldn't that be blocked by e.preventDefault(); since the the whole $('.contact-form-box').on('submit', …) would be called again?

Does the library remove any event listeners on the form? If that was the case, does it do it when the execute() is called or after the first call of the listener? In the former case it would be okay since I can first do all the checks and validations in my form event listener and only call execute() when the form is okay and ready to be submitted. Originally I was testing a similar setup but it seemed not to work that way though…

https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit

When invoking this method directly, no submit event is raised (in particular, the form's onsubmit event handler is not run), and constraint validation is not triggered either.

Wow, thanks! You never stop learning!

I've been trying to figure this out, and while this looks promising, doesn't the (token) always get returned no matter what? I think you have to take that token and submit it to the google verify API server side in order to get the actual valid or unvalid response. Otherwise this method would submit the form no matter what.

Can anyone confirm this?

Sorry to hijack, but we've implemented the recaptchaV3 action on form submit and are noticing a lengthy delay (> 1000 milliseconds) after users click the submit button.

      grecaptcha.ready(function () {
        const account_id = 'XXXXXXXXXX';
        $('#my-form').submit(async function(event) {
          event.preventDefault();
          const $this = $(this);
          await grecaptcha.execute(account_id, {action: 'form_submission'})
            .then(function(token) {
              $('#token-field').val(token);
          });
          $this.unbind('submit').submit();
        });
      });

Is this expected? Otherwise, the only other thing I can see is refreshing the token every 120 seconds on the client system to work around the "lag on submit" we're seeing. Would these sorts of token refreshes have negative impacts?

Thanks for your consideration.