No menu items!

How to Implement Token Authentication in Next.js Using JWTs

Token authentication is a popular strategy used in safeguarding web and mobile applications from unauthorized access. In Next.js, you can utilize the authentication features provided by Next-auth.


Alternatively, you can opt to develop a custom token-based authentication system using JSON Web Tokens (JWTs). By doing so, you ensure that you have more control over the authentication logic; essentially, customizing the system to precisely match the requirements of your project.


Set Up a Next.js Project

To get started, install Next.js by running the command below on your terminal.

 npx create-next-app@latest next-auth-jwt --experimental-app 

This guide will utilize Next.js 13 which includes the app directory.

Next.js 13 project folder structure on VS Code.

Next, install these dependencies in your project using npm, the Node Package Manager.

 npm install jose universal-cookie 

Jose is a JavaScript module that provides a set of utilities for working with JSON Web Tokens while the universal-cookie dependency provides a simple way to work with browser cookies in both the client-side and server-side environments.

Create the Login Form User Interface

Open the src/app directory, create a new folder, and name it login. Inside this folder, add a new page.js file and include the code below.

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

export default function LoginPage() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" name="username" />
      </label>
      <label>
        Password:
        <input type="password" name="password" />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

The code above creates a login page functional component that will render a simple login form on the browser to allow users to enter a username and a password.

The use client statement in the code ensures that a boundary is declared between server-only and client-only code in the app directory.

In this case, it’s used to declare that the code in the login page, particularly, the handleSubmit function is only executed on the client; otherwise, Next.js will throw an error.

Now, let’s define the code for the handleSubmit function. Inside the functional component, add the following code.

 const router = useRouter();

const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const username = formData.get("username");
    const password = formData.get("password");
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ username, password }),
    });
    const { success } = await res.json();
    if (success) {
      router.push("/protected");
      router.refresh();
    } else {
      alert("Login failed");
    }
 };

To manage the login authentication logic, this function captures the user credentials from the login form. It then sends a POST request to an API endpoint passing along the user details for verification.

If the credentials are valid, indicating the login process was successful—the API returns a success status in the response. The handler function will then use Next.js’ router to navigate the user to a specified URL, in this case, the protected route.

Define the Login API Endpoint

Inside the src/app directory, create a new folder and name it api. Inside this folder, add a new login/route.js file and include the code below.

 import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";

export async function POST(request) {
  const body = await request.json();
  if (body.username === "admin" && body.password === "admin") {
    const token = await new SignJWT({
      username: body.username,
    })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("30s")
      .sign(getJwtSecretKey());
    const response = NextResponse.json(
      { success: true },
      { status: 200, headers: { "content-type": "application/json" } }
    );
    response.cookies.set({
      name: "token",
      value: token,
      path: "https://www.makeuseof.com/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

The primary task for this API is to verify the login credentials passed in the POST requests using mock data.

Upon successful verification, it generates an encrypted JWT token associated with the authenticated user details. Finally, it sends a successful response to the client, including the token in the response cookies; otherwise, it returns a failure status response.

Implement Token Verification Logic

The initial step in token authentication is generating the token after a successful login process. The next step is to implement the logic for token verification.

Essentially, you will use the jwtVerify function provided by the Jose module to verify the JWT tokens passed with subsequent HTTP requests.

In the src directory, create a new libs/auth.js file and include the code below.

 import { jwtVerify } from "jose";

export function getJwtSecretKey() {
  const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
  if (!secret) {
    throw new Error("JWT Secret key is not matched");
  }
  return new TextEncoder().encode(secret);
}

export async function verifyJwtToken(token) {
  try {
    const { payload } = await jwtVerify(token, getJwtSecretKey());
    return payload;
  } catch (error) {
    return null;
  }
}

The secret key is used in signing and verifying the tokens. By comparing the decoded token signature to the expected signature, the server can effectively verify that the provided token is valid, and ultimately, authorize the users’ requests.

Create .env file in the root directory and add a unique secret key as follows:

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

Create a Protected Route

Now, you need to create a route that only authenticated users can gain access to. To do so, create a new protected/page.js file in the src/app directory. Inside this file, add the following code.

 export default function ProtectedPage() {
    return <h1>Very protected page</h1>;
  }

Create a Hook to Manage the Authentication State

Create a new folder in the src directory and name it hooks. Inside this folder add a new useAuth/index.js file and include the code below.

 "use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";

export function useAuth() {
  const [auth, setAuth] = React.useState(null);

  const getVerifiedtoken = async () => {
    const cookies = new Cookies();
    const token = cookies.get("token") ?? null;
    const verifiedToken = await verifyJwtToken(token);
    setAuth(verifiedToken);
  };
  React.useEffect(() => {
    getVerifiedtoken();
  }, []);
  return auth;
}

This hook manages the authentication state on the client side. It fetches and verifies the validity of the JWT token present in cookies using the verifyJwtToken function, and then sets the authenticated user details to the auth state.

By doing so, it allows other components to access and utilize the authenticated user’s information. This is essential for scenarios like making UI updates based on authentication status, making subsequent API requests, or rendering different content based on user roles.

In this case, you’ll use the hook to render different content on the home route based on the authentication state of a user.

An alternative approach you might consider is handling state management using Redux Toolkit or employing a state management tool like Jotai. This approach guarantees components can get global access to the authentication state or any other defined state.

Go ahead and open the app/page.js file, delete the boilerplate Next.js code, and add the following code.

 "use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
           <h1>Public Home Page</h1>
           <header>
              <nav>
                {auth ? (
                   <p>logged in</p>
                ) : (
                  <Link href="/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

The code above utilizes the useAuth hook to manage the authentication state. In doing so, it conditionally renders a public home page with a link to the login page route when the user is not authenticated, and displays a paragraph for an authenticated user.

Add a Middleware to Enforce Authorized Access to Protected Routes

In the src directory, create a new middleware.js file, and add the code below.

 import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";

const AUTH_PAGES = ["/login"];

const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));

export async function middleware(request) {

  const { url, nextUrl, cookies } = request;
  const { value: token } = cookies.get("token") ?? { value: null };
  const hasVerifiedToken = token && (await verifyJwtToken(token));
  const isAuthPageRequested = isAuthPages(nextUrl.pathname);

  if (isAuthPageRequested) {
    if (!hasVerifiedToken) {
      const response = NextResponse.next();
      response.cookies.delete("token");
      return response;
    }
    const response = NextResponse.redirect(new URL(`/`, url));
    return response;
  }

  if (!hasVerifiedToken) {
    const searchParams = new URLSearchParams(nextUrl.searchParams);
    searchParams.set("next", nextUrl.pathname);
    const response = NextResponse.redirect(
      new URL(`/login?${searchParams}`, url)
    );
    response.cookies.delete("token");
    return response;
  }

  return NextResponse.next();

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

This middleware code acts as a guard. It checks to ensure that when users want to access protected pages, they are authenticated and authorized to access the routes, in addition to, redirecting unauthorized users to the login page.

Securing Next.js Applications

Token authentication is an effective security mechanism. However, it’s not the only strategy available to safeguard your applications from unauthorized access.

To fortify applications against the dynamic cybersecurity landscape, it’s important to adopt a comprehensive security approach that holistically addresses potential security loopholes and vulnerabilities to guarantee thorough protection.

Related

How to Use ChatGPT as a Detailed and Interactive Text-Based RPG

OpenAI’s ChatGPT is arguably the most advanced AI currently...

4 New Threats Targeting Macs in 2023 and How to Avoid Them

The past decade has witnessed a drastic change in...

What Are Improper Error Handling Vulnerabilities?

Do you know that little things like the errors...

5 AI-Powered Book Recommendation Sites and Apps to Find Your Next Read

Can ChatGPT find the best next book that you...

What Is Forefront AI and Is It Better Than ChatGPT?

Key Takeaways Forefront AI is an online...