MENU
Authorization
Where authentication establishes who is making a request, authorization decides what that particular, already-identified user is allowed to see or do. A signed-in user is not automatically entitled to every page or every record — authorization is the set of checks that enforce that distinction: role checks ("is this user an admin?"), permission checks ("can this user edit this document?"), and ownership checks ("does this record belong to this user?").
Coarse checks in Middleware, fine checks close to the data
Next.js Middleware runs before a request reaches a route, which makes it a convenient place to redirect obviously unauthorized requests before any rendering work happens — for example, sending anyone without a session cookie away from an entire /admin route group. But Middleware runs on the Edge Runtime, matches routes by pattern, and typically can't afford a database round-trip on every request, so it's best suited to coarse, structural checks: is there a session at all, does its role claim look right.
Fine-grained checks — does this specific user own this specific record, does their role actually grant this specific action — belong in the Server Component, Server Action, or Route Handler that's about to touch the data. This is defense in depth: a Middleware matcher misconfiguration, a route added after the matcher was written, or a direct call to a Server Action from outside the page it's normally attached to should never be the only thing standing between a request and data it shouldn't reach. Treat every Server Action and Route Handler as if it could be called directly, because it can be.
Protecting routes and redirecting unauthorized users
Two outcomes are worth distinguishing, and Next.js has conventions for both:
- Not authenticated at all: typically redirected to a login page with redirect() from next/navigation, as shown in Authentication.
- Authenticated, but not permitted: this is a genuine 401 (Unauthorized) or 403 (Forbidden) outcome rather than a redirect. Since Next.js 15.1, the unauthorized() and forbidden() functions let a Server Component, Server Action, or Route Handler bail out and render a dedicated unauthorized.tsx or forbidden.tsx boundary file (siblings of not-found.tsx) instead of throwing a generic error. These APIs are gated behind the authInterrupts experimental flag in next.config.js, so check the installed Next.js version's docs before relying on the exact signature in production.
next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
}
export default nextConfigmiddleware.ts:
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has('session')
if (!hasSession) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*'],
}app,admin,users,page.tsx:
import { forbidden } from 'next/navigation'
import { requireUser } from '@/lib/auth'
export default async function AdminUsersPage() {
const user = await requireUser() // redirects to /login if not authenticated
if (user.role !== 'admin') {
forbidden() // renders app/forbidden.tsx
}
// safe to fetch and render admin-only data below
return <p>Admin user list.</p>
}If your Next.js version doesn't yet support authInterrupts, the same intent can be expressed by redirecting to a dedicated "not permitted" page or by returning a 403 response from a Route Handler — the underlying principle (check permissions at the point of access, and fail in a way that's explicit rather than a silent empty page) matters more than the specific API.