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