Skip to content

Authentication Patterns

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.

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 server
import { 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)
}

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 only
import { 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 data
import '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"]
Optimistic gate in middleware, real check in the DAL
How do you read the session cookie on the server?
What should middleware do for auth?
Why is middleware alone not sufficient for authorization?
Where does the authoritative authorization check belong?