Protect your app login page from brute force attacks using reCAPTCHA

The code in this tutorial was tested on AppGini 22.13 and 22.14

A common problem facing all websites is malicious 'bots', automated scripts designed to "efficiently" perform harmful actions to websites. These include posting spam in forums, signing up with fake accounts (usually to post spam), trying to hack legit accounts by making thousands of password guesses in login forms (also known as brute force attacks), .. etc.

A very efficient way of preventing these attacks is to use reCAPTCHA, a tool offered by Google that adds a simple challenge to the page you want to protect. This challenge is trivially easy for humans to solve, but almost impossible for bots.

In this tutorial, we'll see how to protect the login form of an AppGini application with a reCAPTCHA. At the time of writing this tutorial, Google offers 3 types of reCAPTCHA:

  • reCAPTCHA v2: "I'm not a robot" checkbox.
  • reCAPTCHA v2: Invisible reCAPTCHA badge (no user interaction required)
  • reCAPTCHA v3: Invisible, provides a score indicating probability of user being human (0.0 = a bot, 1.0 = a human) .. needs some tweaking and a bit harder to implement.

We'll use the 1st type ("I'm not a robot" checkbox) in this tutorial. If you wish to use a different type, please keep reading this tutorial to see the general steps involved, then read the reCAPTCHA documentation for the specific code to use for other types.

Step 1: Obtain reCAPTCHA credentials from Google

In order to be able to use reCAPTCHA, we must obtain a site key and secret key from Google reCAPTCHA dashboard. The following video describes the steps in detail:

Make sure to enter the correct domain(s) for the servers hosting your AppGini app in the above step.

Step 2: Store reCAPTCHA credentials into a config file

Create a file inside the hooks folder, naming it reCaptcha.config.php. Add the following code to it:

<?php
  define('RECAPTCHA_SITE_KEY', 'xxxxxxxxxxxxx');
  define('RECAPTCHA_SECRET_KEY', 'yyyyyyyyyyyyy');

Replace xxxxxxxxxxxxx and yyyyyyyyyyyyy in the above code with your reCAPTCHA site key and secret key, respectively.

Important security note: If you are using git or any other code repository, you must add this file to your .gitignore file (if using git), or any other ignore mechanism to avoid including the secret keys into your code repository. API keys and other secrets should never be committed into code repositories.

Step 3: Show the captcha in your login form

Next, add the following code to hooks/footer-extras.php:

<?php if(Request::val('signIn') || Request::val('loginFailed')) { ?>
  <?php
    // reCaptcha config
    include(__DIR__ . '/reCaptcha.config.php');
  ?>
  <script src="https://www.google.com/recaptcha/api.js" async defer></script>
  <script>$j(() => {
    // add reCAPTCHA checkbox to the login form, after the 'remember me' checkbox
    $j(`<div
        class="g-recaptcha bspacer-lg"
        data-sitekey="<?php echo RECAPTCHA_SITE_KEY; ?>"
        data-theme="light"
    ></div>`).insertAfter('div.checkbox')

    // prevent form submission if reCAPTCHA not solved
    $j('form').on('submit', (e) => {
      if(!grecaptcha.getResponse().length)
        e.preventDefault();
    })

    // disable 'sign in' button if reCAPTCHA not solved
    $j('#submit').prop('disabled', true);
    setInterval(() => {
      $j('#submit').prop('disabled', !grecaptcha.getResponse().length);
    }, 100)
  })</script>
<?php } ?>

After saving the above code, test your form in the browser. Here is an example:

So far, we've successfully implemented the front-end (UI) part of the captcha. Next, we'll implement the back-end part, where we'll check that the user has actually solved the captcha challenge in order to sign in.

Step 4: Server-side (back-end) validation of reCAPTCHA

In hooks/__global.php, add the following code before the line containing function login_ok:

  // reCaptcha config
  include(__DIR__ . '/reCaptcha.config.php');

  // invalidate login if reCAPTCHA is invalid
  if(
    Request::val('signIn')
    && Request::val('username')
    && Request::val('password')
    && !validRecaptcha()
  ) {
    unset($_REQUEST['username']);
    unset($_REQUEST['password']);
    unset($_REQUEST['signIn']);
    redirect('index.php?loginFailed=1');
  }

  function validRecaptcha() {
    // get reCAPTCHA response from login form
    $reCaptchaResponse = Request::val('g-recaptcha-response');
    if(!$reCaptchaResponse) return false;

    if(!function_exists('curl_init')) return false;

    // send a POST request to Google's reCAPTCHA validation API endpoint
    $ch = curl_init();
    curl_setopt_array($ch, [
      CURLOPT_URL => 'https://www.google.com/recaptcha/api/siteverify',
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => http_build_query([
        'secret' => RECAPTCHA_SECRET_KEY,
        'response' => $reCaptchaResponse,
      ]),
      CURLOPT_RETURNTRANSFER => true,
    ]);

    $googleRespJson = curl_exec($ch);
    curl_close($ch);

    // if error response, abort
    if($googleRespJson === false) return false;
    $googleResp = @json_decode($googleRespJson, true);
    if($googleResp === null) return false;

    return !empty($googleResp['success']);
  }

Troubleshooting

  • Are you using AppGini 22.13 or above? We haven't tested the code in this page on older versions, and can't be sure if it works on them.
  • Have you stated the domain correctly in step 1?
  • If the login form shows the reCAPTCHA checkbox correctly, but login doesn't work despite solving the reCAPTCHA, please make sure you have php-curl installed and enabled.

Conclusion

The above tutorial explains how to protect your AppGini app's login form with a reCAPTCHA to prevent brute force attacks that try to guess account credentials.

You can protect other forms in your app (for example the sign up form, or any data entry form) by slightly modifying the code in step 3 to add the captcha component to other forms. You can then reuse the validRecaptcha() function from step 4 to validate the captcha. I'll leave this as an exercise to you :)