Back to Blog

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

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
February 12, 2027
13 min read

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

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 100+ 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} />;
}

๐Ÿš€ 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

See Also


Working With 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.

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 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

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.