Authentication
Authentication patterns using NextAuth.js (Auth.js) and Clerk for protecting routes and managing sessions in Next.js
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 linesAuthentication — 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 exportdefault auth(...)not a standalonemiddleware()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
bcryptjsorargon2for 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
Related Skills
API Routes
Route Handlers for building REST APIs, handling webhooks, streaming responses, and CORS in Next.js App Router
App Router
App Router fundamentals including file-based routing, layouts, loading states, and parallel routes in Next.js
Data Fetching
Data fetching and caching strategies including Server Components, fetch options, ISR, and the Next.js cache layers
Deployment
Deployment strategies for Next.js including Vercel, self-hosting with Node.js, Docker containers, and static export
Image Optimization
Image optimization with next/image, asset handling, fonts, and performance tuning for Next.js applications
Middleware
Middleware patterns for request interception, redirects, rewrites, authentication guards, and geo-routing in Next.js