reCaptcha: No Bots Allowed
Recently, one of our client’s sites has been dealing with a large number of malicious bots signing up for its email newsletter. There are a few common strategies for dealing with bot traffic:
- Honey Pot: Hide form inputs from users, but keep them visible to bots, so if they are filled in we will know it is being accessed by a bot
- Confirmation Emails: Users are sent an email and are emailed a link to confirm that the email was valid
- Captcha: Some sort of puzzle/question that a bot will have trouble answering
Due to the sophistication and volume of bots, we decided that the most elegant solution for this site was to use a captcha system. But what captcha system? A captcha typically involves showing the user an image of a word and the user must type out the word. Usually the image is warped in a way that a computer will have a tough time recognizing it.
Unfortunately, we've gotten to the point where some bots are even better than humans at this game, so we turned to Google's NoCaptcha reCaptcha.
For most users, the NoCaptcha reCaptcha is simply a checkbox that the user can click.
Google's reCaptcha uses IP address, cookies, and mouse movements to determine if the user is a bot. There are also other proprietary strategies, but Google has little incentive to release these... as it would make cracking the captcha easier.
If Google cannot tell from the immediate behavior if a user is a bot or not, sometimes the user will need to complete a simple matching quiz like this:
Let's get it built!
Prerequisites
We are working with a standard Craft site and are able to interface with the frontend via a javascript module. reCaptcha is pretty flexible, so this is just one possible implementation.
You will also need to register your site with Google to get a site key and secret key. A test site key and secret key are available for development here.
Development
First, we need to display the reCaptcha widget in the html:
Markup
<form method="post" action=""><div id="recaptcha-widget"></div></form> <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> <script type="text/javascript"> var recaptcha; var onloadCallback = function(response) { recaptcha = grecaptcha.render('recaptcha-widget', { 'sitekey' : 'sitekey_goes_here', 'theme' : 'light', 'callback' : recaptchaCallback }); }; </script>
There are quite a few things going on here, so let's break it down.
First line is our form with a div container that the reCaptcha widget will be seen in. Next the script that asynchronously loads the necessary reCaptcha data from Google. Lastly, we include a script which will actually render the reCaptcha widget. We choose which div the widget is inserted into with ‘recaptcha-widget’. We also include the necessary reCaptcha sitekey, styling option (either light or dark), and the callback function which will execute when the user completes the reCaptcha.
Now that the reCaptcha is displaying properly, let’s build the callback function in a javascript module:
Javascript
window.recaptchaCallback = function(recaptchaResponse){ };
This function must be initialized before the reCaptcha widget is rendered, as the callback function must exist to be tied to the reCaptcha. Also, you can see that the function belongs to 'window' to it, so it can be accessed from the global scope.
When the callback function is called, it will pass a variable (recaptchaResponse), which is a unique token generated when the user completes the reCaptcha. In order to verify that this token is valid, we need call the Google API to confirm. Let's pass this post request to a plugin controller:
Javascript
window.recaptchaCallback = function(recaptchaResponse){ $.post('/', { action: 'recaptchas/verify', response: recaptchaResponse }, function(data, textStatus, jqXHR) { }); };
If you’re wondering why we’re posting to ‘/’, that’s because our route is actually passed through the action variable. This is a Craft-specific way to pass information between javascript and a Craft plugin controller. It will now execute the actionVerify function in the Recaptchas controller.
Inside our plugin controller, we make a simple post request, sending over all the necessary information:
PHP
public function actionVerify() { $recaptchaResponse = craft()->request->getParam('response'); $secret = 'google_recaptcha_secret_key'; $url = 'https://www.google.com/recaptcha/api/siteverify'; $fields = array('secret' => $secret, 'response' => $recaptchaResponse); $fields_string = http_build_query($fields); $ch = curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_POST,count($fields)); curl_setopt($ch,CURLOPT_POSTFIELDS,$fields_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result = curl_exec($ch); curl_close($ch); $json = json_decode($result, true); return $this->returnJson(array('success' => $json['success'])); }
This is pretty straightforward. We're setting up a post request to the Google reCaptcha API. We include two parameters, our site's secret key and the recaptchaResponse generated by the captcha. The response to the post will then be a boolean if the recaptchaResponse was valid. Let's now parse the response back in the module:
Javascript
window.recaptchaCallback = function(recaptchaResponse){ $.post('/', { action: 'recaptchas/verify', response: recaptchaResponse }, function(data, textStatus, jqXHR) { if (data['success']) { _submitEmailToList($form); } }); };
If the response is successful, now we call a function to submit the email to the newsletter. It can also be a good idea to add a way to reset the reCaptcha, in case a user needs to submit the form multiple times. We just add a brief function after the if statement:
Javascript
grecaptcha.reset(recaptcha);
Final Thoughts
Google’s NoCaptcha reCaptcha is a neat tool to filter out pesky bots. My biggest frustration is their lack of styling options for the reCaptcha box. There are two different size options and two color options. Other than that, there is no way to change what the captcha looks like. Luckily, the box is very minimal and can hopefully jive with most site designs. Overall though, implementation wasn’t too tricky and it works great on the site. Check out the developer’s guide here if you’re interested.
I'm working on a plugin version of this, to make it easier to implement on Craft sites. Keep an eye out for a blog post detailing that!