React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

authnextauthauthenticationsessionoauthmiddlewareproviders

NextAuth.js / Auth.js v5 - Authentication for Next.js App Router with providers, sessions, and middleware

Recipe

npm install next-auth@beta
// auth.ts (root of project)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID!,
      clientSecret: process.env.AUTH_GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
    }),
  ],
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
 
export const { GET, POST } = handlers;
// .env.local
AUTH_SECRET=your-random-secret-at-least-32-chars
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
AUTH_GOOGLE_ID=...
AUTH_GOOGLE_SECRET=...

When to reach for this: You need OAuth login (GitHub, Google, etc.), session management, and route protection in a Next.js App Router application.

Working Example

// app/components/AuthButtons.tsx
import { auth, signIn, signOut } from "@/auth";
 
export async function SignInButton() {
  const session = await auth();
 
  if (session?.user) {
    return (
      <div className="flex items-center gap-3">
        {session.user.image && (
          <img
            src={session.user.image}
            alt=""
            className="w-8 h-8 rounded-full"
          />
        )}
        <span className="text-sm">{session.user.name}</span>
        <form
          action={async () => {
            "use server";
            await signOut();
          }}
        >
          <button className="text-sm text-gray-500 hover:text-gray-700">
            Sign Out
          </button>
        </form>
      </div>
    );
  }
 
  return (
    <div className="flex gap-2">
      <form
        action={async () => {
          "use server";
          await signIn("github");
        }}
      >
        <button className="bg-gray-900 text-white px-4 py-2 rounded text-sm">
          Sign in with GitHub
        </button>
      </form>
      <form
        action={async () => {
          "use server";
          await signIn("google");
        }}
      >
        <button className="bg-blue-600 text-white px-4 py-2 rounded text-sm">
          Sign in with Google
        </button>
      </form>
    </div>
  );
}
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const session = await auth();
 
  if (!session) {
    redirect("/api/auth/signin");
  }
 
  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <p className="mt-2">Welcome, {session.user?.name}!</p>
      <pre className="mt-4 bg-gray-100 p-4 rounded text-sm">
        {JSON.stringify(session, null, 2)}
      </pre>
    </div>
  );
}
// middleware.ts
import { auth } from "./auth";
 
export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
 
  if (isProtected && !isLoggedIn) {
    return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
  }
});
 
export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

What this demonstrates:

  • Auth.js v5 setup with GitHub and Google providers
  • Server Component session access with auth()
  • Server Action sign in/out forms
  • Protected pages with redirect
  • Middleware-based route protection

Deep Dive

How It Works

  • Auth.js v5 exports auth(), signIn(), signOut(), and handlers from a single configuration
  • auth() returns the current session in Server Components, Server Actions, API routes, and middleware
  • Sessions are JWT-based by default (no database required); add a database adapter for persistent sessions
  • The handlers export provides GET and POST route handlers for the /api/auth/[...nextauth] catch-all route
  • OAuth flow: user clicks sign in, redirects to provider, provider redirects back with code, Auth.js exchanges code for tokens and creates a session
  • AUTH_SECRET is required in production for signing JWTs; generate with npx auth secret

Variations

Credentials provider (email/password):

import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        const parsed = z
          .object({
            email: z.string().email(),
            password: z.string().min(8),
          })
          .safeParse(credentials);
 
        if (!parsed.success) return null;
 
        const user = await prisma.user.findUnique({
          where: { email: parsed.data.email },
        });
 
        if (!user || !user.password) return null;
 
        const valid = await bcrypt.compare(parsed.data.password, user.password);
        if (!valid) return null;
 
        return { id: String(user.id), name: user.name, email: user.email };
      },
    }),
  ],
});

Prisma database adapter:

npm install @auth/prisma-adapter
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [GitHub, Google],
  session: { strategy: "database" }, // Use database sessions instead of JWT
});

Extending the session with custom fields:

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role; // Add role from database user
      }
      return token;
    },
    async session({ session, token }) {
      session.user.id = token.sub!;
      session.user.role = token.role as string;
      return session;
    },
  },
});
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession["user"];
  }
}

Client-side session access:

// app/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
 
export function AuthProvider({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}
 
// app/components/ClientProfile.tsx
"use client";
import { useSession } from "next-auth/react";
 
export function ClientProfile() {
  const { data: session, status } = useSession();
 
  if (status === "loading") return <p>Loading...</p>;
  if (!session) return <p>Not signed in</p>;
 
  return <p>Signed in as {session.user?.name}</p>;
}

TypeScript Notes

  • Extend the Session and JWT types via module augmentation in types/next-auth.d.ts
  • auth() returns Session | null; always check for null before accessing session.user
  • Provider-specific profile types are available but rarely needed
  • The User type can be extended for custom database fields
import type { Session } from "next-auth";
 
async function getUser(): Promise<Session["user"] | null> {
  const session = await auth();
  return session?.user ?? null;
}

Gotchas

  • AUTH_SECRET missing — App crashes in production without AUTH_SECRET. Fix: Generate it with npx auth secret and add to your environment variables. Required for JWT signing.

  • Callback URL mismatch — OAuth providers reject redirects if the callback URL doesn't match. Fix: Register https://yourdomain.com/api/auth/callback/github (and /google) in each provider's developer settings.

  • Session null in client componentsauth() only works server-side. Fix: Use SessionProvider and useSession() hook for client components. Wrap your layout with <SessionProvider>.

  • Middleware runs on every request — Without a matcher, auth middleware runs on static assets, API routes, etc. Fix: Always configure config.matcher to limit middleware to protected paths.

  • Credentials provider limitations — Credentials provider does not support database sessions out of the box (JWT only). Fix: Use JWT strategy with credentials. For database sessions, use OAuth providers with a database adapter.

  • Edge runtime compatibility — Some database adapters and bcrypt don't work in Edge runtime. Fix: Use bcryptjs instead of bcrypt. Check adapter compatibility with Edge. Use runtime: "nodejs" in middleware if needed.

  • v4 to v5 migration — Auth.js v5 has a significantly different API from NextAuth v4. Fix: Follow the official migration guide. Key changes: NextAuth() returns auth instead of using getServerSession(), and next-auth/react is only for client components.

Alternatives

LibraryBest ForTrade-off
Auth.js / NextAuth v5Full OAuth + session management for Next.jsComplex setup for custom flows
ClerkDrop-in auth UI componentsPaid service, vendor lock-in
Supabase AuthSupabase ecosystem integrationTied to Supabase
LuciaLightweight, DIY authMore manual setup, less abstraction
KindeB2B auth with organizationsPaid service
Custom JWTFull controlMust handle security, refresh, revocation yourself

FAQs

What does the auth() function return and where can I call it?
  • Returns Session | null containing user info and expiry
  • Can be called in Server Components, Server Actions, API routes, and middleware
  • Cannot be called in client components -- use useSession() from next-auth/react instead
  • Always check for null before accessing session.user
How does the OAuth flow work with Auth.js v5 and Next.js?
  1. User clicks sign-in, form action calls signIn("github")
  2. Next.js redirects to the OAuth provider's authorization page
  3. User grants permission, provider redirects back with a code
  4. Auth.js exchanges the code for tokens and creates a JWT session
  5. The session cookie is set and subsequent auth() calls return the session
Gotcha: Why does my app crash in production with "AUTH_SECRET is missing"?
  • AUTH_SECRET is required for signing JWTs in production
  • Generate it with npx auth secret and add to your environment variables
  • It must be at least 32 characters
  • In development, Auth.js generates a temporary secret automatically
How do I protect routes with middleware?
// middleware.ts
import { auth } from "./auth";
 
export default auth((req) => {
  if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
    return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
  }
});
 
export const config = {
  matcher: ["/dashboard/:path*"],
};

Always set config.matcher to limit middleware to protected paths.

How do I access the session in a client component?
  • Wrap your layout with <SessionProvider> from next-auth/react
  • Use the useSession() hook in client components
  • useSession() returns { data: session, status } where status is "loading", "authenticated", or "unauthenticated"
How do I add a Credentials provider for email/password login?
  • Import Credentials from next-auth/providers/credentials
  • Define credentials fields and an authorize function
  • The authorize function validates credentials and returns a user object or null
  • Credentials provider only supports JWT sessions, not database sessions
How do I extend the session with custom fields like role?
callbacks: {
  async jwt({ token, user }) {
    if (user) token.role = user.role;
    return token;
  },
  async session({ session, token }) {
    session.user.role = token.role as string;
    return session;
  },
}

Also extend the Session type via module augmentation in types/next-auth.d.ts.

How do I type-extend the Session interface in TypeScript?
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession["user"];
  }
}

This adds id and role to session.user throughout your app.

Gotcha: Why does auth() return null in my client component?
  • auth() is a server-only function and cannot be called in client components
  • Use <SessionProvider> and useSession() hook for client-side session access
  • Make sure SessionProvider wraps the component tree in your layout
How do I set up a Prisma database adapter for persistent sessions?
import { PrismaAdapter } from "@auth/prisma-adapter";
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: "database" },
  providers: [GitHub, Google],
});

Install @auth/prisma-adapter and set session.strategy to "database".

Gotcha: Why does my middleware run on every request including static assets?
  • Without a matcher config, middleware runs on all routes including _next/static, images, and API routes
  • Always configure config.matcher to limit it to protected paths
  • Example: matcher: ["/dashboard/:path*", "/settings/:path*"]
What are the key differences between NextAuth v4 and Auth.js v5?
  • v5 exports auth() instead of requiring getServerSession(authOptions)
  • v5 uses a single NextAuth() call that returns auth, signIn, signOut, and handlers
  • next-auth/react (useSession, SessionProvider) is only for client components
  • The configuration file is typically auth.ts at the project root