Implementing JWT Authentication in a Next.js Application

Introduction

JSON Web Tokens (JWT) are a popular method for handling authentication in web applications. JWTs are compact, URL-safe tokens that can be used to securely transmit information between parties. This article provides a step-by-step guide on implementing JWT authentication in a Next.js application, including generating and verifying tokens, handling user login, and protecting routes.

Setting Up the Project
 

Create a New Next.js Project

If you haven’t already set up a Next.js project, create one using.

npx create-next-app@latest my-next-app
cd my-next-app

Install Required Packages

You’ll need a few packages to handle JWTs and manage environment variables.

npm install jsonwebtoken bcryptjs dotenv
  • jsonwebtoken: Library to sign and verify JWTs.
  • bcryptjs: Library to hash passwords.
  • dotenv: Library to manage environment variables.

Setting up Environment Variables

Create a .env.local file at the root of your project to store your secret key and other sensitive information.

JWT_SECRET=your-secret-key

Replace your secret key with a strong, random string.

Creating Utility Functions
 

JWT Utility Functions

Create a file named auth.js in the lib directory to manage JWT operations.

mkdir lib
touch lib/auth.js

Add the following code to auth.js.

// lib/auth.js
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
export function signToken(user) {
  return jwt.sign(user, SECRET, { expiresIn: '1h' });
}
export function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET);
  } catch (err) {
    return null;
  }
}

Password Hashing Utility

Add password hashing functionality in auth.js.

import bcrypt from 'bcryptjs';
export async function hashPassword(password) {
  const salt = await bcrypt.genSalt(10);
  return bcrypt.hash(password, salt);
}
export async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash);
}

Setting Up API Routes
 

User Registration

Create a registration route to handle user registration and password hashing.

mkdir pages/api
touch pages/api/register.js

Add the following code to register.js.

// pages/api/register.js
import { hashPassword } from '../../lib/auth';
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { username, password } = req.body;
    // Validate input
    if (!username || !password) {
      return res.status(400).json({ error: 'Missing username or password' });
    }
    // Hash the password
    const hashedPassword = await hashPassword(password);
    // Save user to the database (pseudo-code)
    // await saveUserToDatabase(username, hashedPassword);
    res.status(201).json({ message: 'User registered' });
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

User Login

Create a login route to handle user authentication and token generation.

touch pages/api/login.js

Add the following code to login.js.

// pages/api/login.js
import { signToken, verifyPassword } from '../../lib/auth';

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { username, password } = req.body;
    // Validate input
    if (!username || !password) {
      return res.status(400).json({ error: 'Missing username or password' });
    }
    // Fetch user from the database (pseudo-code)
    // const user = await fetchUserFromDatabase(username);
    // Verify password (pseudo-code)
    // const isPasswordValid = await verifyPassword(password, user.password);
    if (/* user exists and password is valid */) {
      // Generate JWT token
      const token = signToken({ username });
      res.status(200).json({ token });
    } else {
      res.status(401).json({ error: 'Invalid credentials' });
    }
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Protected Route

Create a protected API route that requires authentication.

touch pages/api/protected.js

Add the following code to protected.js.

// pages/api/protected.js
import { verifyToken } from '../../lib/auth';

export default function handler(req, res) {
  if (req.method === 'GET') {
    const { authorization } = req.headers;
    if (authorization && authorization.startsWith('Bearer ')) {
      const token = authorization.replace('Bearer ', '');
      const user = verifyToken(token);
      if (user) {
        res.status(200).json({ message: 'Protected data', user });
      } else {
        res.status(401).json({ error: 'Invalid token' });
      }
    } else {
      res.status(401).json({ error: 'No token provided' });
    }
  } else {
    res.setHeader('Allow', ['GET']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Client-Side Authentication
 

Login Page

Create a login page to allow users to authenticate.

touch pages/login.js

Add the following code to login.js.

// pages/login.js
import { useState } from 'react';

export default function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const handleLogin = async () => {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ username, password }),
    });
    const { token } = await res.json();

    if (token) {
      localStorage.setItem('token', token);
      // Redirect or show success message
    } else {
      // Handle login error
    }
  };
  return (
    <div>
      <h1>Login</h1>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button onClick={handleLogin}>Login</button>
    </div>
  );
}

Protected Page

Create a protected page that requires authentication.

touch pages/protected.js

Add the following code to protected.js.

// pages/protected.js
import { useEffect, useState } from 'react';
export default function ProtectedPage() {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const token = localStorage.getItem('token');
      const res = await fetch('/api/protected', {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const result = await res.json();
      setData(result);
    };

    fetchData();
  }, []);

  return (
    <div>
      <h1>Protected Page</h1>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}
    </div>
  );
}

Best Practices

  • Secure Your JWT Secret: Keep your JWT secret key secure, and do not expose it in your codebase.
  • Token Expiration: Set an appropriate expiration time for tokens and handle token renewal or refreshing.
  • HTTPS: Ensure your application uses HTTPS to protect token transmissions.
  • Error Handling: Implement proper error handling and user feedback mechanisms for authentication failures.

Summary

Implementing JWT authentication in a Next.js application provides a secure and efficient way to handle user authentication and authorization. By creating utility functions for JWT operations, setting up registration and login routes, and protecting API routes, you can build a robust authentication system. Proper handling of tokens and security practices will ensure a secure application environment.