How to protect your Next.js Routes with reCAPTCHA
Published: 2024-05-26 06:58:04

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.


![captcha](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmrqvybktaxasdha9n28.jpeg)

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.


![create captcha](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwj6vza30g3k20mr6i9z.jpeg)

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.


![recaptcha keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/71qs5sxuz3gb6ipdu2oz.jpeg)

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.

```yaml
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:

```tsx
<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

```tsx
<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`

```tsx
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`)

```tsx
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 to solve a challenge to our users, which is perfect to provide them a great user experience.

I hope this was useful.

Cheers