Reputation: 1550
I'm in a bind, and Javascript world it's very weird.
I need to make a form with the reCAPTCHA v3 token resolved before proceeding with form submission. Since some events are bound to the form submit
event, the event loop must wait until it's resolved otherwise it fails:
While I'm trying to do is to have a listener for the submit
event that blocks the submission until the token is resolved before going on with other listeners.
Currently the documentation on the reCAPTCHA script is zero, and I haven't found a reliable way to actually and forcefully resolve the token. The reCAPTCHA script is asynchronous, so there is no way to wait until the token is resolved:
// Let's retrieve the form by its id.
let form = document.getElementById('example_form');
let addToken = (form) => {
grecaptcha.execute('site_key', {
// some options
}).then(token => {
// Include the token as an input inside the form so it's sent.
});
};
form.addEventListener('submit', () => {
return addToken(form); // <-- This won't work, since it's async.
});
Upvotes: 2
Views: 8186
Reputation: 323
JavaScript cannot wait for the recaptcha request to finish. I made that work with a timestamp global variable that I compare to the current timestamp at each submit. So, onSubmit has 2 behaviours: if requesting a new token is necessary, then onSubmit only takes care of the recaptcha token (submission is prevented). If not necessary, then it does the usual (validations and stuff).
<script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
<script>
$(function() {
// last recaptcha token timestamp in ms (to check if the refresh interval of max 2 minutes is reached in between submit attempts)
recaptchaLoginLastTokenTimestamp = 0;
$("#loginform").on('submit', function(e){
nowTimestamp = new Date().getTime();
// if 100 seconds passed since last recaptcha token => get new token
if(nowTimestamp - recaptchaLoginLastTokenTimestamp > 100000){
// stop submission
e.preventDefault();
// call the recaptcha service to get a token
grecaptcha.ready(function() {
grecaptcha.execute('SITE_KEY', {action:'action_login'})
.then(function(token)
{
// add token value to form
document.getElementById('recaptcha_response_token').value = token;
// update last token timestamp
recaptchaLoginLastTokenTimestamp = new Date().getTime();
// re-submit (with the updated token timestamp, the normal submit process will occur, with validations and other stuff that are bind to submission; no infinite recursive submit calls)
$("#loginform").submit();
});
});
}
});
});
</script>
Upvotes: -1
Reputation: 89
I had the same issue. This is my solution:
async function recaptchaCall(){
var recaptcha_token = '';
grecaptcha.ready(() => {
grecaptcha.execute(grecaptchaKey, {action: 'submit'}).then((token) => {
recaptcha_token = token;
});
});
while(recaptcha_token == ''){
await new Promise(r => setTimeout(r, 100));
}
return recaptcha_token;
}
let recaptcha_token = await recaptchaCall();
I think it's the best solution since it is not possible to make it as an synchronously function correctly. I'm using setTimeout to wait until I receive Recaptcha answer. I hope it is helpful.
Upvotes: 5
Reputation: 1550
Figured out, there is no way to make recaptcha.execute()
synchronous. In other words, waiting for the token to be resolved from servers is impossible.
Instead, you should hammer nonchalantly the reCAPTCHA servers by requesting an initial token, and then setting up the same action through an interval of 100 seconds to be sure the token is received before expiration.
This code is adjusted for many forms. Use at your own risk.
const site_key = 'HEREYOURSITEKEY';
// This function retrieves the token from reCAPTCHA.
const retrieveToken = (form) => {
// Mark the form as unresolved until the retrieval ends.
form.unresolved = true;
// Get the token.
grecaptcha.execute(site_key, {
action: form.action.substring(action.indexOf('?'), action.length).replace(/[^A-z\/_]/gi, '')
}).then(token => {
// Append the token to the form so it's sent along the other data.
let child = document.createElement('input');
child.setAttribute('type', 'hidden');
child.setAttribute('name', '_recaptcha');
child.setAttribute('value', token);
form.appendChild(child);
form.unresolved = false;
});
};
// We will loop for each form in the document. You can add a "filter" method if you want.
Array.from(document.getElementByTagName('form'))
.forEach(form => {
// Mark it as unresolved from the beginning.
form.unresolved = true;
// Add an event listener that disables submission if the form is unresolved.
form.addEventListener('submit', event => {
if (form.unresolved) {
event.preventDefault();
}
});
// Resolve the token at startup.
retrieveToken(form);
// And retrieve a new token each 100 seconds. Bite me.
setInterval(() => refreshToken(form), 100 * 1000);
});
Upvotes: 1
Reputation: 29062
You could just disable form submission until the token is loaded and show some loader instead. In the then
callback, you save the token in a form field and enable the submission of the form (and hide the loader).
If you want to make it more seamless, you could hide this process from the user: If the user submits the form before the token is loaded, then you disable the button (and prevent the regular submission attempt) and show a loader instead and remember that a submission was pending, and when the token is later received you check if a submission was pending, and if yes, you programmatically submit the form (including the token).
This way the user won't encounter the loader unless they actually submit the form too early.
Upvotes: 0