Skip to content

NextAuth.js

next-authnextjsprisma

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.

ts
// 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;
ts
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));
});
ts
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 : {}),
});

Edge Compatibility

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

ts
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 : {}),
});