MENU
Authentication
Authentication answers a single question: who is making this request? It's distinct from authorization, which answers what that identified user is allowed to do. Next.js doesn't ship an authentication system of its own — instead, the App Router gives you several places to run authentication logic, and it's up to you (or a library) to wire them together.
Credentials-based vs. provider-based authentication
Most apps use one of two broad patterns, and many use both:
- Credentials-based authentication: the user supplies an identifier (email, username) and a secret (password) directly to your app. You look up the account, verify the password against a stored hash (never the plain password), and issue a session. All of this logic runs on your own server.
- Provider-based authentication (OAuth / OIDC): the user is redirected to a third-party identity provider (Google, GitHub, etc.), authenticates there, and is redirected back with proof of identity. Your app never sees the user's password; it exchanges an authorization code for tokens and creates a session from the provider's response.
Hand-rolling credentials auth is reasonable for simple apps. Hand-rolling OAuth is rarely worth it — the token exchange, state/PKCE handling, and provider quirks are exactly what authentication libraries exist to absorb.
Where auth logic lives in the App Router
- Server Actions: the natural home for handling a login or sign-up form submission. A 'use server' function receives the submitted credentials, verifies them against your data store, and writes a session cookie via the cookies() API. See Session Management for how the cookie itself is set.
- Route Handlers: used for authentication flows that need a plain HTTP endpoint rather than a form submission — most commonly an OAuth callback URL (e.g. app/api/auth/callback/[provider]/route.ts) that a third-party provider redirects back to.
- Server Components: read the current session (never write it) before rendering, so protected content is never sent to the client in the first place. This is stronger than hiding UI with client-side JavaScript, since the HTML itself is never generated for an unauthenticated request.
- Middleware: runs before a request reaches a route and can redirect unauthenticated visitors early, based on the presence of a session cookie. Middleware runs on the Edge Runtime, so it's best used for a cheap, optimistic check (does a session cookie exist and look well-formed?) rather than a database lookup. Treat it as the first line of defense, not the only one — see Authorization for why checks still belong closer to the data.
Checking auth state before rendering protected content
A common pattern is a small server-only helper that reads the session and either returns the current user or redirects. Calling it at the top of a protected Server Component means nothing below it executes — including data fetches — unless the user is actually signed in.
lib,auth.ts:
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { decryptSession } from './session'
export async function getCurrentUser() {
const sessionCookie = (await cookies()).get('session')?.value
if (!sessionCookie) return null
return decryptSession(sessionCookie) // returns null if invalid/expired
}
export async function requireUser() {
const user = await getCurrentUser()
if (!user) redirect('/login')
return user
}app,dashboard,page.tsx:
import { requireUser } from '@/lib/auth'
export default async function DashboardPage() {
const user = await requireUser() // redirects before anything below runs
return <p>Welcome back, {user.name}.</p>
}app,actions.ts:
'use server'
import { redirect } from 'next/navigation'
import { verifyPassword } from '@/lib/password'
import { createSession } from '@/lib/session'
import { db } from '@/lib/db'
export async function login(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
const account = await db.user.findUnique({ where: { email } })
if (!account || !(await verifyPassword(password, account.passwordHash))) {
return { error: 'Invalid email or password.' }
}
await createSession(account.id) // writes the session cookie
redirect('/dashboard')
}Whichever pattern you choose, keep the code that verifies identity on the server. Client Components can render a login form and call a Server Action, but the verification itself — comparing password hashes, validating an OAuth token, decrypting a session — should never ship to the browser.