14

I got a contact form on my website on Laravel and I'd like to place a ReCaptcha v3 but for now the result I got from the verification is the error "timeout-or-duplicate".

Can you help me from A to Z ? I don't know where to go...

My head :

<script src="https://www.google.com/recaptcha/api.js?render=My_Site_Key"></script>
  <script>
    grecaptcha.ready(function () {
      grecaptcha.execute('My_Site_Key', { action: 'contact' }).then(function (token) {
        var recaptchaResponse = document.getElementById('recaptchaResponse');
          recaptchaResponse.value = token;
      });
    });
  </script>

The contact form :

<form action="{{ route('contact.post') }}" id="contact-form" method="post" name="contactForm">
   <input type="hidden" name="_token" id="token" value="{{ csrf_token() }}">
   <input type="hidden" name="recaptcha_response" id="recaptchaResponse">
   <fieldset>
     <div class="col-sm-12">
       <input id="name" name="name" placeholder="Nom*" type="text">
     </div>
     <div class="col-sm-12">
       <input id="email" name="email" placeholder="Email*" type="text">
     </div>
     <div class="col-sm-12">
       <input id="object" name="object" placeholder="Objet*" type="text" autocomplete="off">
     </div>
     <div class="col-xs-12">
       <textarea cols="5" id="message" name="message" placeholder="Votre message...*"></textarea>
     </div>
     <div class="col-xs-12">
       <button class="submit active" id="contact-submit">ENVOYER</button>
     </div>
     <div class="error col-xs-12">
       <h3></h3>
     </div>
     <div class="success col-xs-12">
       <h3>Merci ! Votre message a été envoyé !</h3>
     </div>
   </fieldset>
</form>

Route:

Route::post('/contact', array('as' => 'contact.post', 'uses' => 'ContactController@send'));

The Contact Controller :

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Mail;

class ContactController extends Controller
{
    public function send() {
      $info = array(
          'name' => Input::get('name'),
          'email' => Input::get('email'),
          'object' => Input::get('object'),
          'message' => Input::get('message')
      );
      if($info['name'] == "" || $info['email'] == "" || $info['object'] == "" || $info['message'] == "") {
          return json_encode(['response' => 'Tous les champs doivent être remplis !']);
      }
      if(!filter_var($info['email'], FILTER_VALIDATE_EMAIL)) {
          return json_encode(['response' => 'Vous devez rentrer une adresse e-mail valide !']);
      }
      $ip = Request()->ip();

      // Build POST request:
      $recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
      $recaptcha_secret = 'My_Secret_Key';
      $recaptcha_response = $_POST['recaptcha_response'];
      // Make and decode POST request:
      $recaptcha = file_get_contents($recaptcha_url . '?secret=' . $recaptcha_secret . '&response=' . $recaptcha_response);
      $recaptcha = json_decode($recaptcha);
      // Take action based on the score returned:
      if ($recaptcha->score < 0.5) {
          return json_encode(['response' => 'Vous êtes considéré comme Bot/Spammer !', 'score' => $recaptcha->score]);
      }

      Mail::send(['email.html.contact', 'email.text.contact'], ['info' => $info, 'ip' => $ip], function($message) use ($info) {
          $message->to('contact@bryangossuin.be')->subject('Bryan Gossuin | Formulaire de contact');
          $message->replyTo($info['email'], $info['name']);
      });
      return json_encode(['response' => 'success','']);
  }
}

Finaly the javascript

      $('#contact-form').on('submit', function(e) {
          e.preventDefault();
          swal({
              title: "Souhaitez-vous vraiment envoyer ce mail ?",
              icon: "warning",
              buttons: {
                cancel: {
                  text: "Annuler",
                  value: false,
                  visible: true,
                  closeModal: true,
                },
                confirm: "Envoyer",
              }
            })
            .then((value) => {
              if (value) {
                  $.ajax({
                          method: "POST",
                          url: "contact",
                          cache: false,
                          data: $(this).serialize(),
                          dataType: 'json',
                          success: function(json) {
                              console.log(json.score);
                              if (json.response == 'success') {
                                  $('#contact-form').trigger("reset");
                                  swal("E-mail envoyé", "Merci de votre demande !", "success");
                              } else {
                                  swal("Erreur !", json.response, "error");
                              }
                          }
                      }
                  )
               }
            });
      });

The output I got from google is

{
  "success": false,
  "error-codes": [
    "timeout-or-duplicate"
  ]
}

and I expect it to be

{
  "success": true,
  "score" : x,
  "error-codes": '',
}

I guess the problem is because the « method post » is used two times because when I Check directly On the API Google to verify the user token it show le thé code but right after I refresh the page it show me « timeout or duplicate » but I dont know how to fix this

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Saku
  • 305
  • 1
  • 2
  • 8

6 Answers6

7

I got this from people double clicking the submit button on the form.

Matthew Lock
  • 13,144
  • 12
  • 92
  • 130
6

As stated in the documentation this error is caused by:

  1. Validity time of the token expired (After you get the response token, you need to verify it within two minutes)
  2. Token has been used previously. To confirm that, log the token value before is used (error log, local file, whatever)

My resolution for 1, set an interval that calls the set token function, so it is refreshed every 2 minutes.

$(document).ready(function() {

            SetCaptchaToken();
            setInterval(function () { SetCaptchaToken(); }, 2 * 60 * 1000);

    });

Resolution for 2, fix your code :)

Jorge
  • 1,178
  • 2
  • 13
  • 33
6

The problem is this piece of code:

<script src="https://www.google.com/recaptcha/api.js?render=My_Site_Key"></script>
  <script>
    grecaptcha.ready(function () {
      grecaptcha.execute('My_Site_Key', { action: 'contact' }).then(function (token) {
        var recaptchaResponse = document.getElementById('recaptchaResponse');
          recaptchaResponse.value = token;
      });
    });
  </script>

The token is only valid for 2 minutes after you execute is called as stated in the docs:

Note: reCAPTCHA tokens expire after two minutes. If you're protecting an action with reCAPTCHA, make sure to call execute when the user takes the action.

Thus, if you spend more then 2 minutes on the contact-form, you get the timout error,. Thats why its recommended in the docs to only call execute if the user actually submits your form / takes action. In vanilla JS it would look like this:

<script src="https://www.google.com/recaptcha/api.js?render=My_Site_Key"></script>
<script>
  grecaptcha.ready(function() {
      document.getElementById('contact-form').addEventListener("submit", function(event) {
        event.preventDefault();
        grecaptcha.execute('My_Site_Key', {action: 'contact'}).then(function(token) {
           document.getElementById("recaptchaResponse").value= token; 
           document.getElementById('contact-form').submit();
        });
      }, false);
  });
</script>
Adam
  • 25,960
  • 22
  • 158
  • 247
4

Every time the page reloads you get a new token from google . You can use that token only once . Somehow if you are using that token more than once to get the response from google Api , you will get that error . Check this error reference https://developers.google.com/recaptcha/docs/verify?hl=en

Nihar Sarkar
  • 1,187
  • 1
  • 13
  • 26
2

I been googling looking for answers specifically similar to your use case.

reCaptcha V3 does not have reset API.

I solve the problem by when Password or Email authentication failed on your side, execute this again on your AJAX if failed. So that the value get replace with new g-token without reloading the site again, since following Google Documentation like me, the script execute on ready at your "signin page"

 grecaptcha.ready(function() {
              grecaptcha.execute('abhkdfhlasdfhldafhlashflasdhl', {action: 'submit'}).then(function(token) {
                document.getElementById('g-token').value = token;
              });
            });
Dennis Ong
  • 51
  • 5
1

The issue is likely caused because the script is running more than once.

Is there anywhere else in the code that could be submitting the form more than once?

I had a similar issue and a simple console log in the results part of the JS showed that it was being printed twice i.e. the form was submitting twice.

If it is not a code issue, a user may be double clicking the button. You could do a simple on click event to disable the button and this would remove the error.

Declan Kay
  • 86
  • 5