May 19, 2017

Google reCAPTCHA and Drupal 8

All internet users repeatedly encountered various captcha. In short, this is an abbreviation of "Completely Automated Public Turing Test to Tell Computers and Humans Apart". A person can easily and quickly solve the problem of recognition of distorted or intersecting letters or numbers. To confirm that the person is really a human and not a spam bot.

Over time, the captcha evolved. The simplest of them offer a person to perform a simple mathematical operation or answer a question. More complex — to recognize the word or words in the image. In recent years, almost everywhere we meet captcha from Google — reCAPTCHA. It determines when to check the user (usually recognition of similar images on a given topic) and does not spoil the appearance of your form.

Invisible captcha on the front-end and how to write a handler on the back-end using Drupal

There is a ready module for Drupal 8. But we will look at working with reCaptcha, without using this module.

The process of the captcha is as follows: from the front-end we get a token, send this token to the back-end. Back-end makes a POST request to Google API with data: IP user and token received from the front.

Front-end

Add a captcha script to the page (in the documentation it’s noted that the HTTPS protocol is  required):

<script src="https://www.google.com/recaptcha/api.js?hl=ru" async defer></script>

?hl=ru it's a language option (here we use Russian).

Inside a (some) form we add the markup of the captcha:

<div class="g-recaptcha"
  data-sitekey="sitekey"
  data-callback="onSubmit"
  data-size="invisible">
</div>

When submitting a form, we must call grecaptcha.execute(); (for example in onSubmit). At the same time, there is no need to do additional actions to embed the token in the form, the captcha will do everything for us. After submitting the form, we will have a g-recaptcha-response parameter. It contains the token of the captcha to validate on the server.

It is possible without a direct call recaptcha.execute(), then data-sitekey and data-callback parameters are written directly on the form's submit button. The first option is more universal.

Back-end

Example of processing on Drupal 8 (data from POST request):

use GuzzleHttp\Exception\RequestException;

... 
 
// Get ReCaptcha token from front end
$data['google_captcha_token'] = '';
$data['google_captcha_token'] = \Drupal::request()->request->get('g-recaptcha-response');
 
// dev:
\Drupal::logger('my_module')->notice($data['google_captcha_token']);
 
$user_ip = \Drupal::request()->getClientIp();
$secret_key = '<GOOGLE_SECRET_KEY>';
 
// Check (true - spam)
$spam = $this->checkSpamRecaptcha($secret_key, $user_ip, $data['google_captcha_token']);
 
if (!$spam) {
  // ...do stuff here
} 
else {
  // ...spamer!
}

Do not forget to substitute your key in $secret_key. And delete or change the string with logging.

Check function:

/**
 * Check Google API for ReCaptcha information
 *
 * POST (application/x-www-form-urlencoded) request to
 * https://www.google.com/recaptcha/api/siteverify
 *
 * @param $secret_key
 *  ReCaptcha secret key from Google recaptcha service
 *
 * @param $remoteip
 *  User ip
 *
 * @param $g_recaptcha_response
 *  Special token from front-end
 *
 * @return bool
 *  TRUE - spam, FALSE - human and good man
 */
public function checkSpamRecaptcha($secret_key, $remoteip, $g_recaptcha_response) {
  $spamer = true;
  $client = \Drupal::httpClient();
 
  try {
    // Sending application/x-www-form-urlencoded POST requests requires that you 
    // specify the POST fields as an array in the form_params request options.
    // http://docs.guzzlephp.org/en/latest/quickstart.html#sending-requests
    $body = [
      'form_params' => [
        'secret' => $secret_key,
        'response' => $g_recaptcha_response,
        'remoteip' => $remoteip,
      ],
    ];
    $response = $client->request('POST', 'https://www.google.com/recaptcha/api/siteverify', $body);
    $data = $response->getBody();
 
    $recaptcha = json_decode($data);
 
    if ($recaptcha->success) {
      $spamer = false;
    }
  }
  catch (RequestException $e) {
    \Drupal::logger('my_module')->notice($e->getMessage());
  }
  return $spamer;
}