Skip to main content
Technology & EngineeringNextjs332 lines

Authentication

Authentication patterns using NextAuth.js (Auth.js) and Clerk for protecting routes and managing sessions in Next.js

Quick Summary30 lines
You are an expert in implementing authentication for Next.js applications using NextAuth.js (Auth.js v5) and Clerk.

## Key Points

- Protect routes at the Middleware layer for a single point of enforcement rather than checking auth in every page.
- Use JWTs (`session: { strategy: "jwt" }`) with Auth.js for edge-compatible Middleware — database sessions require a Node.js runtime.
- Always verify the session in Server Actions and Route Handlers, even if Middleware protects the route.
- Store only essential data (user ID, role) in the JWT/session token. Fetch full profiles on demand.
- Use Clerk if you want managed infrastructure, pre-built UI, and features like multi-factor auth and organizations out of the box.
- Use Auth.js if you need full control, self-hosting, or support for custom credential flows.
- **Not protecting Server Actions**: Middleware only guards page/API requests. Server Actions are POST requests to the same URL and need explicit auth checks.
- **Credentials provider without adapter limitations**: Auth.js Credentials provider does not persist sessions by default. You must use JWT strategy and handle user creation manually.
- **Session not available in Middleware**: Auth.js wraps Middleware via `auth()`. Make sure you export `default auth(...)` not a standalone `middleware()` function.
- **Clerk environment variable naming**: Clerk requires `NEXT_PUBLIC_` prefix for the publishable key. Missing it causes hydration errors.
- **Redirect loops**: If your sign-in page is protected by Middleware, unauthenticated users loop between the page and Middleware. Always exclude auth pages from the matcher.

## Quick Example

```bash
npm install next-auth@beta
npx auth secret  # generates AUTH_SECRET
```

```tsx
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
```
skilldb get nextjs-skills/AuthenticationFull skill: 332 lines
Paste into your CLAUDE.md or agent config

Authentication — Next.js

You are an expert in implementing authentication for Next.js applications using NextAuth.js (Auth.js v5) and Clerk.

Overview

Next.js does not include built-in authentication. The two most popular solutions are Auth.js (the successor to NextAuth.js) for self-hosted, flexible auth, and Clerk for a managed, drop-in auth platform. Both integrate deeply with the App Router, Server Components, Middleware, and Server Actions.

Core Philosophy

Authentication in Next.js should be layered, not singular. The Middleware layer acts as the first gate, redirecting unauthenticated users before a route even begins to render. But Middleware alone is not enough — Server Actions, Route Handlers, and Server Components must each independently verify the session because they represent distinct attack surfaces. Defense in depth means every layer assumes the others might be bypassed.

The choice between Auth.js and Clerk reflects a broader architectural decision: control versus convenience. Auth.js gives you full ownership of the authentication flow, the database schema, and the session strategy, which matters when you have custom requirements like multi-tenant isolation, unusual OAuth providers, or regulatory constraints around data residency. Clerk abstracts all of that away in exchange for a managed service that handles MFA, user management UI, and organization switching out of the box. Neither is universally better; the right choice depends on your operational capacity and product requirements.

Session data should be minimal and read-heavy. Store only the user ID and role in the JWT or session token, and fetch the full profile on demand when a page needs it. Stuffing large objects into the session bloats every request, creates stale-data bugs, and increases the surface area for token-based attacks. Treat the session as an identity claim, not a data cache.

Core Concepts

Auth.js (NextAuth.js v5)

Auth.js provides a provider-agnostic authentication framework supporting OAuth, email/password, magic links, and credentials-based auth.

Clerk

Clerk is a managed authentication and user management service that provides pre-built UI components, webhook-based sync, and multi-tenancy support with minimal configuration.

Implementation Patterns

Auth.js Setup

Installation and configuration:

npm install next-auth@beta
npx auth secret  # generates AUTH_SECRET
// auth.ts (project root)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub,
    Google,
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user?.hashedPassword) return null;
        const valid = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        );
        return valid ? user : null;
      },
    }),
  ],
  pages: {
    signIn: "/login",
    error: "/auth/error",
  },
  callbacks: {
    async session({ session, token }) {
      if (token.sub) session.user.id = token.sub;
      if (token.role) session.user.role = token.role as string;
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role;
      }
      return token;
    },
  },
  session: { strategy: "jwt" },
});

Route handler:

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

Using the session in Server Components:

// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect("/login");

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Role: {session.user.role}</p>
    </div>
  );
}

Sign-in and sign-out with Server Actions:

// app/login/page.tsx
import { signIn } from "@/auth";

export default function LoginPage() {
  return (
    <div>
      <form
        action={async () => {
          "use server";
          await signIn("github");
        }}
      >
        <button type="submit">Sign in with GitHub</button>
      </form>

      <form
        action={async (formData) => {
          "use server";
          await signIn("credentials", {
            email: formData.get("email"),
            password: formData.get("password"),
            redirectTo: "/dashboard",
          });
        }}
      >
        <input name="email" type="email" required />
        <input name="password" type="password" required />
        <button type="submit">Sign in</button>
      </form>
    </div>
  );
}

Middleware protection:

// middleware.ts
import { auth } from "@/auth";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");

  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL("/login", req.nextUrl));
  }
});

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Clerk Setup

Installation:

npm install @clerk/nextjs

Environment variables:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

Provider setup:

// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

Middleware protection:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isProtectedRoute = createRouteMatcher([
  "/dashboard(.*)",
  "/api/private(.*)",
]);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Pre-built UI components:

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";

export default function SignInPage() {
  return (
    <div className="flex justify-center py-24">
      <SignIn />
    </div>
  );
}

Accessing user data in Server Components:

import { currentUser, auth } from "@clerk/nextjs/server";

export default async function DashboardPage() {
  const user = await currentUser();
  if (!user) redirect("/sign-in");

  return <h1>Hello, {user.firstName}</h1>;
}

Accessing user data in Client Components:

"use client";
import { useUser } from "@clerk/nextjs";

export function UserGreeting() {
  const { isLoaded, isSignedIn, user } = useUser();
  if (!isLoaded) return <Skeleton />;
  if (!isSignedIn) return null;
  return <p>Hello, {user.firstName}</p>;
}

Protecting Server Actions

"use server";
import { auth } from "@/auth"; // Auth.js
// or: import { auth } from "@clerk/nextjs/server"; // Clerk

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error("Unauthorized");

  await db.post.create({
    data: {
      title: formData.get("title") as string,
      authorId: session.user.id,
    },
  });
}

Best Practices

  • Protect routes at the Middleware layer for a single point of enforcement rather than checking auth in every page.
  • Use JWTs (session: { strategy: "jwt" }) with Auth.js for edge-compatible Middleware — database sessions require a Node.js runtime.
  • Always verify the session in Server Actions and Route Handlers, even if Middleware protects the route.
  • Store only essential data (user ID, role) in the JWT/session token. Fetch full profiles on demand.
  • Use Clerk if you want managed infrastructure, pre-built UI, and features like multi-factor auth and organizations out of the box.
  • Use Auth.js if you need full control, self-hosting, or support for custom credential flows.

Common Pitfalls

  • Not protecting Server Actions: Middleware only guards page/API requests. Server Actions are POST requests to the same URL and need explicit auth checks.
  • Credentials provider without adapter limitations: Auth.js Credentials provider does not persist sessions by default. You must use JWT strategy and handle user creation manually.
  • Session not available in Middleware: Auth.js wraps Middleware via auth(). Make sure you export default auth(...) not a standalone middleware() function.
  • Clerk environment variable naming: Clerk requires NEXT_PUBLIC_ prefix for the publishable key. Missing it causes hydration errors.
  • Redirect loops: If your sign-in page is protected by Middleware, unauthenticated users loop between the page and Middleware. Always exclude auth pages from the matcher.

Anti-Patterns

  • Relying solely on Middleware for auth enforcement. Middleware protects page navigations, but Server Actions are POST requests to the same URL and bypass the Middleware matcher. Every Server Action and Route Handler must independently call auth() and verify the session before proceeding.

  • Storing sensitive data in the JWT payload. JWTs are signed but not encrypted by default. Putting email addresses, permissions lists, or PII in the token exposes them to anyone who intercepts the cookie or inspects localStorage. Keep the payload to a user ID and role, and fetch the rest server-side.

  • Rolling your own password hashing or session management. Cryptography is easy to get subtly wrong. Use bcryptjs or argon2 for hashing, and let Auth.js or Clerk manage session tokens, CSRF protection, and cookie security. Custom implementations almost always have timing attacks, weak entropy, or missing rotation logic.

  • Checking auth in every page component instead of centralizing it. Duplicating if (!session) redirect("/login") across dozens of pages is fragile and easy to forget. Centralize route protection in Middleware with a matcher pattern, and use per-component checks only for authorization (role/permission) logic that varies by page.

  • Mixing Auth.js and Clerk in the same project. Both libraries expect to own the Middleware export, the session API, and the auth context. Combining them creates conflicting cookie names, confusing provider resolution, and double-authentication overhead. Pick one and commit to it.

Install this skill directly: skilldb add nextjs-skills

Get CLI access →