JWT Authentication With Next.js

JWT Authentication With Next.js
Photo by Matthew Henry / Unsplash

To use JSON Web Tokens (JWT) for authentication in a Next.js application, you can use a combination of server-side and client-side code to handle the login and logout processes, as well as checking for the presence of the JWT token on every page.

  1. On the server-side, you can create a route to handle login requests and use the jsonwebtoken package to create a JWT token for the user. You can then send the token back to the client and store it in an httpOnly and secure cookie.
  2. On the client-side, you can create a form that sends a login request to the server, and then stores the token in a cookie when it is received. You can also create a logout button that clears the token from the cookie.
  3. You can create a middleware function, that will be executed on every page, with the token checking and redirection logic. In your _app.js file you can use this middleware function to check if the token is present and if not, redirect the user to the login page.
  4. To log out the user, you can clear the token from the cookie and redirect the user to the login page.
  5. In your api routes, you can use a middleware function to check the token before handling any request and check that it's still valid and was not tampered with, then allow or deny access to the specific route.

Here is an example of how you might implement JWT-based authentication in a Next.js application, with server-side and client-side code for handling the login, logout, and token checking processes:

Server-side (api/login.js)

import jwt from 'jsonwebtoken';

const secret = 'yoursecret';

export default async (req, res) => {
  try {
    // Validate the login credentials
    const { email, password } = req.body;
    // Check if the user is valid and exists
    const user = await checkUserCredentials(email, password);
    if (!user) {
      res.status(401).json({ message: 'Invalid email or password' });
      return;
    }
    // Create the JWT token
    const token = jwt.sign({ id: user.id }, secret);
    // Set the token in a `token` cookie
    res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure`);
    res.status(200).json({ message: 'Logged in' });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: 'An error occurred, please try again later' });
  }
}

Server-side (api/logout.js)

export default (req,res) => {
  res.setHeader("Set-Cookie", "token= ; expires = Thu, 01 Jan 1970 00:00:00 UTC; path=/;")
  res.status(200).json({ message: 'Logged out' });
}

Middleware function (lib/middleware/auth.js)

import jwt from 'jsonwebtoken';

const secret = 'yoursecret';

export default function authenticate(req, res, next) {
  try {
    const token = req.headers.cookie.split("token=")[1];
    if (!token) throw new Error("User is not logged in");
    const decoded = jwt.verify(token, secret);
    req.user = decoded;
    next();
  } catch (err) {
    console.error(err);
    res.redirect("/login");
  }
}

On each protected page, you can use getServerSideProps or getInitialProps to check the presence of the JWT token and redirect the user to the login page if the token is not found:

import authenticate from '../../lib/middleware/auth';

export const getServerSideProps = async (ctx) => {
    authenticate(ctx.req, ctx.res);
    ...
}

On the client-side, you can create a Login component that sends a login request to the server, and then stores the token in a cookie when it is received.

Client-side (components/Login.js)

import { useState } from 'react';
import fetch from 'isomorphic-unfetch';
import { useRouter } from 'next/router'

export default function Login() {
  const [error, setError] = useState('');
  const router = useRouter();
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    try {
      const res = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email.value, password: password.value }),
      });
      if (res.status === 401) {
        setError('Invalid email or password');
        return;
      }
      router.push('/')
    } catch (error) {
      console.error(error);
      setError('An error occurred, please try again later');
    }
  };
  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" />
      <input type="password" name="password" placeholder="Password" />
      <button type="submit">Log in</button>
      {error && <p>{error}</p>}
    </form>
  );
}

You can also create a LogoutButton component that clears the token from the cookie and redirects the user to the login page

Client-side (components/LogoutButton.js)

import { useRouter } from 'next/router'

export default function LogoutButton() {
  const router = useRouter();
  const handleClick = () => {
    // Clear the token from the cookie
    document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    // Redirect the user to the login page
    Router.push('/login');
}

It is important to remember that using JWT has some security consideration, for example, JWT is stateless, so it doesn't depend on server-side storage. And also, cookies are vulnerable to cross-site request forgery (CSRF) attacks, it's important to validate the origin of the requests to prevent it.