How to Enable a Google reCAPTCHA (aka Captcha) Image in an Angular/Node Application

image-post mask In this Brilliant Britz blog (say that 10 times fast) I will talk about how I embedded a simple captcha widget in my Angular/Node application. This task requires your Angular client and Node server to be changed, as the node server needs to make a request to the Google reCAPTCHA API server to verify key and challenge question. Having the Angular and Node kind of separation of concerns I'll go through the Angular side first, and then I'll end with the Node server side implementation.

Source Code for this example can be found here:  recaptcha sample

Angular/Client Side

reCAPTCHA comes with a couple of out of the box API's (ie-Java & PHP), however, at the time of this writing there is no JavaScript one. So I found this Angular directive which wraps up the basic API in a nice and easy to use service.

https://github.com/mllrsohn/angular-re-captcha

The author, steffenmllr, creates a simple yet effective library leveraging both the $provider service and Angular directives to use reCAPTCHA. Basically, it wraps up the AJAX calls reCaptcha needs to make when sending the challenge information to the server. Also, the included recaptcha directive requires an ngModel, which, it uses, to automatically attach the passed on information in the POST request to the server. I suggest taking a look at the code behind it, as it does this in an elegant manner. Shown below is an example of the directive in HTML:

                <div re-captcha ng-model="email.captcha">

If you didn't want to require an external JS library, like the reCAPTCHA one mentioned above, you can easily make your own. I didn't feel like reinventing the wheel, so I just thought I would use steffenmllr's.

Ok, first things first, and that is to sign up for your Google reCAPTCHA API keys. These are the public and private keys needed to verify the client that will be making the request is actually tied to the challenge picture. To obtain your keys you must have a valid Google mail account. If you have one then navigate to reCAPTCHA where you will be prompted to enter your Gmail credentials. Once logged in, you will need to know which domain you plan on using reCAPTCHA for. In addition to the domain name, any respective subdomain will be granted to use the same key. For example, when registering www.test.com the subdomain www.subdomain.test.com will also work.

By default, all keys work on "localhost" (or "127.0.0.1"), so you can always develop and test on your local machine.

For this blog I am only going to be concerned with localhost, however, the steps will be exactly the same when registered to another domain.

image

As the above image shows, I have added a domain called localhost. My localhost domain is a hyperlink, when clicked, displays both the private and public key associated with the registered domain.

The Public Key is for AngularJS/front-end and Private key is for NodeJS/back-end.

For localhost any private key with the public key will be sucessfull

Once you have the public key we can install it in our Angular application. To do this we can either create a module and insert the following code or just add it to the application module's .config section like so:

angular.module('myApp', ['reCAPTCHA'])

.config(function(reCAPTCHAProvider){

    //set Google API Public Key
    //Local
    reCAPTCHAProvider.setPublicKey('6LdUu_cSAAAAAJT-SnxZm_EL_NwazPuCwgfb70Wo');

    // optional: gets passed into the Recaptcha.create call
    reCAPTCHAProvider.setOptions({
        theme: 'blackglass'
    });
})

In addition to setting the public key you can pass in options during the app config function. For example, here I am using the blackglass theme supplied by reCAPTCHA.

Usage

To let the reCAPTCHA directive we just supply the recaptcha attribute on a div tag, which directs where to put the reCAPTCHA challenger. The directive does require that you have an ng-model defined for the reCAPTCHA challenge and answer to be stored in. Below is an example of how to use it:

                    <div re-captcha ng-model="email.captcha"></div>

This code is of course contained in a form which bundles up the ng-model of the email and sends it to the server in a POST http request. To accomplish this POST request, I used an Angular service to send the model. Below is the service I created to handle the POST request:

.factory('Recaptcha', ['$scope', '$resource', function ($scope, $resource) {

    return $resource(
        '/email/send',
        {id: '@id'},
        {
            send: {
                url: '/email/send',
                method: 'POST'
            }
        }
    )
}]);

Once the request is sent and received by Node we can then process verification. The full HTML source is found here.

We want to verify reCAPTCHA challenge on the server, so we dont have the client forcefully create a request based on challenge, as this could be done by a bot and completely trash the purpose of a captcha challenge.

Node/Server Side

On the server side all we need to do is send a request to Googles reCAPTHCA's API server which verifies our challenge (the recaptcha image), the public key is a match for the private key, and the IP address from the challengee is the same. To do this you must first create a Express route to handle and accept the POST request from our Angular service. To do this, I created an Express Router object that listens on the url /email/send. This is shown in this file found here. As one can see, I have this just sending back a "email sent" response message and dont really use a email service like Amazon SES(Simple Email Service), however, setting up a email transport (i.e. - nodemailer using SES) within node is not difficult, but is not in the context of this writeup. So lets jump into the code and start from there.

Receive POST Request from Angular Service

In order to receive requests from Angular we must first set up an Express route. As mentioned above, I have created a file specifically for my Express routes. This file is required in my app.js like so:

var routes = require('./routes/index');

and applied to the root path / like this:

app.use('/', routes);

where app is my express server. Now, within the Express routes file (index.js) I have 2 routes. One that handles the root path, which responds with the Angular index.html page, and the other at /email/send, which handles our verification and sending an email. When a POST request is made to the server we process the the request by first verifying the captcha information.

Verify Captcha Info

Before we process whatever we are trying to secure with the reCAPTCHA image we must verify that the information provided is correct. To accomplish this we make a request to Googles reCAPTCHA service and provide the information needed. Now I use a great Node package for making HTTP requests very simple. It's called request and it's github repo can be found here: request github repo. Using this package I can now internally make a request to the verification server like so:

    request.post('http://www.google.com/recaptcha/api/verify', {
        //should always store private keys as environment variables for many reasons
        form: {privatekey: process.env.RECAPTCHA_PRIVATE_KEY,
            //need requestors ip address
            remoteip: req.connection.remoteAddress,
            challenge: sender.captcha.challenge,
            response: sender.captcha.response}
    },
    function (err, res, body) {

        if(err){
            console.log('ERROR:\n ', err );
        }
        //if the request to googles verification service returns a body which has false within it means server failed
        //validation, if it doesnt verification passed
        if (body.match(/false/) === null) {

           response.send(200,'email sent');
        } else {
            response.send(500, {message: "Recaptcha Validation Failed.  Please Re-Enter the reCAPTCHA challenge.", err: err})
        }

    }
);

Now there are a couple things to note here. First, this section here:

      form: {privatekey: process.env.RECAPTCHA_PRIVATE_KEY,
            //need requestors ip address
            remoteip: req.connection.remoteAddress,
            challenge: sender.captcha.challenge,
            response: sender.captcha.response}

This is the information that the verification server requires. As you can see I have stored the reCAPTHCA private key in an environment variable, however, like stated above, when dealing with localhost any string in a private key will work. Once you go to a domain that is not localhost(127.0.0.1) then you will need to make sure the keys are accurate. This is the reason why the github source will work out of the box. The challenge is just the ID to the recaptcha challenge on the verification server, and response is what the user typed in to answer the challenge. The remoteip is the IP address of the user's computer, which is found in the request object. Now what do you do with the response back from the verification server?

Handling Response from Verification Server

Recaptcha's verification server will respond to our request with a true or false value. Of course, true means "all good" , and false means "No way." The strange thing is that the true and false words are in the body as a string, so we need to decipher that to flag if it was sucessfull in verifying or not. There might be a better way, but what I did was just add a simple regular expression to check if the string contains the word false, and if it did then it would not send continue processing and send a 500 response back with an error message. I demonstrate this right here:

        //if the request to googles verification service returns a body which has false within it means server failed
        //validation, if it doesnt verification passed
        if (body.match(/false/) === null) {

           response.send(200,'email sent');
        } else {
            response.send(500, {message: "Recaptcha Validation Failed.  Please Re-Enter the reCAPTCHA challenge.", err: err})
        }

Conclusion

So thats pretty much it. Real simple once someone writes it out, but kind of confusion when following the documentation. Anyways, hope this helps, and please leave any questions/comments in the comments sections. Thanks for viewing