Arcjet / NextAuth integration
Arcjet can protect your NextAuth login and signup routes from brute force attacks and other abuse. You can also use the Auth.js authenticated user ID to implement user-specific rate limits.
What is Arcjet?
Arcjet helps developers protect their apps in just a few lines of code. Bot detection. Rate limiting. Email validation. Attack protection. Data redaction. A developer-first approach to security.Example use case
- Protect your NextAuth route handlers by rate limiting requests and blocking bots.
- Limit access to a free API endpoint based on the client IP address.
- Provide a higher rate limit for authenticated clients based on their NextAuth user ID.
See an example Next.js implementation on GitHub.
Protect NextAuth route handlers
Arcjet can extend the NextAuth route handler to protect it from abuse.
This example configures a rate limit on the NextAuth route POST
handler which
is used for authentication and logout attempts. It allows up to 10 requests
within a 60 second window and also prevents bots from making requests.
NextAuth recommends creating a catch-all API route to handle all authentication requests. Arcjet extends this handler:
// This example is for NextAuth 4, the current stable versionimport arcjet, { detectBot, slidingWindow } from "@arcjet/next";import NextAuth from "next-auth";import GithubProvider from "next-auth/providers/github";import { NextResponse } from "next/server";
export const authOptions = { // Configure one or more authentication providers // See https://next-auth.js.org/configuration/initialization#route-handlers-app providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ],};
const handler = NextAuth(authOptions);
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ slidingWindow({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only interval: 60, // tracks requests across a 60 second sliding window max: 10, // allow a maximum of 10 requests }), detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ],});
const ajProtectedPOST = async (req: Request, res: Response) => { // Protect with Arcjet const decision = await aj.protect(req); console.log("Arcjet decision", decision);
if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return NextResponse.json({ error: "Too Many Requests" }, { status: 429 }); } else { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } }
// Then call the original handler return handler(req, res);};
export { handler as GET, ajProtectedPOST as POST };
NextAuth recommends creating a catch-all API route to handle all authentication requests. Arcjet extends this handler:
// This example is for NextAuth 4, the current stable versionimport arcjet, { detectBot, slidingWindow } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";import NextAuth from "next-auth";import GithubProvider from "next-auth/providers/github";
export const authOptions = { // Configure one or more authentication providers // See https://next-auth.js.org/configuration/initialization#route-handlers-app providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ],};
const handler = NextAuth(authOptions);
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ slidingWindow({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only interval: 60, // tracks requests across a 60 second sliding window max: 10, // allow a maximum of 10 requests }), detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only allow: [], // "allow none" will block all detected bots }), ],});
const ajProtectedHandler = async ( req: NextApiRequest, res: NextApiResponse,) => { if (req.method === "POST") { // Protect with Arcjet const decision = await aj.protect(req); console.log("Arcjet decision", decision);
if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return res.status(429).json({ error: "Too many requests" }); } else { return res.status(403).json({ error: "Forbidden" }); } } }
// Then call the original handler return handler(req, res);};
export default ajProtectedHandler;
Rate limits using NextAuth user ID
Arcjet rate limits allow custom
characteristics to
identify the client and apply the limit. Using NextAuth’s
getServerSession()
helpers you can pass through a user ID.
// This example is for NextAuth 4, the current stable versionimport arcjet, { tokenBucket } from "@arcjet/next";import { getServerSession } from "next-auth";import GithubProvider from "next-auth/providers/github";import { NextResponse } from "next/server";
export const authOptions = { // Configure one or more authentication providers // See https://next-auth.js.org/configuration/initialization#route-handlers-app providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ],};
// The arcjet instance is created outside of the handlerconst aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com characteristics: ["user"], // Track based on the Clerk userId rules: [ // Create a token bucket rate limit. Other algorithms are supported. tokenBucket({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only refillRate: 5, // refill 5 tokens per interval interval: 10, // refill every 10 seconds capacity: 10, // bucket maximum capacity of 10 tokens }), ],});
export async function GET(req: Request) { // Get the current user from NextAuth const session = await getServerSession(authOptions); if (!session || !session.user || !session.user.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
// A very simple hash to avoid sending PII to Arcjet. You may wish to add a // unique salt prefix to protect against reverse lookups. const email = session.user.email; const emailHash = require("crypto") .createHash("sha256") .update(email) .digest("hex");
// Deduct 5 tokens from the user's bucket const decision = await aj.protect(req, { user: emailHash, requested: 5, });
console.log("Arcjet decision", decision);
if (decision.isDenied()) { return NextResponse.json( { error: "Too Many Requests", reason: decision.reason, }, { status: 429, }, ); }
return NextResponse.json({ message: "Hello World" });}
// This example is for NextAuth 4, the current stable versionimport arcjet, { tokenBucket } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";import { getServerSession } from "next-auth";import GithubProvider from "next-auth/providers/github";
export const authOptions = { // Configure one or more authentication providers // See https://next-auth.js.org/configuration/initialization#route-handlers-app providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ],};
// The arcjet instance is created outside of the handlerconst aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com characteristics: ["user"], // Track based on the Clerk userId rules: [ // Create a token bucket rate limit. Other algorithms are supported. tokenBucket({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only refillRate: 5, // refill 5 tokens per interval interval: 10, // refill every 10 seconds capacity: 10, // bucket maximum capacity of 10 tokens }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { // Get the current user from NextAuth const session = await getServerSession(req, res, authOptions); if (!session || !session.user || !session.user.email) { return res.status(401).json({ error: "Unauthorized" }); }
// A very simple hash to avoid sending PII to Arcjet. You may wish to add a // unique salt prefix to protect against reverse lookups. const email = session.user.email; const emailHash = require("crypto") .createHash("sha256") .update(email) .digest("hex");
// Deduct 5 tokens from the user's bucket const decision = await aj.protect(req, { user: emailHash, requested: 5, });
console.log("Arcjet decision", decision);
if (decision.isDenied()) { return res .status(429) .json({ error: "Too Many Requests", reason: decision.reason }); }
return res.status(200).json({ message: "Hello World" });}