How to protect your Next.js Routes with reCAPTCHA

Luca RestagnoLuca Restagno
4 min read

Protecting the public endpoints of your web app, is one of the most important tasks you could do.

Even if you don’t expect much traffic on your websites, malicious attempts can always happen.

It happened to me when I launched a waitlist website, I didn’t expect many eyes to visit the page, but someone noticed the /api/waitlist endpoint, I used to collect the email of the interested users, and they started to call it repeatedly.

One of the easiest mitigations is to add a captcha challenge to the web user interface.

There are different types of captchas, and they have evolved quite a lot over the last few years.

Usually, they are visual quizzes or simple puzzles (called challenges) to solve to unlock a feature on a website.

This is an example, select all square images with traffic lights.

Robots can’t solve these puzzles, therefore the backend request doesn’t start.

But how can you protect your backend with a puzzle solved on the frontend?

How Captcha Protection Works

This is how it works.

When the puzzle is successfully solved, the captcha service delivers a token (a string).

This token is unique for the puzzle resolution of a user, and you need to send it to your backend.

In your backend, you need to validate the token, by calling the captcha service backend.

In this article, I show you how you can implement a captcha protection using on the most famous service reCAPTCHA by Google and your Next.js website.

reCAPTCHA by Google has been improved over time, and the latest version of it, version 3, doesn’t require every user to solve the challenge is the proprietary algorithm of Google doesn’t recognize a suspect client.

Which is a great news for our real users!

Create a reCAPTCHA

First of all, create a reCAPTCHA. Visit the website https://www.google.com/recaptcha/about/ and access the v3 Admin Console.

Once in, click on the “+” plus icon to create a new reCAPTCHA.

Add the label and the domain of your website.

You can select v3 (score based) or v2.

v2 always asks for a challenge, while v3 asks for a challenge only if the user score, automatically calculated, is not high. I select v3.

Click on Submit.

Now Google gives you the Site Key and the Secret Key. Copy them in a secure place.

Now let’s create two environment variables.

Usually you have a .env file for your local development and you need to set them on your hosting solution, like Vercel.

NEXT_PUBLIC_RECAPTCHA_SITE_KEY="you_site_key"
RECAPTCHA_SECRET_KEY="your_secret_key"

Notice that one environment variables is prefixed with NEXT_PUBLIC_ while the other not.

NEXT_PUBLIC_RECAPTCHA_SITE_KEY is accessible from the frontend, and therefore it’s publicly visible, while RECAPTCHA_SECRET_KEY will be accessible only by the backend code, and therefore no one can read the value.

Create your client component

Now, let’s create a Next.js client component with an input field and a button.

For a waitlist it would look like this one:

<input 
    placeholder="your@email.com"
    onChange={e => setEmail(e.target.value)}
/>
<button
        onClick={onAddToWaitlist}
>
    Join the waitlist
</button>

Now, we need to implement onAddToWaitlist so that it calls the reCAPTCHA service.

To integrate reCAPTCHA you need to integrate the Google JavaScript script.

Open your layout.tsx file (if you are using the App Router) and add this

<script
    defer
  type="text/javascript"
  src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
/>

At this point, the JavaScript object grecaptcha will be globally available in our web app.

Let’s implement onAddToWaitlist

const onAddToWaitlist = () => {
    // @ts-ignore
    grecaptcha.ready(function () {
      // @ts-ignore
      grecaptcha
        .execute(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, {
          action: "submit",
        })
        .then(function (token: string) {
          if (email) {
            axios
              .post("/api/waitlist", {
                email,
                captchaToken: token,
              })
              .then(() => {
                                // success
              })
              .catch((err) => {
                  // error
              })
          }
        });
    });
  };

I used axios because it’s comfortable to use, but you can use fetch as well.

Protect your API route

At this point, we are only missing the backend api route (src/app/api/waitlist/route.ts)

import axios, { HttpStatusCode } from "axios";
import { NextResponse } from "next/server";
import qs from "qs";

export async function POST(req: Request) {
  const body = await req.json();
  const email = body.email;
  const captchaToken = body.captchaToken;

  if (!captchaToken) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: HttpStatusCode.Unauthorized }
    );
  }

  if (!email) {
    return NextResponse.json(
      { error: "Email is required" },
      { status: HttpStatusCode.BadRequest }
    );
  }

  const options = {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    data: qs.stringify({
      secret: process.env.RECAPTCHA_SECRET_KEY,
      response: captchaToken,
    }),
    url: "<https://www.google.com/recaptcha/api/siteverify>",
  };

  const response = await axios(options);

  if (response.data.success === false) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: HttpStatusCode.Unauthorized }
    );
  }

  // the captcha token is valid
}

Conclusion

Captchas are one of the most effective ways to protect a website, and the easiest solution to implement.

The captcha service from Google has improved a lot lately, and it doesn’t always require solving a challenge for our users, which is perfect to provide them with a great user experience.

I hope this was useful.

Cheers

Luca

0
Subscribe to my newsletter

Read articles from Luca Restagno directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Luca Restagno
Luca Restagno