Recaptcha integration with Astro components framework

Recaptcha integration with Astro components framework

by Rebeca Murillo • 21 December 2023


Introduction

In order to enforce security on the contact form of my website, I decided to integrate Google’s reCaptcha V3, with the server side token verification. After setting up the scripts, I kept encountering a CORS issue when loading the reCaptcha script. In this guide I will highlight the keypoints to ensure a smooth integration to avoid the CORS issue.

CORS issue when loading reCaptcha V3 script

This integration of reCaptcha invokes a challenge and validates the user behaviour in the background, providing a smoother user experience.

Step by step guide

The requirements for this guide :

  • Nodejs and Astro framework basic knowledge

1. Create Google reCaptcha site

In order to create a Google reCaptcha site configuration, create a new site on the Google reCaptcha admin console.

  • Go to Google reCaptcha admin
  • Create a new site
  • Under type, Select Score based (v3)
  • Domains, enter your website domain. For example: “mywebsite.com”
  • Retrieve the website key, for your front-end website. Identified as MY_reCAPTCHA_SITE_ID in this tutorial
  • Retrieve the secret key, for your backend token validation. Identified as MY_reCAPTCHA_SECRET_KEY

Google reCaptcha admin

2. Integrate reCaptcha script in your Astro website

The script required to integrate reCaptcha in a website is explained in Google’s documentation for V3 Programmatically invoke the challenge.

If your website is implemented with the Astro framework as a static website (Client Side Rendering), take note the following information.

The reCaptcha script needs to be added in the page <head> element, with the async defer attributes, so that the script is downloaded without blocking the page loading. This will ensure that the Javascript compiled for the Astro component is loaded correctly.

More on async defer attributes in this Javascript documentation

<head>
<script
  src="https://www.google.com/recaptcha/api.js?render=MY_reCAPTCHA_SITE_ID" async defer
></script>
</head>  

3. Generate a reCaptcha challenge token on form submit button

Create a new Astro component with your form inputs. In the following example, the form consists of a name, email and message inputs. Then on the submit button, the form data is sent to the backend, which is responsible for verifying the token generated by the reCaptcha challenge.

File src/components/Contact.astro

<script>
  const successMessageElement = document.getElementById("success-message");
  const errorMessageElement = document.getElementById("error-message");
  const informationMessageElement = document.getElementById(
    "information-message"
  );

  function onSuccess() {
    successMessageElement?.classList.remove("hidden");
    errorMessageElement?.classList.add("hidden");
    informationMessageElement?.classList.add("hidden");
  }

  function onError() {
    successMessageElement?.classList.add("hidden");
    errorMessageElement?.classList.remove("hidden");
    informationMessageElement?.classList.add("hidden");
  }

  function onInformation() {
    successMessageElement?.classList.add("hidden");
    errorMessageElement?.classList.add("hidden");
    informationMessageElement?.classList.remove("hidden");
  }

  function sendEmail({ name, email, message }) {
    grecaptcha.ready(function () {
      grecaptcha
        .execute("MY_reCAPTCHA_SITE_ID", {
          action: "submit",
        })
        .then(function (recaptchaToken) {
          postMessage(
            { name, email, message, recaptchaToken },
            onSuccess,
            onError
          );
        });
    });
  }

  async function postMessage(
    { name, email, message, recaptchaToken },
    onSuccess,
    onError
  ) {
    const url = `https://my.backend.api/message`;

    const data = {
      from: email,
      text: message + `\n` + name,
      subject: `Contact message from ${name}`,
      recaptchaToken: recaptchaToken,
    };

    try {
      const response = await fetch(url, {
        method: "POST",
        body: JSON.stringify(data),
      });

      if (response.ok) {
        onSuccess();
      } else {
        onError();
      }
    } catch (e) {
      onError();
    }
  }

  const btnContactSend = document.getElementById("btn-contact-send");
  btnContactSend?.addEventListener("click", function (event) {
    event.preventDefault();
    const name = document.getElementById("input-name")?.value;
    const email = document.getElementById("input-email")?.value;
    const message = document.getElementById("input-message")?.value;

    if (name?.length > 0 && email?.length > 0 && message?.length > 0) {
      sendEmail({ name, email, message });
    } else {
      onInformation();
    }
  });
</script>

<div id="contact" class="flex flex-col w-full md:w-8/12 lg:w-6/12 m-auto px-1">
  <h2 class="text-center">My contact form</h2>
  <div
    class="col-span-1 flex flex-col p-5 rounded-xl my-2 lg:m-10 border-solid border shadow-lg border-gray-950 bg-gray-400"
  >
    <form id="contact-form" class="flex flex-col p-2 lg:p-8 gap-6">
      <Input id="input-name" label="Name" type="text" />
      <Input id="input-email" label="Email" type="text" />
      <Input id="input-message" label="Message" type="textarea" />
      <span class="hidden text-red-500" id="error-message">
        Error sending the email
      </span>
      <span class="hidden text-blue-800" id="information-message">
        The fields are incorrect
      </span>
      <span class="hidden text-green-500" id="success-message">
        Message was sent successfully
      </span>
      <Button id="btn-contact-send" label="Send" type="primary" />
    </form>
  </div>
</div>

You can now call your Contact Astro component in your index.astro page.

<head>
<script
  src="https://www.google.com/recaptcha/api.js?render=MY_reCAPTCHA_SITE_ID" async defer
></script>
</head>
<body>
  <Contact />
</body>

4. Verify the reCaptcha challenge token in your backend server

To verify the token from your backend server, call the siteverify route. More information about the route and the possible response codes in the documentation reCaptcha guide Verifying the user’s response  This is an example of a backend API route controller, which takes a body containing the form inputs and the reCaptcha challenge. The main function is the handler of a serverless function, more about this in my tutorial CI/CD Send email serverless function in AWS

async function validateCaptcha(recaptchaToken) {
  const response = await fetch(
    "https://www.google.com/recaptcha/api/siteverify",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        secret: "MY_reCAPTCHA_SECRET_KEY",
        response: recaptchaToken,
      }),
    }
  );

  const data = await response.json();
  console.log("recaptcha response: ", data);
  return data;
}

exports.handler = async (event) => {
  console.log("event: ", event);

  const { recaptchaToken, from, subject, text } = JSON.parse(event.body);

  // Step 1: Validate the reCAPTCHA response
  const captchaValidationResult = await validateCaptcha(recaptchaToken);
  if (!captchaValidationResult.success) {
    return response(400, { message: "reCAPTCHA verification failed" });
  }

  // Step 2... continue
}