Back to Blog

Next.js Middleware Authentication 2026: JWT Verification, Route Guards

Implement Next.js middleware authentication: JWT verification at the edge, route guards, role-based redirects, workspace context injection, and performance-safe middleware patterns.

Viprasol Tech Team
13 min read
Updated 2027

Next.js Middleware Authentication in 2026: JWT Verification, Route Guards, and Edge Runtime

Quick answer. Next.js middleware runs at the edge before pages or Server Components render, making it the right place for auth checks and redirects. Because the edge runtime lacks Node.js crypto, verify JWTs with the Web Crypto API, then apply route guards, role-based redirects, and inject workspace context into request headers.

Next.js middleware runs at the edge — before the page renders, before Server Components execute, before the request even hits your origin server. This makes it the right place for authentication checks, redirects, and request transformation — but the edge runtime has constraints that trip up most implementations.

This post covers JWT verification in edge middleware (no Node.js crypto — use Web Crypto API), route guards with pattern matching, role-based redirects, injecting workspace context into request headers, and the common mistakes that break performance or create security holes.


Middleware Basics

// middleware.ts — runs before EVERY matched request
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  // Runs at the edge — no Node.js APIs, no file system, no DB
  // Has access to: cookies, headers, URL, geo, IP
  
  return NextResponse.next(); // Continue to the route
}

// Only run middleware on matched paths (avoid running on static assets)
export const config = {
  matcher: [
    /*
     * Match all paths EXCEPT:
     * - _next/static (static files)
     * - _next/image (image optimization)
     * - favicon.ico
     * - public files (images, fonts)
     * - api routes that don't need auth
     */
    "/((?!_next/static|_next/image|favicon.ico|public/|images/).*)",
  ],
};

JWT Verification at the Edge

The edge runtime doesn't have Node.js crypto module — use the Web Crypto API (crypto.subtle):

// lib/auth/edge-jwt.ts
// Web Crypto API — works in Edge Runtime and Node.js 18+

const JWT_ALGORITHM = "HS256";
const encoder = new TextEncoder();

function base64UrlDecode(str: string): Uint8Array {
  const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
  const binary = atob(padded);
  return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}

async function getKey(secret: string): Promise<CryptoKey> {
  return crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"]
  );
}

export interface JWTPayload {
  sub: string;          // userId
  workspaceId: string;
  role: string;
  email: string;
  exp: number;
  iat: number;
}

export async function verifyJWT(token: string): Promise<JWTPayload | null> {
  try {
    const parts = token.split(".");
    if (parts.length !== 3) return null;

    const [headerB64, payloadB64, signatureB64] = parts;

    // Verify signature
    const key = await getKey(process.env.JWT_SECRET!);
    const data = encoder.encode(`${headerB64}.${payloadB64}`);
    const signature = base64UrlDecode(signatureB64);

    const valid = await crypto.subtle.verify("HMAC", key, signature, data);
    if (!valid) return null;

    // Decode payload
    const payload = JSON.parse(
      new TextDecoder().decode(base64UrlDecode(payloadB64))
    ) as JWTPayload;

    // Check expiry
    if (payload.exp < Math.floor(Date.now() / 1000)) return null;

    return payload;
  } catch {
    return null;
  }
}

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Full Authentication Middleware

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/lib/auth/edge-jwt";

// Routes that don't need authentication
const PUBLIC_PATHS = [
  "/",
  "/login",
  "/signup",
  "/forgot-password",
  "/reset-password",
  "/pricing",
  "/blog",
  "/api/auth",        // Auth endpoints (login, signup, refresh)
  "/api/webhooks",    // Stripe webhooks (separate auth)
];

// Routes that require specific roles
const ADMIN_PATHS = ["/admin"];
const BILLING_PATHS = ["/settings/billing", "/settings/usage"];

function isPublicPath(pathname: string): boolean {
  return PUBLIC_PATHS.some(
    (p) => pathname === p || pathname.startsWith(p + "/")
  );
}

function isAdminPath(pathname: string): boolean {
  return ADMIN_PATHS.some((p) => pathname.startsWith(p));
}

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Skip auth for public paths
  if (isPublicPath(pathname)) {
    return NextResponse.next();
  }

  // Get JWT from cookie (preferred) or Authorization header
  const token =
    request.cookies.get("auth-token")?.value ??
    request.headers.get("authorization")?.replace("Bearer ", "");

  if (!token) {
    // Redirect to login with return URL
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("redirect", pathname);
    return NextResponse.redirect(loginUrl);
  }

  const payload = await verifyJWT(token);

  if (!payload) {
    // Invalid or expired token — clear cookie and redirect
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("auth-token");
    return response;
  }

  // Role-based route guard
  if (isAdminPath(pathname) && payload.role !== "admin") {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  // Inject user context into request headers (available in Server Components)
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-user-id", payload.sub);
  requestHeaders.set("x-workspace-id", payload.workspaceId);
  requestHeaders.set("x-user-role", payload.role);
  requestHeaders.set("x-user-email", payload.email);

  // Continue with enriched headers
  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|public/|images/).*)",
  ],
};

Reading Injected Headers in Server Components

// lib/auth/server-context.ts
import { headers } from "next/headers";

export async function getServerUser() {
  const headersList = await headers();

  const userId = headersList.get("x-user-id");
  const workspaceId = headersList.get("x-workspace-id");
  const role = headersList.get("x-user-role");
  const email = headersList.get("x-user-email");

  if (!userId || !workspaceId) return null;

  return { userId, workspaceId, role: role ?? "member", email: email ?? "" };
}

// Usage in Server Component — no DB query needed for basic auth check
// (JWT payload was already verified in middleware)
export default async function DashboardPage() {
  const user = await getServerUser();
  if (!user) return null; // Middleware already redirected, this is a safety net

  return <Dashboard workspaceId={user.workspaceId} role={user.role} />;
}

Next.js - Next.js Middleware Authentication 2026: JWT Verification, Route Guards

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Token Refresh in Middleware

Short-lived access tokens (15 min) + refresh tokens (30 days):

// middleware.ts — with token refresh
import { verifyJWT, verifyRefreshToken, signJWT } from "@/lib/auth/edge-jwt";

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (isPublicPath(pathname)) return NextResponse.next();

  const accessToken = request.cookies.get("access-token")?.value;
  const refreshToken = request.cookies.get("refresh-token")?.value;

  // Try access token first
  let payload = accessToken ? await verifyJWT(accessToken) : null;

  // If access token expired but refresh token valid, issue new access token
  if (!payload && refreshToken) {
    const refreshPayload = await verifyRefreshToken(refreshToken);

    if (refreshPayload) {
      // Issue new access token (edge-compatible — uses Web Crypto)
      const newAccessToken = await signJWT({
        sub: refreshPayload.userId,
        workspaceId: refreshPayload.workspaceId,
        role: refreshPayload.role,
        email: refreshPayload.email,
      });

      payload = await verifyJWT(newAccessToken);

      // Continue AND set new cookie
      const requestHeaders = new Headers(request.headers);
      if (payload) {
        requestHeaders.set("x-user-id", payload.sub);
        requestHeaders.set("x-workspace-id", payload.workspaceId);
        requestHeaders.set("x-user-role", payload.role);
      }

      const response = NextResponse.next({ request: { headers: requestHeaders } });
      response.cookies.set("access-token", newAccessToken, {
        httpOnly: true,
        secure: true,
        sameSite: "lax",
        maxAge: 60 * 15, // 15 minutes
        path: "/",
      });
      return response;
    }
  }

  if (!payload) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("redirect", pathname);
    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete("access-token");
    response.cookies.delete("refresh-token");
    return response;
  }

  // Normal path — inject headers
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-user-id", payload.sub);
  requestHeaders.set("x-workspace-id", payload.workspaceId);
  requestHeaders.set("x-user-role", payload.role);

  return NextResponse.next({ request: { headers: requestHeaders } });
}

Workspace Routing Middleware

For multi-workspace apps where the workspace is in the URL:

// middleware.ts — workspace slug routing
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Detect workspace slug pattern: /[slug]/dashboard, /[slug]/settings, etc.
  const workspaceMatch = pathname.match(/^\/([a-z0-9-]+)\/(.*)/);

  if (workspaceMatch) {
    const [, slug, rest] = workspaceMatch;

    // Skip if slug is a reserved route
    const RESERVED_SLUGS = ["api", "auth", "login", "signup", "admin", "_next", "public"];
    if (RESERVED_SLUGS.includes(slug)) {
      return NextResponse.next();
    }

    // Inject workspace slug for downstream use
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-workspace-slug", slug);

    const payload = await verifyJWT(
      request.cookies.get("access-token")?.value ?? ""
    );

    if (!payload) {
      return NextResponse.redirect(new URL("/login", request.url));
    }

    requestHeaders.set("x-user-id", payload.sub);
    return NextResponse.next({ request: { headers: requestHeaders } });
  }

  return NextResponse.next();
}

Common Mistakes

// ❌ Using Node.js crypto in middleware — breaks edge runtime
import { createHmac } from "crypto"; // Not available in edge!

// ✅ Use Web Crypto API
const key = await crypto.subtle.importKey(/* ... */);

// ❌ Querying database in middleware — too slow for every request
const user = await db.user.findUnique({ where: { id: payload.sub } });

// ✅ Trust JWT payload for basic checks; query DB only in Server Components
// Middleware validates the token; Server Components fetch full user data if needed

// ❌ Running middleware on static assets — wastes edge compute
export const config = { matcher: "/:path*" };

// ✅ Exclude static files
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|public/).*)" ],
};

// ❌ Not setting redirect with return URL — users lose their deep link
return NextResponse.redirect(new URL("/login", request.url));

// ✅ Preserve the intended URL
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl);

Performance Budget

OperationEdge Latency
Cookie read<0.1ms
JWT signature verify (Web Crypto)0.5–2ms
Header injection<0.1ms
Database query10–50ms (❌ avoid)
Total acceptable middleware overhead<5ms

Cost and Timeline

ComponentTimelineCost (USD)
Basic JWT middleware0.5 day$300–$500
Token refresh in middleware0.5–1 day$400–$800
Role-based route guards0.5 day$300–$500
Workspace routing middleware0.5–1 day$400–$800
Full middleware auth system1–2 weeks$5,000–$8,000

Continue Learning


Our Approach at Viprasol

We implement Next.js middleware authentication for SaaS products — from simple JWT route guards through multi-workspace routing with token refresh. Our team has shipped middleware auth systems protecting hundreds of routes with sub-5ms overhead.

What we deliver:

  • Edge-compatible JWT verification using Web Crypto API
  • Route guard configuration with public/protected/admin patterns
  • Token refresh flow without DB queries in middleware
  • Workspace context injection via request headers
  • Security audit: middleware bypass vectors, token storage, cookie flags

Explore our web development services or contact us to secure your Next.js application.

Related: Next.js Monorepo + Turborepo: pnpm Workspaces & CI — structuring a Next.js monorepo with Turborepo and pnpm.

Next.jsTypeScriptAuthenticationSecurityEdge RuntimeMiddleware
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.