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:

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



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.

A reusable session helper, called from a protected Server Component. redirect() from next/navigation stops rendering immediately and sends the browser to the login page.
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>
}
A Server Action handling a credentials login form. On success it hands off to the session helper described in Session Management to actually set the cookie.
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.