Home CAPTCHA
CAPTCHA
Cancel

CAPTCHA

Learning Outcomes

  • explain the role of CAPTCHAs in securing web applications
  • secure a web application using CAPTCHAs
  • utilize a 3rd party service in a web application backend

Resources

Lab

Video walk through of this lab. (NOTE: this video is for an earlier version of lab that used a more complicated but more flexible approach on the client side. The instructions below are for a much simpler client side implementation but will not match the video. The backend material is pretty much the same and has not changed and should match the video).

You can start with your own completed REST assignment, or if there are problems with that, you can use the https://gitlab.com/langarabrian/rest2-final project. If you use that one, be sure to fork, remove the fork relationship and make private.

Set the MONGODBURI environment variable as per previous exercises.

Install the dependencies and run in dev mode:

1
2
3
cd rest2-final
npm install
PORT=8080 npm run dev

Verify that the application is running in the web preview.

In a new terminal tab, try this (remove the spaces around “localhost” first):

1
2
3
curl -d '{"firstName":"Bob", "lastName":"Smith", "email":"[email protected]", "country":"USA", "province":"WA", "postalCode":"12345"}' \
    -H "Content-Type: application/json" \
    -X POST http:// localhost :8080/signups

Refresh your app in the web preview tab and confirm that the new record appears in the table of signups. This means our app is vulnerable to bots that could create 1,000,000s of signups without any human interactions. In the next section we will add a CAPTCHA to discourage bots.

Configure reCAPTCHA Service

Go to the Google reCAPTCHA Admin Console.

Type in a label, like “Signup App”.

For “reCAPTCHA Type”, choose “reCAPTCHA v3”.

For “Domains”, enter the domain from your web preview (which would be something like 8080-dot-3969277-dot-devshell.appspot.com)

Accept the reCAPTCHA Terms of Service.

Click on the “SUBMIT” button.

Record the “Site” and “Secret” keys.

Configure reCAPTCHA in the Frontend

In public/index.html, add the following just before “form-validation.js”:

<script src="https://www.google.com/recaptcha/api.js"></script>

Add an id attribute to the form element as follows:

<form class="needs-validation" id="signupForm" novalidate>

Rewrite the markup for the “Signup” but as follows with your actual reCAPTCHA site key:

<button class="btn btn-primary btn-lg btn-block g-recaptcha"
        data-sitekey="YOUR_ACTUAL_SITE_KEY_HERE"
        data-callback='onSubmit'
        data-action='signup'>Signup</button>

In public/form-validation.js, add the following code at the very top of the file – completely outside the large anonymous function:

let globalToken = "";

function onSubmit(token) {
  globalToken = token;
  document.getElementById("signupForm").requestSubmit();
}

Pass the reCAPTCHA token to the backend by including it in the call to the signups.create() method invocation:

signups.create({
  firstName: $('#firstName').val(),
  lastName: $('#lastName').val(),
  email: $('#email').val(),
  country: $('#country').val(),
  province: $('#province').val(),
  postalCode: $('#postalCode').val(),
  token: globalToken
});

Back in the terminal tab where the app is running, type Ctrl-C to stop the Node app temporarily.

Configure reCAPTCHA in the Application Backend

We are going to create a hook function to be called before a new signup is created to validate the reCAPTCHA:

npm i -g @feathersjs/cli    # only if you need to
feathers generate hook

Answer the questions as follows:

1
2
3
4
What is the name of the hook? process-signup
What kind of hook should it be? before
What service should this hook be for? signups
What methods should the hook be for? create

In the newly generated file, src/hooks/process-signup.ts, add the following imports to bring in modules that we will need to interact with the reCAPTCHA verification service:

// used to build the query string passed to the reCAPTCHA service
import querystring from 'querystring';

// used to connect with the reCAPTCHA service
import axios from 'axios';

The generated hook function does nothing by default. It takes a single parameter, context, which contains information about the incoming service request and returns it unmodified, without performing any additional actions:

return context;

Replace the body of the hook function with the following code block:

    // extract the incoming request data
    const { data } = context;

    // FYI
    console.log( data );

    // verify the incoming token against the reCAPTCHA service
    const response = await axios.post(
      'https://www.google.com/recaptcha/api/siteverify',
      querystring.stringify({
        secret: process.env.RECAPTCHA_SECRET,
        response: data.token
      })
    );

    // FYI
    console.log(response.data);

    // if the response fails or the score is too low, throw an error
    if ( !response.data.success || response.data.score < 0.95 ) {
      throw new Error('reCAPTCHA fail');      
    }

    // if everything is OK, carry on
    return context;

Set the RECAPTCHA_SECRET environment variable as follows (using your actual secret):

1
export RECAPTCHA_SECRET=your-actual-secret-here

Re-start the backend. Try submitting the form. Note your score in the backend terminal window. Play around with the response.data.score threshold to get something acceptable.

Right now there is nothing in the frontend UI to indicate that the user has failed the reCAPTCHA. If you look in the web console in the browser, you can see the thrown exception. Let’s add a block in the HTML that is normally hidden. We will reveal it when we detect the exception. In the public/index.html file, add the following just above the “Signup” button:

<div class="alert alert-danger d-none" role="alert" id="captchaMessage">
    Failed reCAPTCHA
</div>

Handle reCAPTCHA Fails Gracefully in the Frontend

In public/form-validation.js, rewrite the signups.create() method invocation with a then() and catch() method invocations as follows to handle the successful and error cases:

signups.create({
  firstName: $('#firstName').val(),
  lastName: $('#lastName').val(),
  email: $('#email').val(),
  country: $('#country').val(),
  province: $('#province').val(),
  postalCode: $('#postalCode').val(),
  token: globalToken
})
.then(signup => {
  addSignup(signup);
  $('#captchaMessage').addClass('d-none');
  form.classList.remove('was-validated');
  form.reset();
})
.catch(() => {
  $('#captchaMessage').removeClass('d-none');
});

Assignment

  1. Complete the Feathers JS Tutorial in The Feathers guide with the following modifications:
    • complete the TypeScript version
    • instead of the NeDB datastore, use the Mongoose driver instead
  2. Incorporate reCAPTCHA v3 into the tutorial so that a new account is only registered if the score is above 0.7.