Authentication Patterns
The idea in one sentence
Section titled “The idea in one sentence”Do a lightweight optimistic check in middleware to redirect obviously-unauthed requests, but do the REAL authorization close to the data — in a Data Access Layer that verifies the session before returning anything.
Sessions and cookies
Section titled “Sessions and cookies”Authentication usually rides on a session stored in an HTTP-only cookie. On the server you read that cookie with cookies() from next/headers.
// app/lib/session.ts — read the session cookie on the serverimport { cookies } from 'next/headers'import { decrypt } from '@/app/lib/crypto'
export async function getSession() { const cookie = (await cookies()).get('session')?.value if (!cookie) return null return decrypt(cookie)}Optimistic check in middleware
Section titled “Optimistic check in middleware”Middleware is the right place for a cheap, optimistic gate: if there is clearly no session, redirect before rendering. This is fast, but it only redirects — it must not be your only line of defense.
// middleware.ts — optimistic redirect onlyimport { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'
const protectedRoutes = ['/dashboard']
export function middleware(request: NextRequest) { const path = request.nextUrl.pathname const hasSession = request.cookies.has('session') if (protectedRoutes.some((p) => path.startsWith(p)) && !hasSession) { return NextResponse.redirect(new URL('/login', request.url)) } return NextResponse.next()}Real authorization lives in a Data Access Layer
Section titled “Real authorization lives in a Data Access Layer”Middleware alone is not sufficient. It does not run for every data path, and layouts do not re-run on every navigation — so a stale or forged request can slip past. The fix is a Data Access Layer (DAL): a single place that verifies the session right before it returns data. Wrap it in React cache so it runs once per request.
// app/lib/dal.ts — verify the session close to the dataimport 'server-only'import { cache } from 'react'import { redirect } from 'next/navigation'import { getSession } from '@/app/lib/session'
export const verifySession = cache(async () => { const session = await getSession() if (!session?.userId) { redirect('/login') } return { isAuth: true, userId: session.userId }})Then every page, Server Action, or route handler that touches protected data calls verifySession() first. For real projects, libraries like NextAuth (Auth.js), Clerk, or Lucia handle sessions, providers, and tokens for you — but the DAL pattern for authorization still applies on top of them.
graph TD A["Request"] --> B["middleware: optimistic redirect"] B --> C["Page / Server Action"] C --> D["verifySession() in DAL"] D -->|no session| E["redirect to /login"] D -->|valid| F["Return protected data"]