Next.js Protected Routes: Middleware, JWT, and Server-Side Authorization
- Technology & Frameworks
- Protected routes in Next.js require two layers.
- `proxy.ts` (or `middleware.ts` on Next.js 15) performs a fast session check and redirects anonymous users.
- Authorization belongs next to the data, not in middleware.
- JWTs must be verified with signature validation, not decoded.
- Server Components, Route Handlers, and Server Actions must all re-check authorization.
- The same architecture applies whether you’re using custom JWTs, Auth.js, Clerk, or Supabase Auth.
- Server Components
- Route Handlers
- Server Actions
- APIs
- Read session cookie
- Verify JWT signature
- Verify expiration
- Redirect anonymous users
- Fast
- Cheap
- Excellent user experience
- Has no database access
- Cannot detect revocations
- Cannot enforce business rules
- Should not be treated as authorization
- Verify session again
- Load current user
- Apply role checks
- Apply ownership checks
- Return safe DTOs
- Read cookie
- Verify token signature
- Verify expiration
- Refresh session if required
- Redirect anonymous users
- Database queries
- Permission checks
- Role lookups
- Ownership validation
- Include protected application routes
- Exclude static assets
- Exclude image optimization routes
- Exclude public pages such as login and signup
- Exclude authentication endpoints when they do not require the protected-route check
- Signature
- Expiry
- Issuer
- Audience
- Required claims
- Multiple services consume the token
- Middleware needs authentication without DB access
- External identity providers issue tokens
- One application exists
- One database exists
- Immediate revocation is important
- Verify sessions
- Load users
- Apply authorization rules
- Return safe DTOs
- Hide sensitive fields
- The account has been deleted
- The user was disabled
- The role changed
- Access was revoked
- Authentication
- Authorization
- Input
Next.js Protected Routes: Middleware, JWT, and Server-Side Authorization
Vladislav Shkutovich Full-Stack Engineer
Sep 17, 2026 27 min
TL;DR #
Version note: This article uses proxy.ts, introduced in Next.js 16. On Next.js 15 and earlier, use middleware.ts. The security architecture is the same: request interception is an early session check, while authorization belongs in the server-side data layer.
Next.js Authentication: Middleware vs. Real Route Protection #
Search for Next.js protected routes and you’ll find dozens of tutorials that recommend the same pattern:
1. Create middleware 2. Read a session cookie 3. Verify a JWT 4. Redirect if authentication fails
That pattern is useful, but it is incomplete. Protected routes in Next.js are not really about pages. They are about data. Modern App Router applications expose data through:
As a result, Next.js middleware protected routes should be treated as a user-experience feature, not a security boundary. This article explains the architecture we use for Next.js authentication, why middleware alone is insufficient, and how to build protected routes that remain secure even when middleware is bypassed, misconfigured, or skipped.
What Are Protected Routes in Next.js? #
The phrase “protected route” can mean three different things.
Hiding UI #
A Client Component checks for a session and shows a login screen instead of content. This is cosmetic protection. The underlying data may still be accessible.
Blocking Navigation #
A middleware or `proxy.ts` check intercepts requests before rendering. This improves user experience because signed-out users are redirected immediately.
Protecting Data #
Authorization happens next to the database query, Route Handler, or Server Action. This is the only definition that survives direct requests to your endpoints.
For App Router applications, the third definition is the one that matters.
Next.js Protected Routes: The Two-Layer Security Model #
Every protected route should answer the same question twice:
1. Is this user likely authenticated? 2. Is this user actually allowed to access this data?
Layer 1: Middleware or proxy.ts #
The first check happens before rendering.
Responsibilities:
Benefits:
Limitations:
Layer 2: Data Access Layer #
The second check happens where data is retrieved.
Responsibilities:
This is where authorization belongs.
Why Middleware Alone Can’t Protect Next.js Routes #
A protected route implemented only in middleware is not truly protected. Middleware exists in front of the application. Authorization needs to live inside the application.
The CVE-2025-29927 Example #
CVE-2025-29927 is a useful example of why middleware should not be the application’s only authorization boundary. The broader principle is simple: protection implemented only at the request boundary should never be the sole control protecting sensitive data.
The Security Principle #
Use middleware to decide: Should this navigation continue?
Use the Data Access Layer to decide: Can this user access this data?
How to Protect Routes in Next.js 16 with proxy.ts #
Next.js 16 renamed middleware to `proxy.ts`.
Basic Flow #
`Request`
`↓`
`proxy.ts`
`↓`
`JWT verification`
`↓`
`Redirect if unauthenticated`
`↓`
`Page / Route Handler / Server Action`
`↓`
`Data Access Layer`
`↓`
`Authorization`
`↓`
`Data returned`
What proxy.ts Should Do #
Keep middleware lightweight:
What proxy.ts Should Not Do #
Avoid:
Keep these checks in the server-side authorization layer.
Scoping Protected Routes with `config.matcher` #
A matcher determines which requests reach `proxy.ts`. It controls where `proxy.ts` runs, but it does not define your application’s complete authentication or authorization policy.
Use the matcher to keep Proxy focused on routes that benefit from an early session check:
The important distinction is that a route excluded from the matcher is not automatically authorized. Sensitive data and operations must still enforce authentication and authorization at the Server Component, Route Handler, Server Action, or Data Access Layer that accesses them.
`matcher` controls where Proxy runs; it does not define who is allowed to access your data.
JWT Verification for Next.js Authentication #
A common mistake in NextJS auth implementations is decoding a JWT instead of verifying it.
Bad #
TypeScript
`decodeJwt(token)`
Show more lines
This only decodes Base64 data.
Decoding only reads the token’s payload; it does not verify its signature.
Good #
`TypeScript`
`jwtVerify(token, secretKey, {`
`algorithms: ["HS256"],`
`issuer: SESSION_ISSUER,`
`audience: SESSION_AUDIENCE,`
`})`
Show more lines
This validates:
Only verified tokens should be treated as authenticated sessions.
JWT vs Opaque Sessions #
A JWT is useful when:
An opaque session ID is often simpler when:
Building a Data Access Layer for Next.js Auth #
Sensitive data access and authorization should happen in a trusted server-side layer close to the data.
Responsibilities #
The DAL should:
Example Pattern #
`const session = await getSession()`
`if (!session) {`
`redirect("/login")`
`}`
Why Sessions Must Be Rechecked #
A JWT might still be valid while:
Those checks require current authoritative server-side state.
Route Handlers, Server Actions, and Server Components #
Modern Next.js applications expose multiple execution paths. Each must perform authorization.
Server Components #
Pages should consult the Data Access Layer before rendering sensitive data.
Server Actions #
Server Actions are public endpoints. Treat them the same way you would treat REST APIs. A Server Action is callable through an HTTP request, so don’t assume that because it is defined in a server-only file it is automatically authorized.
Validate:
on every request.
`"use server";`
`export async function deleteProject(projectId: string) {`
`const user = await requireUser();`
`const project = await getProject(projectId);`
`if (project.ownerId !== user.id) {`
`throw new Error("Forbidden");``}`
`await deleteProjectFromDatabase(projectId);`
`}`
Route Handlers #
Route Handlers should never trust page-level protection. They must independently verify authorization.
Role-Based Access Control for Next.js Protected Routes #
Role checks belong in the database layer.
Do Not Trust JWT Role Claims #
A token issued before a demotion still contains the old role.
For example:
`JWT: admin`
`Database: member`
For permissions that can change during a session, authorization should use the current authoritative server-side state rather than trusting potentially stale role claims in a JWT.
Recommended Approach #
TypeScript
`requireRole("admin")`
Show more lines
The helper:
1. Loads current user 2. Reads current role 3. Applies rules 4. Allows or denies access
Protected Routes with Auth.js and Supabase #
The architecture remains the same regardless of provider.
Auth.js Protected Routes #
Auth.js is often used to implement protected routes in Next.js, and its middleware can simplify the initial authentication check.
However:
The authentication provider does not change this security model.
Supabase Protected Routes in Next.js #
When implementing Supabase protected routes in Next.js:
Middleware frequently:
Authorization should still happen in:
Authentication identifies users. Authorization grants access.
Common Mistakes in Next.js Authentication #
1. Middleware As The Only Protection #
The most common mistake.
2. Decoding Instead of Verifying JWTs #
Decoded payloads prove nothing.
3. Trusting Layouts #
Layouts do not always rerun. Do not treat them as security boundaries.
4. Treating Server Actions As Private #
They are public HTTP endpoints.
5. Storing Protected Files Under public/ #
Files inside `/public` bypass your authorization logic.
6. Trusting Role Claims #
When permissions can change during a session, use current server-side role data rather than potentially stale JWT claims.
7. Infinite Sliding Sessions #
Always enforce a maximum session lifetime.
8. Database Queries in Middleware #
Middleware runs on every matched request.
Should You Build Next.js Authentication Yourself? #
For most applications, an established authentication library reduces the amount of security-sensitive code you need to maintain. Libraries provide:
A custom implementation is reasonable when you only need:
Anything larger usually benefits from an established authentication solution.
Practical implementation note #
In production Next.js + Payload projects, we’ve used both CMS-managed authentication and `jose` for verifying tokens from OIDC flows. The implementation choice depends on whether authentication is handled by the CMS, an identity provider, or the application itself.
How We Verified the Architecture #
We run the same checks in every Next.js security audit. Testing included:
The critical test:
1. Keep a valid cookie 2. Delete the account 3. Access a protected route
Result:
This demonstrates why authorization belongs next to the data.
Next.js Protected Routes: Secure the Data, Not Just the Page #
In Next.js, protected routes are ultimately about controlling access to data, not pages. Middleware and `proxy.ts` provide a fast authentication check that verifies sessions, redirects anonymous users, and keeps requests cheap.
The Data Access Layer understands the current state of your application: users, roles, ownership rules, revocations, and business logic. It is the security boundary that decides what data leaves the server. Whether you use custom JWT authentication, Auth.js, Supabase Auth, or another Next.js authentication solution, the architecture stays the same:
Not sure where your app stands? #
Most authentication issues we find in Next.js projects are not exotic exploits. They are the gaps covered in this article: authorization that exists only in middleware, JWTs that are decoded but never verified, Server Actions assumed to be private, and role claims trusted over current server-side data.
Our Next.js security audit applies the same checks used to validate this architecture, including tampered tokens, revoked users, role changes, and every entry point that bypasses middleware.
If you need a second set of eyes on those risks, you can book a Next.js audit.
Next.js Protected Routes FAQ
A function that runs in front of the application on every request the matcher selects, before routing and rendering, and can redirect, rewrite or pass the request through. On Next.js 16 the file is `proxy.ts` and the docs call the feature Proxy; `middleware.ts` still works and prints a deprecation warning.
At the project root or inside `src/`, next to `app/`, named `proxy.ts` on Next.js 16 or `middleware.ts` on 15. If both exist, `next build` stops with "Both middleware file ... and proxy file ... are detected".
Read them from the request, `request.cookies.get("session")?.value`, and set them on the response you return, `response.cookies.set(...)`. In Server Components, Server Actions and Route Handlers, use `cookies()` from `next/headers`, which is async in Next.js 15 and 16.
In a cookie set by the server with `HttpOnly`, `Secure` and `SameSite=Lax`. Never in localStorage: any script on the page can read it, and middleware never sees it. If a route delivers files to an `<a download>` anchor, answer a refusal with a 204 and no body, so that no browser has anything to save as the file.
No. Next.js loads one `proxy.ts` or one `middleware.ts` per project. Compose inside it: branch on `request.nextUrl.pathname`, or call small functions in order and return the first response that is not `NextResponse.next()`.
No. It sees a token and never the store, so it cannot know that an account was deleted or a role revoked, and on unpatched self-hosted versions CVE-2025-29927 let a request header skip it entirely. Verify again in a Data Access Layer that every Server Component, Server Action and Route Handler calls before touching data.
Related articles
Next.js Redirects: A Developer's Guide (2026)
Jan 30, 2025
Technology & Frameworks NextJS
Self-hosted Next.js: When Vercel is Not an Option
Aug 4, 2026
Robert Hart Expert
Technology & Frameworks Vercel NextJS
Common Next.js Project Challenges in 2024
Mar 13, 2024
Technology & Frameworks NextJS
Next.js Serverless Functions Guide: Patterns, Performance & Reliability
Sep 18, 2026
Technology & Frameworks NextJS
.png&w=828&q=75)
Vite vs. Next.js: When to Use Which? An Honest Comparison
Aug 20, 2026
Technology & Frameworks NextJS
Next.js CMS: Best Headless CMS Choices for 2026
Aug 1, 2026
+1
Eugene Boruhov 
Anton Meleshkin
Alex Hramovich
Gleb Colei
Aleksei Zhilyuk