Next Auth
Last modified on Wed 12 Feb 2025

What is NextAuth?

NextAuth is an open-source authentication library designed specifically for Next.js applications. It streamlines adding authentication to your projects by providing a robust, flexible, and secure solution that integrates directly with Next.js API routes and pages. With built-in support for popular OAuth providers, email/passwordless login, and custom credential systems, NextAuth makes it easier to implement Single Sign-On (SSO) and other authentication patterns without reinventing the wheel.

It simplifies authentication by handling session management, token issuance, and secure communication between your client and server. It leverages Next.js’s file-based API routing to create endpoints (e.g., /api/auth/[...nextauth]) that manage sign-in, sign-out, and session verification automatically.

How it works

Use cases

NextAuth can be used in various scenarios:

Pros and Cons

Pros

Cons

Basic Setup

Installation

Install NextAuth and any necessary provider packages:

pnpm add -E next-auth

If you’re using a specific provider (e.g., Google), make sure to install any required packages as outlined in the NextAuth Providers documentation.

Secret Key

In order to automatically generate an example AUTH_SECRET in your .env.local file, you can run:

pnpx auth secret

⚠️ Warning! For Production environments make sure you're using ASYMMETRIC RS256 3072-bit key - you can read more about Security of JWT here. If you want to hide the data inside token, use JWE.

Configuration

Create configuration file at /lib/auth.ts - include which providers you want to use and any session settings.

import {
  getServerSession as getNextAuthServerSession,
  NextAuthOptions,
} from "next-auth";
import GitHubProvider from "next-auth/providers/github";
// ...import any other providers or config as needed

export const authOptions: NextAuthOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID || "",
      clientSecret: process.env.GITHUB_SECRET || "",
    }),
    // ...add more providers here
  ],
  // You can choose between JWT or DB sessions
  session: { strategy: "jwt" }, // (default strategy)

  // callbacks let you customize or enrich the session
  callbacks: {
    async session({ session, token, user }) {
      // e.g. attach a "role" to the session
    //   session.user.role = user.role;
      return session;
    },
  },
  // ...more NextAuth configuration
};

// For better reusability, encapsulate the session logic in a separate hook
export const getServerSession = () => getNextAuthServerSession(authOptions);

Authentication endpoint

Create a route handler at /app/api/auth/[...nextauth]/route.ts:

import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

Using NextAuth in components

Client components

You can consume the session in your client components using next-auth/react hooks.

"use client";

import { useSession, signIn, signOut } from "next-auth/react";

export default function HomePage() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <p>Loading...</p>;
  }

  if (session) {
    return (
      <div>
        <p>Signed in as {session.user?.email}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </div>
    );
  }

  return (
    <div>
      <p>You are not signed in.</p>
      <button onClick={() => signIn("github")}>Sign in with GitHub</button>
    </div>
  );
}

Server Components

In Server Components (or server-side logic), you can use getServerSession from next-auth to ensure data is fetched only for authenticated users.

import { getServerSession } from "@/lib/auth";

export default async function DashboardPage() {
  const session = await getServerSession();

  if (!session) {
    // You can redirect or throw an error
    return <div>Please sign in to access your dashboard.</div>;
  }

  return <div>Welcome to the dashboard, {session.user?.name}!</div>;
}

Best practices & gotchas

  1. Use HTTPS Everywhere
    • Always serve your app over HTTPS to ensure secure cookie transmission.
  2. Secure Cookies
    • By default, NextAuth sets httpOnly, sameSite cookies. Keep these settings to limit XSS/CSRF attack vectors.
  3. Token Rotation
    • If you enable JWT sessions, consider token rotation or refresh tokens for better security.
  4. Custom Callbacks
    • Enrich the session object with user roles or data.
    • Map external provider data to your custom user fields.
    • Handle token rotation or advanced encryption logic.
  5. Stay Up to Date
    • NextAuth is actively developed. Watch the changelog for new features and security updates.

Gotchas

  1. Route Handlers vs. Pages
    • If you used older patterns (e.g., /pages/api/auth/\[...nextauth].ts), switch to App Router route handlers.
  2. Database Requirements
    • If you use a database session strategy, make sure the schema is set up (NextAuth can generate it for certain DBs).
  3. Provider Rate Limits
    • Social providers might rate-limit logins if you do lots of short-interval sign-ins.
  4. CSRF and Custom Forms
    • Credential-based sign-ins require anti-CSRF tokens, which NextAuth handles automatically, but be cautious if you implement fully custom forms.
  5. Deploying on Serverless
    • NextAuth works on platforms like Vercel seamlessly, but if you use custom serverless hosts, check for any environment-specific limitations.

NextAuth + CASL

CASL is a popular library for Role/Permission-based Access Control. It defines “abilities” that specify what a user can or cannot do in your application. Typically, you’d combine your authentication solution (who is the user? are they logged in?) with an authorization layer (what is the user allowed to do?).

You can combine CASL and NextAuth together, because they serve different purposes:

Configuring CASL with NextAuth

Install dependencies

pnpm add -E @casl/ability @casl/react

Add roles to the session

In NextAuth, use the callbacks.session function to add user roles or permissions into the session object. For example:

async session({ session, user }) {
  session.user.role = user.role; 
  return session;
},

Define abilities with CASL

// lib/casl.ts
import { AbilityBuilder, Ability } from "@casl/ability";

export default function defineAbilitiesFor(user) {
  const { can, cannot, build } = new AbilityBuilder(Ability);

  if (user.role === "admin") {
    can("manage", "all");
  } else {
    can("read", "Post");
    can("delete", "Post", { authorId: user.id }); // Only allow deleting own posts
    cannot("delete", "Post").unless({ authorId: user.id }); // Prevent deleting others' posts
    // etc.
  }

  return build();
}

Use the session

When a user logs in via NextAuth, call defineAbilitiesFor(session.user) to create a CASL “ability” instance. Then check permissions in your components, API routes, or server logic.

Example real-world configuration:

Remember to add the [...nextauth] route handler before the rest of configuration files!

CASL config:

// lib/casl.ts
import { AbilityBuilder, PureAbility } from "@casl/ability";

type Actions = "manage" | "create" | "read" | "update" | "delete";
type Subjects = "Users" | "Posts" | "all"; // Example domain models

export type AppAbility = PureAbility<[Actions, Subjects]>;

export function defineAbilitiesFor(params: { role: string; userId: string }) {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(PureAbility);

  const { role } = params;

  if (role === "admin") {
    // Admin can do everything
    can("manage", "all");
  } else if (role === "editor") {
    can("read", "Posts");
    can("create", "Posts");
    can("update", "Posts");
    cannot("delete", "Posts");
  } else {
    // role === "user"
    can("read", "Posts");
    cannot("create", "Posts");
    cannot("delete", "Posts");
    // etc.
  }

  return build();
}

NextAuth config:

// lib/auth.ts
import {
  getServerSession as getNextAuthServerSession,
  NextAuthOptions,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

const MOCK_USER = {
  id: "1",
  name: "John Doe",
  email: "john@example.com",
  hashedPassword: "hashedPassword",
  role: "user",
};

const findUserByEmail = async (_email: string) => {
  return MOCK_USER;
};
const verifyPassword = async (_password: string, _hashedPassword: string) => {
  return true;
};

export const authOptions: NextAuthOptions = {
  session: {
    strategy: "jwt",
  },
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials, req) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error("Missing username or password");
        }

        const user = await findUserByEmail(credentials.email);
        if (!user) {
          // Remember to never leak if there's actually a user with given email, always show the end-user "Invalid password" error!
          throw new Error("User not found");
        }

        const isValid = await verifyPassword(
          credentials.password,
          user.hashedPassword
        );
        if (!isValid) {
          throw new Error("Invalid password");
        }

        // Return a "safe" user object. NextAuth will store this in JWT token
        return {
          id: user.id,
          name: user.name,
          email: user.email,
          // User role used by CASL
          role: user.role,
        };
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      // If `user` is defined, it means we're in the process of the user signing in
      if (user) {
        token.user = user;
      }
      return token;
    },
    async session({ session, token }) {
      // Add the user role to the session for CASL
      if (session.user && token) {
        session.user = token.user;
      }
      return session;
    },
  },
  // Optionally, add pages if you want custom error or signIn pages
  // pages: {
  //   signIn: '/login',
  //   error: '/login?error=CredentialsSignin', // example
  // },
};

// For better reusability, encapsulate the session logic in a separate hook
export const getServerSession = () => getNextAuthServerSession(authOptions);

Typescript Module Augmentation:

Create src/typings/next-auth.d.ts file:

/// <reference types="next-auth" />

import type { DefaultSession, DefaultUser } from "next-auth";
import type { DefaultJWT } from "next-auth/jwt";

type AppUser = DefaultUser & {
  id: string;
  name: string;
  email: string;
  role: string;
};

declare module "next-auth" {
  interface User extends AppUser {
    // Strictly override the base type "string | null | undefined" with "string"
    name: string;
    email: string;
  }

  interface Session extends DefaultSession {
    user: AppUser;
  }
}

declare module "next-auth/jwt" {
  interface JWT extends DefaultJWT {
    user: AppUser;
  }
}

Example server component:

import { getServerSession } from "@/lib/auth";
import { defineAbilitiesFor } from "@/lib/casl";

export default async function DashboardPage() {
  const session = await getServerSession();

  if (!session?.user) {
    return <div>Please sign in first.</div>;
  }

  const ability = defineAbilitiesFor({
    role: session.user.role,
    userId: session.user.id,
  });

  // Example usage
  const canCreatePost = ability.can("create", "Posts");

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Your role: {session.user.role}</p>

      {canCreatePost ? (
        <div>Show "Create Post" button or form here.</div>
      ) : (
        <p>You do not have permission to create posts.</p>
      )}
    </div>
  );
}

Example client component:

"use client";

import { useSession, signIn, signOut } from "next-auth/react";
import { useState } from "react";

export default function LoginPage() {
  const { data: session, status } = useSession();
  const [credentials, setCredentials] = useState({ email: "", password: "" });

  if (status === "loading") {
    return <p>Loading...</p>;
  }

  if (session) {
    return (
      <div>
        <p>Signed in as {session.user.email}</p>
        <p>Your role is: {session.user.role}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </div>
    );
  }

  return (
    <form>
      <label>
        Email
        <input
          type="email"
          value={credentials.email}
          onChange={(e) =>
            setCredentials({ ...credentials, email: e.target.value })
          }
        />
      </label>
      <label>
        Password
        <input
          type="password"
          value={credentials.password}
          onChange={(e) =>
            setCredentials({ ...credentials, password: e.target.value })
          }
        />
      </label>
      <button
        type="submit"
        onClick={(e) => {
          e.preventDefault();
          signIn("credentials", {
            email: credentials.email,
            password: credentials.password,
          });
        }}
      >
        Sign in
      </button>
    </form>
  );
}

Example route handler:

// app/api/protected-resource/route.ts
import { NextResponse } from "next/server";
import { getServerSession } from "@/lib/auth";
import { defineAbilitiesFor } from "@/lib/casl";

export async function GET() {
  const session = await getServerSession();

  if (!session?.user) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  const ability = defineAbilitiesFor({
    role: session.user.role,
    userId: session.user.id,
  });

  // For example, we check if the user can "read" a "Post"
  if (!ability.can("read", "Posts")) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  // Otherwise, proceed with returning the resource
  return NextResponse.json({ message: "Here is the protected data" });
}