Next.js Protected Routes: Middleware, JWT, and Server-Side Authorization

focusreactive.com · 2026-09-29 · by Long Nguyen ·generic
Protect Next.js routes with proxy.ts, verified JWTs, and data-layer authorization that stops unauthorized access.

Next.js Protected Routes: Middleware, JWT, and Server-Side Authorization

  • Technology & Frameworks
  • Next.js Protected Routes: Middleware, JWT, and Server-Side Authorization

    Vladislav Shkutovich Full-Stack Engineer

    Sep 17, 2026 27 min

  • TL;DR #

  • 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.
  • 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:

  • Server Components
  • Route Handlers
  • Server Actions
  • APIs
  • 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:

  • Read session cookie
  • Verify JWT signature
  • Verify expiration
  • Redirect anonymous users
  • Benefits:

  • Fast
  • Cheap
  • Excellent user experience
  • Limitations:

  • Has no database access
  • Cannot detect revocations
  • Cannot enforce business rules
  • Should not be treated as authorization
  • Layer 2: Data Access Layer #

    The second check happens where data is retrieved.

    Responsibilities:

  • Verify session again
  • Load current user
  • Apply role checks
  • Apply ownership checks
  • Return safe DTOs
  • 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:

  • Read cookie
  • Verify token signature
  • Verify expiration
  • Refresh session if required
  • Redirect anonymous users
  • What proxy.ts Should Not Do #

    Avoid:

  • Database queries
  • Permission checks
  • Role lookups
  • Ownership validation
  • 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:

  • 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
  • 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:

  • Signature
  • Expiry
  • Issuer
  • Audience
  • Required claims
  • Only verified tokens should be treated as authenticated sessions.

    JWT vs Opaque Sessions #

    A JWT is useful when:

  • Multiple services consume the token
  • Middleware needs authentication without DB access
  • External identity providers issue tokens
  • An opaque session ID is often simpler when:

  • One application exists
  • One database exists
  • Immediate revocation is important
  • 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:

  • Verify sessions
  • Load users
  • Apply authorization rules
  • Return safe DTOs
  • Hide sensitive fields
  • Example Pattern #

    `const session = await getSession()`

    `if (!session) {`

    `redirect("/login")`

    `}`

    Why Sessions Must Be Rechecked #

    A JWT might still be valid while:

  • The account has been deleted
  • The user was disabled
  • The role changed
  • Access was revoked
  • 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:

  • Authentication
  • Authorization
  • Input
  • 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:

  • It improves navigation by handling the initial authentication check.
  • Authorization still belongs in your Data Access Layer (DAL).
  • Server Actions and Route Handlers still require their own authorization checks.
  • 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:

  • Refreshes sessions
  • Attaches authentication context
  • Authorization should still happen in:

  • Server Components
  • Route Handlers
  • Server Actions
  • Database policies
  • 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:

  • Password hashing
  • Session rotation
  • OAuth providers
  • Magic links
  • Email verification
  • Security hardening
  • A custom implementation is reasonable when you only need:

  • A single signed cookie
  • One purpose
  • One verification route
  • 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:

  • Tampered tokens
  • Revoked users
  • Role changes
  • Open redirect attempts
  • Download endpoints
  • Cross-site requests
  • Session refresh behavior
  • Middleware migration scenarios
  • The critical test:

    1. Keep a valid cookie 2. Delete the account 3. Access a protected route

    Result:

  • Middleware permits navigation
  • The Data Access Layer denies access
  • User is redirected to login
  • 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:

  • Middleware improves navigation
  • The Data Access Layer protects data
  • You need both, but only the Data Access Layer has the final say
  • 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

    All Vladislav’s Articles

    Next.js Redirects: A Developer's Guide (2026)

    Jan 30, 2025

    Anton Meleshkin

    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

    Alex Hramovich

    Technology & Frameworks NextJS

    Next.js Serverless Functions Guide: Patterns, Performance & Reliability

    Sep 18, 2026

    Maksim Bashchuk

    Technology & Frameworks NextJS

    .png&w=828&q=75)

    Vite vs. Next.js: When to Use Which? An Honest Comparison

    Aug 20, 2026

    Gleb Colei

    Technology & Frameworks NextJS

    Next.js CMS: Best Headless CMS Choices for 2026

    Aug 1, 2026

    Aleksei Zhilyuk

    +1

  • Eugene Boruhov

Headless CMS Deep Dive NextJS

focusreactive.com · 22 images · 42 links

Comments (0)

No comments yet