Arcjet / Auth.js integration
Arcjet can protect your Auth.js 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 Auth.js route handlers by rate limiting requests and blocking bots.
- Provide a higher rate limit for authenticated clients based on their Auth.js user ID.
See an example Next.js implementation on GitHub.
Protect Auth.js route handlers
Arcjet can extend the Auth.js route handler to protect it from abuse.
This example configures a rate limit on the Auth.js 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.
Auth.js recommends creating a catch-all API route to handle all authentication requests. Arcjet extends this handler:
// This example is for Auth.js 5, the successor to NextAuth 4import arcjet, { detectBot, slidingWindow } from "@arcjet/next";import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";import { NextRequest, NextResponse } from "next/server";// @ts-ignoreimport type { NextAuthConfig } from "next-auth";
export const config = { providers: [GitHub],} satisfies NextAuthConfig;
const handlers = NextAuth(config);
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 }), ],});
// Protect the sensitive actions e.g. login, signup, etc with Arcjetconst ajProtectedPOST = async (req: NextRequest) => { 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 }); } }
return handlers.POST(req);};
// You could also protect the GET handler, but these tend to be less sensitive// so it's not always necessaryconst GET = async (req: NextRequest) => { return handlers.GET(req);};
export { GET, ajProtectedPOST as POST };
Auth.js recommends creating a catch-all API route to handle all authentication requests. Arcjet extends this handler:
// This example is for Auth.js 5, the successor to NextAuth 4import arcjet, { detectBot, slidingWindow } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";// @ts-ignoreimport type { NextAuthConfig } from "next-auth";
export const config = { providers: [GitHub],} satisfies NextAuthConfig;
const handlers = NextAuth(config);
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 handlers(req, res);};
export default ajProtectedHandler;
Rate limits using Auth.js user ID
Arcjet rate limits allow custom
characteristics to
identify the client and apply the limit. Using the Auth.js
auth
helper you can pass through a
user ID.
// This example is for Auth.js 5, the successor to NextAuth 4import arcjet, { tokenBucket } from "@arcjet/next";import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";// @ts-ignoreimport type { NextAuthConfig } from "next-auth";
export const config = { providers: [GitHub],} satisfies NextAuthConfig;
const { auth } = NextAuth(config);
const aj = arcjet({ key: process.env.ARCJET_KEY!, characteristics: ["userId"], // 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 const GET = auth(async (req: any) => { if (req.auth) { console.log("User:", req.auth.user);
// If there is a user ID then use it, otherwise use the email let userId: string; if (req.auth.user?.id) { userId = req.auth.user.id; } else if (req.auth.user?.email) { // 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 = req.auth.user!.email; const emailHash = require("crypto") .createHash("sha256") .update(email) .digest("hex");
userId = emailHash; } else { return Response.json({ message: "Unauthorized" }, { status: 401 }); }
// Deduct 5 tokens from the token bucket const decision = await aj.protect(req, { userId, requested: 5 }); console.log("Arcjet Decision:", decision);
if (decision.isDenied()) { return Response.json( { error: "Too Many Requests", reason: decision.reason, }, { status: 429, }, ); }
return Response.json({ data: "Protected data" }); }
return Response.json({ message: "Unauthorized" }, { status: 401 });}) as any;
// This example is for Auth.js 5, the successor to NextAuth 4import arcjet, { tokenBucket } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";// @ts-ignoreimport type { NextAuthConfig } from "next-auth";
export const config = { providers: [GitHub],} satisfies NextAuthConfig;
const { auth } = NextAuth(config);
const aj = arcjet({ key: process.env.ARCJET_KEY!, characteristics: ["userId"], // 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,) { const session = await auth(req, res); if (!session || !session.user || !session.user.email) { return res.status(401).json({ error: "Unauthorized" }); }
console.log("User:", session.auth.user);
// If there is a user ID then use it, otherwise use the email let userId: string; if (session.auth.user?.id) { userId = session.auth.user.id; } else if (session.auth.user?.email) { // 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.auth.user!.email; const emailHash = require("crypto") .createHash("sha256") .update(email) .digest("hex");
userId = emailHash; } else { return Response.json({ message: "Unauthorized" }, { status: 401 }); }
// Deduct 5 tokens from the token bucket const decision = await aj.protect(req, { userId, requested: 5 }); console.log("Arcjet Decision:", decision);
if (decision.isDenied()) { return Response.json( { error: "Too Many Requests", reason: decision.reason, }, { status: 429, }, ); }
return Response.json({ data: "Protected data" });}
Chaining middleware
If you want to protect every page with Arcjet Shield automatically you can run it through Next.js middleware. Auth.js can also use middleware to add authentication to your pages. You can chain the two together.
// This example is for Auth.js 5, the successor to NextAuth 4import arcjet, { createMiddleware, shield } from "@arcjet/next";import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";// @ts-ignoreimport type { NextAuthConfig, NextAuthRequest } from "next-auth";
export const config = { // matcher tells Next.js which routes to run the middleware on. // This runs the middleware on all routes except for static assets. matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],};const aj = arcjet({ key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), ],});
export const authConfig = { providers: [GitHub],} satisfies NextAuthConfig;
const { auth } = NextAuth(authConfig);
export const authMiddleware = auth(async (req: NextAuthRequest) => { if (!req.auth) { // If the user is not authenticated, return a 401 Unauthorized response. You // may wish to redirect to a login page instead. return Response.json({ message: "Unauthorized" }, { status: 401 }); }});
export default createMiddleware(aj, authMiddleware);