NextAuth.js
![]() | ![]() | ![]() |
This template uses JWT (strategy) to manage session persistence. Since it leverages the Prisma adapter, users also have the option to manage sessions in the database. Each approach has its own pros and cons, but the default choice is set to JWT.
IMPORTANT
Recently, Prisma has been adapted to run on Edge. However, database drivers and database types / hosting providers are required. See more details
Why Choose JWT?
Authentication in Next.js is typically handled via middleware, which operates in the Edge runtime. Adapters, including Prisma, need to be compatible with the Edge runtime, and platform-specific support is often required. By using JWT, these compatibility concerns are eliminated, simplifying the implementation and avoiding potential runtime issues.
However, as you may know, managing sessions with JWT comes with challenges such as difficulty in purging tokens and syncing claims. For these reasons, I personally recommend managing sessions in a database whenever possible.
Split Config Best Practice
When using middleware to call the Prisma adapter, it will result in errors due to the need for drivers and other dependencies, as mentioned above. However, this method can still be utilized in cases where you want to continue storing user information and other data in the database using the Prisma adapter.
This approach(lazy initialization
) involves a split strategy where the adapter is excluded from the options passed to the middleware, but included in the main application code.
// don't import prisma or @auth/prisma-adapter here to avoid importing it at Edge
// [auth][error] JWTSessionError: Read more at https://errors.authjs.dev#jwtsessionerror
// [auth][cause]: Error: PrismaClient is not configured to run in Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware). In order to run Prisma Client on edge runtime, either:
import type { NextAuthConfig } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const configForTest = {
jwt: {
encode: async ({ token }) => {
return btoa(JSON.stringify(token));
},
decode: async ({ token }) => {
if (!token) {
return {};
}
return JSON.parse(atob(token));
},
},
} satisfies Omit<NextAuthConfig, "providers">;
export const config = {
pages: {
signIn: "/signin",
},
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
// https://authjs.dev/guides/role-based-access-control#persisting-the-role
role: profile.role ?? "USER",
};
},
}),
],
callbacks: {
redirect: ({ url, baseUrl }) => {
return baseUrl;
},
session: ({ session, token }) => {
if (session?.user && token.user.id) {
session.user.id = token.user.id;
session.user.name = token.user.name;
session.user.email = token.user.email ?? "";
session.user.image = token.user.image;
session.user.role = token.user.role;
}
return session;
},
},
// https://authjs.dev/getting-started/deployment#docker
trustHost: true,
...(process.env.NEXTAUTH_TEST_MODE === "true" ? configForTest : {}),
} satisfies NextAuthConfig;
import NextAuth from "next-auth";
import { NextResponse } from "next/server";
import { config as authConfig } from "./app/_clients/nextAuthConfig";
const { auth } = NextAuth(authConfig);
export const config = { matcher: ["/me(.*)"] };
export default auth(async function middleware(req) {
if (req.auth?.user.role === "USER") {
return NextResponse.next();
}
return NextResponse.rewrite(new URL("/signin", req.url));
});
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import type { Adapter } from "next-auth/adapters";
import type { JWT } from "next-auth/jwt";
import { config, configForTest } from "./nextAuthConfig";
import { prisma } from "./prisma";
export const { auth, handlers } = NextAuth({
// https://authjs.dev/getting-started/migrating-to-v5#edge-compatibility
...config,
adapter: <Adapter>PrismaAdapter(prisma),
session: {
strategy: "jwt",
},
callbacks: {
...config.callbacks,
jwt: async ({
token,
/* user exists when only signing in */ user,
trigger,
}): Promise<JWT> => {
const userId = token.sub ?? user?.id;
if (!userId) {
throw new Error("Token is invalid");
}
// support for multiple devices
const me = await prisma.user.findUnique({
where: {
id: userId,
},
});
// to avoid accessing devices having invalid JWT if user is not found
if (trigger !== "signUp" && !user && !me) {
throw new Error("User not found");
}
// in favor of the user's latest data and update the token
token.user = {
id: userId,
name: me?.name ?? token.name,
email: me?.email ?? token.email,
role: me?.role ?? user.role,
image: me?.image ?? token.picture,
};
return token;
},
},
...(process.env.NEXTAUTH_TEST_MODE === "true" ? configForTest : {}),
});
Keeping User Information Up-to-Date
As mentioned earlier, managing multiple devices with JWT can be challenging. In this template, user information is verified against the User table on every request to ensure it is always up-to-date. This approach helps prevent issues such as users accessing the service from another device even after their account has been deleted.
NOTE
auth()
only returns the information contained within the claims
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import type { Adapter } from "next-auth/adapters";
import type { JWT } from "next-auth/jwt";
import { config, configForTest } from "./nextAuthConfig";
import { prisma } from "./prisma";
export const { auth, handlers } = NextAuth({
// https://authjs.dev/getting-started/migrating-to-v5#edge-compatibility
...config,
adapter: <Adapter>PrismaAdapter(prisma),
session: {
strategy: "jwt",
},
callbacks: {
...config.callbacks,
jwt: async ({
token,
/* user exists when only signing in */ user,
trigger,
}): Promise<JWT> => {
const userId = token.sub ?? user?.id;
if (!userId) {
throw new Error("Token is invalid");
}
// support for multiple devices
const me = await prisma.user.findUnique({
where: {
id: userId,
},
});
// to avoid accessing devices having invalid JWT if user is not found
if (trigger !== "signUp" && !user && !me) {
throw new Error("User not found");
}
// in favor of the user's latest data and update the token
token.user = {
id: userId,
name: me?.name ?? token.name,
email: me?.email ?? token.email,
role: me?.role ?? user.role,
image: me?.image ?? token.picture,
};
return token;
},
},
...(process.env.NEXTAUTH_TEST_MODE === "true" ? configForTest : {}),
});