JWT Authentication in Next.js — Step-by-Step Guide

Authentication is a fundamental requirement in modern web applications. This guide walks through building a simple and secure JWT (JSON Web Token) authentication system in Next.js using the App Router.


What is JWT?

A JSON Web Token (JWT) is a compact, URL-safe string used to securely transmit information between a client and a server.

It consists of three parts:

  • Header

  • Payload

  • Signature

After a successful login, the server generates a token and sends it to the client. The client includes this token in subsequent requests for authentication.





Project Setup

Create a new Next.js application and install the required package:

npx create-next-app@latest jwt-auth-app
cd jwt-auth-app
npm install jsonwebtoken

Step 1: Add Secret Key

Create a .env.local file in the root of your project:

JWT_SECRET=your_super_secret_key

Step 2: Create Login API (Generate Token)

Create the login route:

app/api/login/route.js


export async function POST(req) {
  const body = await req.json();
  console.log("BODY:", body);

  const { username, password } = body;

  if (username !== "admin" || password !== "123") {
    console.log("Invalid credentials");
    return new Response(JSON.stringify({ error: "Invalid" }), { status: 401 });
  }

  console.log("Login success");
  return new Response(JSON.stringify({ message: "ok" }));
}
or

import jwt from "jsonwebtoken"; import { NextResponse } from "next/server"; export async function POST(req) { const { username, password } = await req.json(); // Replace with database validation if (username !== "admin" || password !== "123") { return NextResponse.json({ error: "Invalid credentials" }, { status: 401 }); } const token = jwt.sign( { username, role: "admin" }, process.env.JWT_SECRET, { expiresIn: "1h" } ); const response = NextResponse.json({ message: "Login successful" }); response.cookies.set("token", token, { httpOnly: true, secure: true, path: "/", }); return response; }

Step 3: Create Protected API

app/api/protected/route.js

import jwt from "jsonwebtoken";
import { NextResponse } from "next/server";

export async function GET(req) {
  const token = req.cookies.get("token")?.value;

  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    return NextResponse.json({
      message: "Protected data",
      user: decoded,
    });
  } catch {
    return NextResponse.json({ error: "Invalid token" }, { status: 401 });
  }
}

Step 4: Add Middleware Protection

Create a middleware file at the root:

middleware.js

import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";

export function middleware(req) {
  const token = req.cookies.get("token")?.value;

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

  try {
    jwt.verify(token, process.env.JWT_SECRET);
    return NextResponse.next();
  } catch {
    return NextResponse.redirect(new URL("/login", req.url));
  }
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Step 5: Create Login Page

app/login/page.js

"use client";

import { useState } from "react";

export default function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const handleLogin = async () => {
    setLoading(true);
    setError("");

    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ username, password }),
      headers: {
        "Content-Type": "application/json",
      },
    });

    if (!res.ok) {
      setError("Invalid credentials");
      setLoading(false);
      return;
    }

    setLoading(false);
    window.location.href = "/dashboard";
  };

  return (
    <div style={styles.container}>
      <h1 style={styles.title}>Login</h1>

      <input
        type="text"
        placeholder="Username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        style={styles.input}
      />

      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        style={styles.input}
      />

      {error && <p style={styles.error}>{error}</p>}

      <button
        onClick={handleLogin}
        disabled={loading}
        style={styles.button}
      >
        {loading ? "Logging in..." : "Login"}
      </button>
    </div>
  );
}

const styles = {
  container: {
    height: "100vh",
    display: "flex",
    flexDirection: "column",
    justifyContent: "center",
    alignItems: "center",
    gap: "12px",
    fontFamily: "sans-serif",
  },
  title: {
    fontSize: "28px",
  },
  input: {
    padding: "10px",
    width: "220px",
    borderRadius: "6px",
    border: "1px solid #ccc",
  },
  button: {
    padding: "10px 20px",
    borderRadius: "6px",
    border: "none",
    backgroundColor: "#2563eb",
    color: "white",
    cursor: "pointer",
  },
  error: {
    color: "red",
    fontSize: "14px",
  },
};

Step 6: Create Protected Page

app/dashboard/page.js

export default function Dashboard() {
  return <h1>Dashboard (Protected)</h1>;
}

Authentication Flow

  1. The user submits login credentials

  2. The server validates the credentials

  3. A JWT is generated and stored in an HTTP-only cookie

  4. Middleware runs on every request to protected routes

  5. If the token is valid, access is granted

  6. If the token is missing or invalid, the user is redirected to the login page


Best Practices

  • Use HTTP-only cookies to prevent client-side access

  • Enable secure: true in production (HTTPS only)

  • Avoid storing sensitive data in the token payload

  • Set expiration times for tokens

  • Use a strong and private secret key


Conclusion

JWT authentication in Next.js provides a stateless and scalable way to secure applications. By combining API routes, cookies, and middleware, you can implement a robust authentication system with minimal complexity.


Comments