Skip to content

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:

/app/api/auth/[...nextauth]/route.ts
// This example is for Auth.js 5, the successor to NextAuth 4
import 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-ignore
import 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 Arcjet
const 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 necessary
const GET = async (req: NextRequest) => {
return handlers.GET(req);
};
export { GET, ajProtectedPOST as POST };

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.

/app/api/private/route.ts
// This example is for Auth.js 5, the successor to NextAuth 4
import arcjet, { tokenBucket } from "@arcjet/next";
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
// @ts-ignore
import 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: "Not authenticated" }, { status: 401 });
}) as any;

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.

/middleware.ts
// This example is for Auth.js 5, the successor to NextAuth 4
import arcjet, { createMiddleware, shield } from "@arcjet/next";
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
// @ts-ignore
import 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);

Discussion