Implementing JWT Middleware in Next.js: A Complete Guide to Auth

Leapcell: The Best of Serverless Web Hosting Implementing Authentication and Authorization with JWT Middleware in Next.js: From Basics to Practical Application I. Introduction: Why Choose JWT for Authentication? In modern web development, user authentication and authorization are core aspects of building secure applications. JSON Web Token (JWT), with its stateless, cross-platform, and lightweight characteristics, has become one of the most mainstream authentication solutions in front-end and back-end separated applications. As the most popular full-stack framework in the React ecosystem, Next.js provides a powerful middleware mechanism that can efficiently implement request interception and route protection. This article will delve into how to achieve user authentication in Next.js through custom middleware in combination with JWT, ensuring that the requests contain valid userid and username, and covering the entire process from basic principles to production-level practices. II. JWT Basics: Core Concepts and Working Principles 2.1 JWT Structure Analysis JWT consists of three parts, separated by .: Header: Contains the token type (JWT) and the signing algorithm (such as HMAC SHA256, RSA). { "alg": "HS256", "typ": "JWT" } Payload: Stores user information (non-sensitive data) and metadata (such as the expiration time exp). { "sub": "1234567890", // User unique identifier (userid) "name": "John Doe", // Username (username) "iat": 1687756800, // Issued at time "exp": 1687760400 // Expiration time (1 hour later) } Signature: Signs the Header and Payload using the algorithm specified in the Header to ensure that the token has not been tampered with. 2.2 Authentication Process Login Phase: The user submits credentials (username/password). After the server verifies successfully, it generates a JWT and returns it to the client. Token Storage: The client stores the JWT in an HttpOnly Cookie or local storage (Cookies are recommended to avoid XSS attacks). Request Interception: Each subsequent request carries the JWT (usually in the Authorization header or Cookie). The server verifies the validity of the token through middleware. Authorization Decision: After successful verification, the userid and username are parsed for permission judgment or data filtering. III. Next.js Middleware: Core Mechanisms and Advantages 3.1 Middleware Types Next.js 13+ introduces a new middleware system that supports two modes: Global Middleware: Takes effect for all routes (except routes that match the middleware-exclude pattern). Route Middleware: Only takes effect for specific routes or route groups (defined through file location or configuration). 3.2 Core Capabilities Request Interception: Before the request reaches the page or API route, modify the request headers, parse Cookies, and verify the identity. Response Control: According to the authentication result, return a redirect response (for example, redirect to the login page if not authenticated). Edge Execution: Supports running on the Vercel Edge Network to achieve low-latency authentication (avoid using Node.js specific APIs). 3.3 Middleware File Structure Create middleware.ts (TypeScript) or middleware.js in the root directory of the project and export an asynchronous function that receives Request and NextRequest objects: import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { // Authentication logic const response = NextResponse.next(); return response; } // Configure the scope of the middleware (example: match all page routes) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] }; IV. Project Setup: From Initialization to Dependency Installation 4.1 Create a Next.js Project npx create-next-app@latest jwt-auth-demo # Choose TypeScript, App Router (default), ESLint (optional) 4.2 Install Dependencies npm install jsonwebtoken cookie @types/cookie # JWT handling and Cookie parsing npm install bcryptjs # Password hashing (used only on the server side) 4.3 Environment Configuration Create .env.local in the root directory to store the JWT signing key and expiration time: JWT_SECRET=your-strong-secret-key-128-bit-or-longer JWT_EXPIRES_IN=3600 # 1 hour (in seconds) V. Implementing the Login Function: Generating and Returning JWT 5.1 Create a Login API Route Implement the login logic in app/api/auth/login/route.ts: import { NextResponse } from 'next/server'; import jwt from 'jsonwebtoken'; import { compare } from 'bcryptjs'; import { User } from '@/types/user'; // Custom user type // Mock database users (actually need to connect to the database) const mo

Apr 25, 2025 - 05:39
 0
Implementing JWT Middleware in Next.js: A Complete Guide to Auth

Image description

Leapcell: The Best of Serverless Web Hosting

Implementing Authentication and Authorization with JWT Middleware in Next.js: From Basics to Practical Application

I. Introduction: Why Choose JWT for Authentication?

In modern web development, user authentication and authorization are core aspects of building secure applications. JSON Web Token (JWT), with its stateless, cross-platform, and lightweight characteristics, has become one of the most mainstream authentication solutions in front-end and back-end separated applications. As the most popular full-stack framework in the React ecosystem, Next.js provides a powerful middleware mechanism that can efficiently implement request interception and route protection. This article will delve into how to achieve user authentication in Next.js through custom middleware in combination with JWT, ensuring that the requests contain valid userid and username, and covering the entire process from basic principles to production-level practices.

II. JWT Basics: Core Concepts and Working Principles

2.1 JWT Structure Analysis

JWT consists of three parts, separated by .:

  1. Header: Contains the token type (JWT) and the signing algorithm (such as HMAC SHA256, RSA).
   { "alg": "HS256", "typ": "JWT" }
  1. Payload: Stores user information (non-sensitive data) and metadata (such as the expiration time exp).
   { 
     "sub": "1234567890",  // User unique identifier (userid)
     "name": "John Doe",    // Username (username)
     "iat": 1687756800,     // Issued at time
     "exp": 1687760400      // Expiration time (1 hour later)
   }
  1. Signature: Signs the Header and Payload using the algorithm specified in the Header to ensure that the token has not been tampered with.

2.2 Authentication Process

  1. Login Phase: The user submits credentials (username/password). After the server verifies successfully, it generates a JWT and returns it to the client.
  2. Token Storage: The client stores the JWT in an HttpOnly Cookie or local storage (Cookies are recommended to avoid XSS attacks).
  3. Request Interception: Each subsequent request carries the JWT (usually in the Authorization header or Cookie). The server verifies the validity of the token through middleware.
  4. Authorization Decision: After successful verification, the userid and username are parsed for permission judgment or data filtering.

III. Next.js Middleware: Core Mechanisms and Advantages

3.1 Middleware Types

Next.js 13+ introduces a new middleware system that supports two modes:

  • Global Middleware: Takes effect for all routes (except routes that match the middleware-exclude pattern).
  • Route Middleware: Only takes effect for specific routes or route groups (defined through file location or configuration).

3.2 Core Capabilities

  • Request Interception: Before the request reaches the page or API route, modify the request headers, parse Cookies, and verify the identity.
  • Response Control: According to the authentication result, return a redirect response (for example, redirect to the login page if not authenticated).
  • Edge Execution: Supports running on the Vercel Edge Network to achieve low-latency authentication (avoid using Node.js specific APIs).

3.3 Middleware File Structure

Create middleware.ts (TypeScript) or middleware.js in the root directory of the project and export an asynchronous function that receives Request and NextRequest objects:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  // Authentication logic
  const response = NextResponse.next();
  return response;
}

// Configure the scope of the middleware (example: match all page routes)
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

IV. Project Setup: From Initialization to Dependency Installation

4.1 Create a Next.js Project

npx create-next-app@latest jwt-auth-demo
# Choose TypeScript, App Router (default), ESLint (optional)

4.2 Install Dependencies

npm install jsonwebtoken cookie @types/cookie # JWT handling and Cookie parsing
npm install bcryptjs # Password hashing (used only on the server side)

4.3 Environment Configuration

Create .env.local in the root directory to store the JWT signing key and expiration time:

JWT_SECRET=your-strong-secret-key-128-bit-or-longer
JWT_EXPIRES_IN=3600 # 1 hour (in seconds)

V. Implementing the Login Function: Generating and Returning JWT

5.1 Create a Login API Route

Implement the login logic in app/api/auth/login/route.ts:

import { NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { compare } from 'bcryptjs';
import { User } from '@/types/user'; // Custom user type

// Mock database users (actually need to connect to the database)
const mockUsers: User[] = [
  { id: '1', username: 'admin', password: '$2a$10$H6pXZpZ...' } // Hashed password
];

export async function POST(request: Request) {
  const { username, password } = await request.json();

  // Find the user
  const user = mockUsers.find(u => u.username === username);
  if (!user) {
    return NextResponse.json({ error: 'User does not exist' }, { status: 401 });
  }

  // Verify the password
  const isPasswordValid = await compare(password, user.password);
  if (!isPasswordValid) {
    return NextResponse.json({ error: 'Incorrect password' }, { status: 401 });
  }

  // Generate JWT
  const token = jwt.sign(
    { userId: user.id, username: user.username }, // The payload contains userid and username
    process.env.JWT_SECRET!,
    { expiresIn: process.env.JWT_EXPIRES_IN }
  );

  // Set an HttpOnly Cookie (security attribute)
  const response = NextResponse.json({ message: 'Login successful' });
  response.cookies.set('authToken', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // Enable HTTPS in the production environment
    sameSite: 'lax', // Prevent CSRF attacks
    maxAge: Number(process.env.JWT_EXPIRES_IN),
    path: '/'
  });

  return response;
}

5.2 Login Page Component

Create a login form in app/login/page.tsx:

'use client'; // Client component
import { useState } from'react';
import { useRouter } from 'next/navigation';

export default function LoginPage() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });

    if (response.ok) {
      router.push('/dashboard'); // Redirect after successful login
    } else {
      const data = await response.json();
      console.error(data.error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        required
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required
      />
      <button type="submit">Loginbutton>
    form>
  );
}

VI. Implementing the Core Middleware: JWT Verification and User Information Extraction

6.1 Parse the JWT in the Cookie

First, create a utility function utils/jwt.ts to handle JWT verification:

import jwt from 'jsonwebtoken';
import { Cookie } from 'cookie';

export type JwtPayload = {
  userId: string;
  username: string;
  iat?: number;
  exp?: number;
};

export function parseAuthCookie(cookieHeader: string | undefined): string | null {
  if (!cookieHeader) return null;
  const cookies = Cookie.parse(cookieHeader);
  return cookies.authToken || null;
}

export function verifyJwt(token: string): JwtPayload | null {
  try {
    return jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
  } catch (error) {
    console.error('JWT verification failed:', error);
    return null;
  }
}

6.2 Write the Authentication Middleware

Implement the core logic in middleware.ts to intercept requests and verify the JWT:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { parseAuthCookie, verifyJwt } from './utils/jwt';

export async function middleware(request: NextRequest) {
  // 1. Get the JWT from the Cookie
  const token = parseAuthCookie(request.headers.get('cookie'));

  // 2. Define protected routes (example: all pages except the login page)
  const isProtectedRoute = !request.nextUrl.pathname.startsWith('/login');

  if (isProtectedRoute) {
    // 3. No token provided: Redirect to the login page
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    // 4. Verify the JWT
    const payload = verifyJwt(token);
    if (!payload) {
      // The token is invalid or expired, clear the invalid Cookie (optional)
      const response = NextResponse.redirect(new URL('/login', request.url));
      response.cookies.delete('authToken');
      return response;
    }

    // 5. Verification passed: Attach user information to the request context (optional, needs to be used with route parameters)
    // Or obtain it through the getToken function in subsequent processing
  } else {
    // 6. Login page: If authenticated, redirect to the dashboard
    if (token && verifyJwt(token)) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  // 7. Allow the request to continue
  return NextResponse.next();
}

// 8. Configure the scope of the middleware (match all pages except API routes and static resources)
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

6.3 Analysis of the Core Logic of the Middleware

  1. Route Protection Strategy: Determine whether authentication is required through isProtectedRoute (excluding the /login page).
  2. Token Existence Check: When no valid Cookie is carried, redirect to the login page.
  3. Signature Verification and Expiration Check: Use the jsonwebtoken library to verify the token and automatically handle expiration (the exp field).
  4. Security Enhancement: Clear the client Cookie when the token is invalid to avoid reusing the expired token.
  5. Login Page Optimization: When an authenticated user visits the login page, automatically redirect to the dashboard to improve the user experience.

VII. Protecting Routes: Obtaining User Information in Pages

7.1 Obtain User Information in Server Components

In a protected page (such as app/dashboard/page.tsx), obtain the userid and username by re-verifying the JWT:

import { NextResponse } from 'next/server';
import { parseAuthCookie, verifyJwt } from '@/utils/jwt';

export default async function Dashboard() {
  // Get the Cookie from the request headers (server components can access the request object)
  const token = parseAuthCookie(headers.get('cookie'));
  const payload = token? verifyJwt(token) : null;

  if (!payload) {
    // Theoretically, the middleware has intercepted it, but edge cases need to be handled (such as the token expiring during the request)
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return (
    <div>
      <h1>Welcome, {payload.username}!</h1>
      <p>User ID: {payload.userId}</p>
    </div>
  );
}

7.2 Obtain the Authentication Status in Client Components

Obtain the user status from the server through useEffect or a third-party library (such as SWR):

'use client';
import { useEffect, useState } from'react';
import { useRouter } from 'next/navigation';

export default function UserProfile() {
  const [user, setUser] = useState<{ userId: string; username: string } | null>(null);
  const router = useRouter();

  useEffect(() => {
    // The client sends a request to the API route to verify the token and return user information
    const fetchUser = async () => {
      const response = await fetch('/api/auth/user');
      if (response.ok) {
        const data = await response.json();
        setUser(data);
      } else {
        router.push('/login');
      }
    };

    fetchUser();
  }, [router]);

  return (
    user? (
      <div>
        <h2>User Informationh2>
        <p>Username: {user.username}p>
        <p>User ID: {user.userId}p>
      div>
    ) : (
      <p>Loading...p>
    )
  );
}

7.3 Create a User Information API Route

Provide a user information interface in app/api/auth/user/route.ts (needs to be protected by middleware):

import { NextResponse } from 'next/server';
import { parseAuthCookie, verifyJwt } from '@/utils/jwt';

export async function GET(request: Request) {
  const token = parseAuthCookie(request.headers.get('cookie'));
  const payload = token? verifyJwt(token) : null;

  if (!payload) {
    return NextResponse.json({ error: 'Unauthenticated' }, { status: 401 });
  }

  return NextResponse.json({ userId: payload.userId, username: payload.username });
}

VIII. Logging Out: Clearing Client Cookies

8.1 Implement the Logout API Route

Clear the authToken Cookie in app/api/auth/logout/route.ts:

import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const response = NextResponse.redirect(new URL('/login', request.url));
  response.cookies.delete('authToken', { path: '/' }); // Clear the Cookie
  return response;
}

8.2 Logout Button Component

Call the logout API in the client component:

'use client';
import { useRouter } from 'next/navigation';

export default function LogoutButton() {
  const router = useRouter();

  const handleLogout = async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    router.push('/login');
  };

  return <button onClick={handleLogout}>Logoutbutton>;
}

IX. Security Enhancement: Best Practices in the Production Environment

9.1 Cookie Security Attribute Configuration

  • HttpOnly: Prevent XSS attacks (the token cannot be accessed through JavaScript).
  • Secure: Only send in an HTTPS environment (mandatory in the production environment).
  • SameSite: 'lax' or 'strict': Prevent CSRF attacks (lax is suitable for most scenarios, and strict is more strict).
  • Domain and Path: Limit the scope of the Cookie (for example, only allow yourdomain.com to access).

9.2 JWT Signing and Expiration Strategy

  • Use a key of at least 256 bits (HS256 algorithm), or RSA asymmetric encryption (suitable for distributed systems).
  • Set a short expiration time (such as 1 hour), combined with the Refresh Token mechanism to avoid frequent logins.

9.3 Middleware Performance Optimization

  • Edge Middleware: Move non-sensitive authentication logic (such as token existence check) to edge nodes to reduce server load.
  • Cache Control: Skip middleware processing for static resources (such as images, CSS) (excluded through config.matcher).

9.4 Error Handling and Logging

  • Catch JWT parsing errors in the middleware and record detailed logs (it is recommended to use monitoring tools such as Sentry in the production environment).
  • Display general error messages to the client (such as "Authentication failed") to avoid revealing technical details.

9.5 Refresh Token Mechanism (Expansion)

  1. When the user logs in, return both the JWT and the refresh token (stored in a separate HttpOnly Cookie).
  2. When the JWT expires, use the refresh token to request a new JWT from the server without having to log in again.
// Refresh token API example (app/api/auth/refresh/route.ts)
export async function POST(request: Request) {
   const refreshToken = parseAuthCookie(request.headers.get('cookie'));
  // Verify the refresh token (needs to be stored in the server-side database or Redis)
  // Generate a new JWT and return it
}

X. Common Problems and Solutions

10.1 The middleware does not work?

  • Check the file location of middleware.ts (it needs to be in the root directory of the project).
  • Confirm whether the config.matcher matching rules are correct (use absolute paths or regular expressions).

10.2 The Cookie is not carried in the request?

  • Ensure that the path of the Cookie set during login is '/'.
  • When Secure: true is enabled in the production environment, it must be accessed via HTTPS.

10.3 How can client components obtain user information?

  • Obtain it through the server-side API interface (such as /api/auth/user), and avoid directly parsing client Cookies (to prevent XSS).

10.4 The token verification fails in the edge middleware?

  • The edge environment does not support Node.js native modules. Ensure that jsonwebtoken uses a pure JavaScript implementation (such as the ES6 version of the jose library).

XI: Security Enhancement Measures

  • Use HttpOnly + Secure Cookies

  • Set a reasonable SameSite policy

  • The JWT validity period should not exceed 2 hours

  • Rotate encryption keys regularly

  • Implement CSRF protection

XII. Conclusion: Building a Secure and Reliable Authentication System

By combining Next.js middleware with JWT, we have implemented a complete authentication and authorization system, with core advantages including:

  1. Stateless Authentication: The server does not need to store sessions, supporting high concurrency and microservice architectures.
  2. Fine-grained Route Protection: Flexibly configure the routes that need to be protected through config.matcher.
  3. Security Enhancement: Reasonably use Cookie security attributes and short token expiration times to reduce the attack surface.
  4. Good Scalability: Supports the subsequent addition of features such as role-based access control (RBAC) and multi-factor authentication (MFA).

In actual projects, the middleware logic needs to be adjusted according to business requirements (such as permission level judgment), and strict compliance with security best practices is required. The middleware mechanism of Next.js not only simplifies the authentication process but also provides a unified request processing layer for building full-stack applications, which is an indispensable tool in modern web development.

Through the practice in this article, developers can master the entire process from JWT generation, middleware verification to user information extraction, laying a solid foundation for developing more complex authentication systems in the future. Always remember: security is a layered system. In addition to code implementation, it also needs to be combined with infrastructure (such as HTTPS), monitoring systems (such as abnormal login detection), and user education (such as strong password policies) to build a truly reliable application.

Leapcell: The Best of Serverless Web Hosting

Finally, I would like to recommend a platform that is most suitable for deploying services: Leapcell

Image description