
Next.js Authentication Guide 2026
Part 1 covers what production App Router authentication requires, defense in depth, integrating Clerk, the essential Dashboard configuration, and the common Proxy and session pitfalls.
Next.js authentication is more than a sign-in form. A production App Router application needs a session that works across server and client code, fast route-level checks, and authorization next to every protected read or mutation.
Part 1 explains that architecture and implements it with Clerk and the Clerk Dashboard. Part 2 compares Clerk with Auth0, WorkOS, Auth.js (formerly NextAuth.js), Supabase Auth, Firebase Auth, Amazon Cognito, and Okta.
TL;DR
Next.js authentication means keeping sessions across Server and Client Components, protecting routes and data, and picking an App Router-compatible option. Here's each in a line; Part 2 compares them in full.
- Clerk for Next.js B2B teams that want embedded UI, Organizations, and framework-native server APIs.
- Auth0 for mature customer identity and broad enterprise federation.
- WorkOS for modular SSO, Directory Sync, and enterprise onboarding.
- Auth.js (NextAuth.js) for existing self-hosted installations where the team owns its auth stack.
- Supabase Auth for existing Supabase stacks and Row Level Security.
- Firebase Auth for client-rendered React apps already inside the Firebase and Google Cloud ecosystem.
- Amazon Cognito for AWS-native architectures where IAM integration and consolidated billing matter more than developer experience.
- Okta for enterprises already standardizing workforce or customer identity on Okta.
What Next.js authentication requires
Every production Next.js authentication system needs three technical layers.
Session handling across server and client. Applications need a trustworthy way to identify users after sign-in, usually an encrypted session cookie or short-lived token. Server Components, Route Handlers, Server Actions, and Client Components must all resolve it without exposing secrets.
Route and resource protection. Next.js 16's request-interception layer is Proxy (formerly Middleware). It gives you quick redirects but isn't the final security boundary. Authorization belongs close to protected data, and Server Actions and Route Handlers count as public endpoints.
App Router integration. Server Components read authentication server-side; Client Components use hooks or context. Handle both, plus streaming, cached layouts, route prefetching, and cookie refresh.
Authentication answers "Who is this user?", session management carries this across requests, and authorization answers "What may this user do?". Providers supply primitives; your application owns the final data rules.
The four layers of defense in depth
The Next.js authentication guide treats authorization as a sequence of checks, stating "the majority of security checks should be performed as close as possible to your data source." Read them as layers:
- Proxy. Optimistic checks redirecting signed-out traffic before rendering. No database calls. Next.js labels this layer "Optional": it centralizes redirect logic and is the one place to protect static routes, but the docs warn it "should not be your only line of defense."
- Data Access Layer. A centralized module verifying the session and returning only the data the caller is entitled to. Memoize the session read with React's
cache(). - Server Components. Page-level checks. Layouts are the wrong place: they don't re-render on navigation, so the session goes unchecked on every route change.
- Server Actions and Route Handlers. Per-invocation checks. Any exported
'use server'function your application actually calls is reachable by direct POST, whatever page renders the form.
A Data Access Layer is worth the indirection: one file to audit instead of checks spread across every page.
// lib/dal.ts
import { auth } from '@clerk/nextjs/server'
import { cache } from 'react'
export const getSession = cache(async () => {
const { isAuthenticated, orgId, userId } = await auth()
if (!isAuthenticated) return null
return { orgId, userId }
})
export async function requireUser() {
const session = await getSession()
if (!session) throw new Error('Unauthorized')
return session
}Every page, action, and handler then calls requireUser(), and the cache() wrapper makes repeat calls in one request share a single resolution. The walkthrough below calls auth() directly so each step stands alone.
Why the authentication decision carries security weight
Two things give this decision weight. The framework itself has been the weak link more than once, and credentials stay in the attack path even when they aren't the way in.
CVE-2025-29927 was an authorization bypass in Next.js middleware with a CVSS 3.1 base score of 9.1 (Critical). A crafted x-middleware-subrequest request header let an attacker skip middleware execution entirely, taking every middleware-based authorization check with it. The GitHub Security Advisory lists patched releases at 12.3.5, 13.5.9, 14.2.25, and 15.2.3. Exposure depended on where the application ran: self-hosted deployments were affected, while Vercel called itself "incidentally invulnerable" because its routing runs decoupled from the application, and confirmed Netlify and Cloudflare Workers were never affected for the same reason. That last detail is the durable lesson: authorization enforced only in request-interception code held or failed based on the host, not on anything in the application's own source.
The pattern recurred twice in 2026, both rated High. CVE-2026-45109 followed an incomplete fix for a segment-prefetch bypass, patched in May in 15.5.18 and 16.2.6. The July 2026 security release then patched CVE-2026-64642 in 16.2.11, a bypass hitting App Router builds on Turbopack with a single config.i18n.locales entry. Same shape, same conclusion: a check that only lives in the interception layer is one framework bug away from not running.
Credentials sit underneath a large share of breaches even where they aren't the entry point. The 2026 Verizon Data Breach Investigations Report puts vulnerability exploitation behind nearly a third of breaches (31%), "the first time in 19 years that it has surpassed stolen credentials as the biggest point of entry." Credential abuse has fallen to 13% as an initial access vector, behind phishing at 16% (2026 DBIR, page 15, Figure 10). Part of that fall is a counting change: newly tracked Pretexting absorbed cases that used to count as credential abuse, which would otherwise still read 16%. Counted at any point in a breach rather than only as the opening move, credential abuse "still sits on top at 39%" (page 16).
A few rules follow, whichever provider you pick.
Cookies carry the session, so set them correctly. Session cookies need HttpOnly so client-side scripts can't read them, Secure so they only travel over HTTPS, and SameSite to blunt cross-site request forgery. The __Host- prefix adds one more guarantee: the cookie was set by the exact host receiving it, with no domain scope to widen.
Never store tokens in localStorage. Anything readable by JavaScript is readable by an injected script.
Keep JWT payloads boring. A signed token is readable by anyone who holds it. Signing prevents tampering, not inspection. Put an identifier, roles, and permissions in it. Don't put personal data, secrets, or anything you wouldn't paste into a log.
Know where CSRF protection comes from. Next.js Server Actions are POST-only, lean on SameSite cookies, and abort when the Origin header doesn't match Host. Encrypted, non-deterministic action IDs and a 1MB cap on action bodies add more hardening. The Next.js data security guide still tells you to treat every action as reachable by direct POST and verify authentication and authorization inside each one. Route Handlers get none of that automatically. If you write a state-changing GET or a POST handler that accepts cross-origin requests, CSRF protection is your responsibility.
Build versus buy
Rolling your own authentication is achievable; maintaining it is expensive, and the initial email/password requirement hides that cost.
You're signing up for password reset, breached-password checking, MFA, session revocation, rate limiting, email/SMS deliverability, OAuth upgrades, SAML/OIDC management, SCIM, audit logging, and compliance evidence. Together they're a permanent workstream competing with your roadmap.
Building makes sense if authentication is your product, regulatory constraints demand it, or a dedicated security team owns your identity infrastructure. Otherwise, buy.
Setting up Clerk in a Next.js app
These examples use Next.js 16, TypeScript, and Clerk Core 3, which needs Node.js 20.9+. Check the Next.js floor carefully: the published @clerk/nextjs peer range skips 16.0.0 through 16.0.9, so install 16.0.10 or newer on the 16 line, or 15.2.8 or newer on the 15 line. React is floored inside each minor line, so 19.2 users need 19.2.3 or newer. Those are compatibility floors, not security ones: the releases patched against the proxy bypasses above are 16.2.11 (Active LTS) and 15.5.21 (Maintenance LTS).
1. Create the application and choose sign-in options
Start at Create application in the Clerk Dashboard, then open User & authentication to choose how users identify themselves.
The page is organized by identifier and method, each on its own tab:
- Email: email address as an identifier, verification method, and email code or link settings.
- Phone: phone number as an identifier and SMS verification.
- Username: username requirements and allowed characters.
- Password: password policy, complexity rules, and breach detection.
- User profile: profile fields, custom attributes, and account deletion.
Pick identifiers before writing code. Changing the primary identifier later is a data migration, not a settings change.
2. Copy the API keys
Open API keys and copy the publishable and secret keys into .env.local at the project root:
# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...The publishable key is safe in the browser. The secret key isn't.
npm install @clerk/nextjsOne package covers components, hooks, server helpers, and clerkMiddleware().
4. Add the provider and prebuilt components
Place <ClerkProvider> inside <body> in the root layout, then drop in the prebuilt components:
// app/layout.tsx
import { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ClerkProvider>
<header>
<Show when="signed-out">
<SignInButton />
<SignUpButton />
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</header>
<main>{children}</main>
</ClerkProvider>
</body>
</html>
)
}<SignInButton> redirects to the Account Portal by default, or opens a modal with mode="modal". <UserButton> renders an account-management menu. <Show> replaces <SignedIn>, <SignedOut>, and <Protect>, all removed in Core 3, and its when prop also takes a role, permission, plan, or feature check. It controls visibility, not access.
Clerk's Core 3 upgrade guide makes that placement a requirement, not a preference, and npx @clerk/upgrade moves it for you in an existing application.
Next.js 16 caching adds a related constraint. auth() and currentUser() read headers() internally, so calling either inside a 'use cache' function throws. Call auth() outside the cached function and pass its result in as an argument.
5. Add the Clerk proxy
Create proxy.ts at the project root, or in src/ if you use a source directory:
// proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
// Always run for Clerk-specific frontend API routes
'/__clerk/(.*)',
],
}clerkMiddleware() attaches the session to every matched request, which is what makes auth() resolve further down. Too narrow and routes miss Clerk; too broad and Proxy runs where it isn't needed.
Older guides pair this with createRouteMatcher() to name protected routes here. That helper is deprecated and slated for removal in the next major version of @clerk/nextjs, and Clerk's guidance now matches the Next.js position: protect access in the code that reads or mutates the data. That is steps 7 through 9. Move the policy, keep clerkMiddleware().
For Next.js 16, this file is proxy.ts; Next.js 15 and earlier use middleware.ts.
6. Add sign-in and sign-up routes
Create catch-all routes so Clerk can own the multi-step flows:
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return <SignIn />
}The sign-up route mirrors this with <SignUp /> at app/sign-up/[[...sign-up]]/page.tsx. Tell Clerk where these routes live in Paths, which controls redirects and callbacks. If Dashboard paths and file routes disagree, users land on the Account Portal.
Props handle the common adjustments, such as fallbackRedirectUrl="/onboarding" for where users land afterwards. When a design needs a bespoke form instead, Part 2 drops to the useSignIn() hook against this same backend.
7. Read and protect the session in a Server Component
The server has the request context, so read authentication directly. This page-level check is the real gate, not a backstop for Proxy.
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server'
export default async function DashboardPage() {
const { isAuthenticated, orgId, redirectToSignIn, userId } = await auth()
if (!isAuthenticated) return redirectToSignIn()
return (
<main>
<h1>Protected dashboard</h1>
<p>User: {userId}</p>
<p>Organization: {orgId ?? 'Personal account'}</p>
</main>
)
}Scope data queries with the verified userId or orgId. Client Components read presentation state via useAuth() or useUser(); the server-side check remains the security boundary.
8. Protect Route Handlers
Route Handlers are API endpoints. The same auth() helper works there:
// app/api/projects/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { isAuthenticated, userId } = await auth()
if (!isAuthenticated) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// listProjectsForUser is your own data function, scoped by the verified userId
return NextResponse.json({ projects: await listProjectsForUser(userId) })
}Alternatively, await auth.protect() throws instead of returning. For an unauthenticated caller its behavior depends on the call site: a 404 in a Route Handler, a 401 in a Server Action, and a redirect to sign-in on a page request.
9. Protect Server Actions
Server Actions need their own check:
// app/actions/update-profile.ts
'use server'
import { auth } from '@clerk/nextjs/server'
export async function updateProfile(formData: FormData) {
const { userId } = await auth.protect()
const displayName = formData.get('displayName')
if (typeof displayName !== 'string' || displayName.length > 100) {
throw new Error('Invalid display name')
}
return { displayName, userId }
}Note: auth() requires clerkMiddleware() in proxy.ts; without it the helper has no request context.
Configuring Clerk in the Dashboard
Wiring the SDK gives you a working sign-in. The settings below make it production-ready, and only some need application code.
Social connections (OAuth providers)
Open SSO connections → Social to enable OAuth providers. Development instances can use Clerk's shared credentials; production instances require your own client ID and secret from each provider's console.
No code changes are needed. Prebuilt components pick up enabled providers on the next render.
Enterprise SSO
SSO connections → Enterprise configures SAML and OIDC connections. Clerk's Pro and Business plans each include one Enterprise connection per app; connections 2 through 15 are $75/mo each and tier down above that.
Plan your domain model early. Enterprise buyers expect routing by email domain, so connections and Organizations must align.
Multi-factor authentication
Multi-factor controls available second factors: TOTP, SMS, and backup codes. Enabling a factor allows enrollment; <UserButton> exposes it under Manage account → Security. MFA isn't available on the free Hobby plan; it's included on Pro and above.
Passkeys and WebAuthn
Enable passkeys on the Passkeys tab of User & authentication. Passkeys are WebAuthn credentials scoped to one site's origin, so a lookalike domain can't replay them. Most sync across a user's devices through Apple, Google, or Microsoft, and unlock with a gesture like Touch ID or Windows Hello.
The FIDO Alliance's Passkey Index, an anonymized benchmark across nine member organizations including Amazon, Google, and Microsoft, reports a 93% sign-in success rate for passkeys against 63% for other methods such as social login and MFA, plus up to an 81% cut in login-related help desk incidents in fully passwordless environments. FIDO's State of Passkeys 2026, a separate survey of 1,400 enterprise decision-makers, found that 35% of organizations already rolling out passkeys report fewer helpdesk tickets for password resets. That comes straight off your support queue.
Attack protection and restrictions
Rules covers bot detection and brute-force lockouts. Restrictions controls sign-ups via public/restricted modes, plus an Allowlist and Blocklist for identifiers, domains, or phone numbers.
Block disposable email domains and tighten lockout thresholds before launch.
Session lifetime and multi-session mode
Sessions sets session lifetime, inactivity timeout, and single/multi-session mode. These control the long-lived session, separate from the short-lived tokens described in session handling.
Organizations and roles
Enable Organizations in Organizations → Settings, then define access in Roles & Permissions. Settings cover domain verification, membership rules, and creation permissions.
Once roles and permissions exist, enforce them server-side:
// app/dashboard/settings/page.tsx
import { auth } from '@clerk/nextjs/server'
export default async function SettingsPage() {
const { has, isAuthenticated, orgId, redirectToSignIn } = await auth()
if (!isAuthenticated) return redirectToSignIn()
if (!has({ permission: 'org:settings:manage' })) {
return <p>You do not have permission to manage this workspace.</p>
}
// WorkspaceSettings is your own component
return <WorkspaceSettings orgId={orgId} />
}Use has() to render different views, and await auth.protect({ permission: 'org:settings:manage' }) to return a 404 for unauthorized callers. Server-side has() reads session claims, so it resolves custom permissions but not system ones. Always scope tenant queries by the verified orgId from the session, not the URL or body.
JWT templates
JWT templates add custom claims to session and access tokens, allowing third-party services to verify Clerk tokens directly. Session-token claims are readable server-side without extra lookups.
Custom claims aren't on the default sessionClaims type, so declare their shape in a global declaration file:
// types/globals.d.ts
export {}
declare global {
interface CustomJwtSessionClaims {
metadata?: { plan?: string }
}
}The claim then reads as a typed property on the server:
// lib/plan.ts
import { auth } from '@clerk/nextjs/server'
export async function getPlan() {
const { sessionClaims } = await auth()
return sessionClaims?.metadata?.plan ?? 'free'
}Keep templates small. Claims ride on every request.
Webhooks for database sync
Add an endpoint in Webhooks to receive lifecycle events over Svix-backed infrastructure with signed payloads and retries. This keeps your own database in sync with Clerk.
// app/api/webhooks/clerk/route.ts
import { verifyWebhook } from '@clerk/nextjs/webhooks'
export async function POST(request: Request) {
try {
const event = await verifyWebhook(request)
if (event.type === 'user.created') {
// createUserRecord is your own database write
await createUserRecord({ clerkId: event.data.id })
}
return new Response('Success', { status: 200 })
} catch {
return new Response('Webhook verification failed', { status: 400 })
}
}verifyWebhook validates the signature using CLERK_WEBHOOK_SIGNING_SECRET, blocking unsigned or replayed requests. Webhook delivery is eventually consistent, so handlers must tolerate retries and out-of-order arrival.
Production domains and satellites
Configure your primary domain and DNS records in Domains before launch. To span authentication across applications, use Satellites and Allowed subdomains.
Production instances use separate keys. Pull production keys from API keys and set them in your hosting provider, not in committed files.
Proxy, sessions, and runtime pitfalls
Proxy is an early check, not the authorization layer
In Next.js 16, middleware.ts became proxy.ts. Proxy redirects or rewrites requests, but Next.js warns it isn't a complete authorization solution, a position CVE-2025-29927 made concrete.
Server Functions are handled as POST requests to their route. A matcher excluding a path also skips its Server Functions. Moving a Server Action can silently drop it from Proxy coverage, which is why checks belong inside the function.
Clerk reached the same conclusion when it deprecated createRouteMatcher().
Next.js 16 Proxy uses Node.js
Older guidance assuming edge middleware is stale for Next.js 16: Proxy defaults to the Node.js runtime. The runtime config option isn't available in proxy.ts, and setting it throws an error. Edge-only dependencies must use the deprecated middleware.ts. Avoid database lookups in Proxy, as it runs on every matched route (including prefetches), adding latency.
JWT sessions versus database sessions
This fork sits underneath every provider comparison.
Signed tokens carry claims and verify locally without network round trips, scaling horizontally. The trade-off is revocation: tokens remain valid until expiration, delaying "sign out everywhere".
Database sessions store state server-side, allowing immediate revocation for device management. The trade-off is a database lookup on every authenticated request.
Clerk uses a hybrid model: short-lived signed tokens for fast verification, backed by a longer-lived stateful session for central revocation. The short lifetime bounds the revocation window without per-request lookups.
Keep server and client state aligned
Clerk issues 60-second session tokens and refreshes them on a 50-second interval, leaving 10 seconds for network latency. In Core 3, getToken() also refreshes tokens in the background before they expire, so frequent callers stop blocking on the swap.
Two cookies carry that state. The 60-second session token lives in __session, set on your own domain and readable by client-side SDKs. The long-lived client token lives in __client, an HttpOnly cookie on Clerk's Frontend API domain, carrying a rotating_token that rotates on every sign-in to defeat session fixation. Development can't put the client token in a cross-site cookie, so it travels as a __clerk_db_jwt querystring parameter instead. Read none of them directly: call auth() or getToken().
Three App Router mistakes cause most session bugs:
- Matcher gaps: test protected pages, nested routes, APIs, callbacks, and static assets separately.
- Client-only checks: use hooks to shape the interface, never as the only protection around data.
- Layout-only checks: layouts don't re-render on navigation, so protect each page, action, handler, and data-access function directly.
Clerk's SDK validates tokens automatically. If you verify a token yourself, pass the public key explicitly (jwtKey: process.env.CLERK_JWT_KEY) to keep verification networkless; without it, Clerk makes a network call to fetch the JWKS.
That's the foundation: the requirements, a working Clerk integration, and the Proxy and session pitfalls. Part 2 covers scaling authentication, Supabase Row Level Security, and a comparison of 8 providers.
Frequently asked questions
In this series
- Next.js Authentication Guide 2026 (you are here)
- Next.js Authentication Guide 2026 - Part 2