Integración de Recaptcha con el framework Astro components

Integración de Recaptcha con el framework Astro components

por Rebeca Murillo • 21 diciembre 2023


Introducción

Para fortalecer la seguridad del formulario de contacto de mi sitio web, decidí integrar reCaptcha V3 de Google con una verificación de token en el lado del servidor. Después de configurar los scripts, me encontré con un problema de CORS al cargar el script de reCaptcha. En esta guía, destacaré los puntos clave para asegurar una integración fluida y evitar el problema de CORS.

Problema de CORS al cargar el script de reCaptcha V3

Esta integración de reCaptcha invoca un desafío y valida el comportamiento del usuario en segundo plano, brindando una experiencia de usuario más fluida.

Guía paso a paso

Los requisitos previos para esta guía:

  • Conocimientos básicos de Node.js y el framework Astro

1. Crear un sitio de Google reCaptcha

Para crear una configuración de sitio de Google reCaptcha, crea un nuevo sitio en la consola de administración de Google reCaptcha.

  • Accede a la administración de Google reCaptcha
  • Crea un nuevo sitio
  • En el tipo, selecciona Puntuación basada (v3)
  • En Dominios, ingresa el dominio de tu sitio web. Por ejemplo: “mywebsite.com”
  • Obtén la clave del sitio para tu sitio front-end. Identificada como MY_reCAPTCHA_SITE_ID en este tutorial
  • Obtén la clave secreta para la validación del token en el lado del backend. Identificada como MY_reCAPTCHA_SECRET_KEY

Administración de Google reCaptcha

2. Integrar el script de reCaptcha en un sitio Astro

El script necesario para integrar reCaptcha en un sitio web se explica en la documentación de Google para V3 - Invocar el desafío de forma programática.

Para un sitio web implementado con el framework Astro como sitio estático (CSR renderizado en el lado del cliente), debes tener en cuenta la siguiente información.

El script de reCaptcha debe agregarse en el elemento <head> de la página, con los atributos async defer, para que el script se descargue sin bloquear la carga de la página. Esto garantizará que el JavaScript compilado para el componente Astro se cargue correctamente.

Más información sobre los atributos async defer en esta documentación de JavaScript.

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

3. Generar un token de desafío reCaptcha al enviar el formulario

Crea un nuevo componente Astro con tus campos de formulario. En el siguiente ejemplo, el formulario consta de campos de nombre, correo electrónico y mensaje. Luego, al enviar el formulario, los datos del formulario se envían al backend, que es responsable de verificar el token generado por el desafío reCaptcha.

Fichier 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>

Ahora puedes llamar a tu componente de Contacto Astro en tu página index.astro.

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

4. Verificar el token de desafío reCaptcha en tu servidor backend

Para verificar el token desde tu servidor backend, llama a la ruta /siteverify. Obtén más información sobre la ruta y los posibles códigos de respuesta en la documentación Guía de verificación de respuesta de usuario de reCaptcha.

Aquí tienes un ejemplo de controlador de ruta de API backend, que recibe un cuerpo con las entradas del formulario y el desafío reCaptcha. Esta función esta publicada a través de una función sin servidor (serverless), obtén más información al respecto en mi tutorial CI/CD: Enviar un correo electrónico con una función sin servidor en AWS Lambda.

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
}