DEV Community

Cover image for How to protect your Next.js Routes with reCAPTCHA
Luca Restagno
Luca Restagno

Posted on • Originally published at shipped.club

How to protect your Next.js Routes with reCAPTCHA

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

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

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

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"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}`}
/>
Enter fullscreen mode Exit fullscreen mode

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
              })
          }
        });
    });
  };
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)