All posts
Published in javascript

Nodejs JWT Authentication With HTTP Only Cookie

Profile image of Atakan Demircioğlu
By Atakan Demircioğlu
Fullstack Developer

What is HTTP Only Cookie?

Nodejs JWT Authentication With HTTP Only Cookie image 1

What is HTTP Only Cookie?

  • HttpOnly is an additional flag included in a Set-Cookie HTTP response header.
  • If the HttpOnly flag is included in the HTTP response header, the cookie cannot be accessed through the client-side script (if the browser supports it).
  • It is not accessible with document. cookie with JS and you can just send it to the server. (if the browser supports it).
  • As a result, even if a cross-site scripting (XSS) flaw exists, and a user accidentally accesses a link that exploits this flaw, the browser will not reveal the cookie to a third party.
  • If a browser does not support HttpOnly and a website attempts to set an HttpOnly cookie, the HttpOnly flag will be ignored by the browser, thus creating a traditional, script-accessible cookie.

What is JWT? (JSON Web Token)

  • JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
  • This information can be verified and trusted because it is signed digitally.
  • JWT tokens can be signed with a secret.
  • You can use JWT for Authorization or Information Exchange.

Real Word Node.js Application

Let's assume you have a basic login page and you are sending a username and password to an endpoint named as /auth or etc.

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
import HTTPMethod from 'http-method-enum';
import HTTP_STATUS_CODES from 'http-status-enum';
import { serialize } from 'cookie';
const prisma = new PrismaClient();
const KEY = process.env.JWT_KEY;
export default async function authenticate(req, res) {
  const { method } = req;
  try {
    switch (method) {
      case HTTPMethod.POST:
        
        const { email, password } = req.body;
        if (!email || !password) {
          return res.status(HTTP_STATUS_CODES.BAD_REQUEST).json({
            status: 'error',
            error: 'Request missing email or password',
          });
        }
        const user = await prisma.user.findUnique({
          where: {
            email: email,
          },
        });
        if (!user) {
          return res
            .status(HTTP_STATUS_CODES.BAD_REQUEST)
            .json({ status: 'error', error: 'User Not Found' });
        }
        bcrypt.compare(password, user.password).then((isMatch) => {
          if (isMatch) {
            const payload = {
              id: user.id,
              email: user.email,
              createdAt: user.createdAt,
              username: user.username,
              fullname: user.fullname,
            };
            jwt.sign(
              payload,
              KEY,
              {
                expiresIn: 60 * 60 * 24 * 30,
              },
              (_err, token) => {
                const serialized = serialize('token', token, {
                  httpOnly: true,
                  secure: process.env.NODE_ENV === 'production',
                  sameSite: 'strict',
                  maxAge: 60 * 60 * 24 * 30,
                  path: '/',
                });
                res.setHeader('Set-Cookie', serialized);
                res.status(HTTP_STATUS_CODES.OK).json({
                  success: true,
                  user: {
                    email: payload.email,
                    username: payload.username,
                    fullname: payload.fullname,
                  },
                });
              },
            );
          } else {
            res.status(HTTP_STATUS_CODES.BAD_REQUEST).json({
              status: 'error',
              error: 'Password and email does not match.',
            });
          }
        });
        break;
    }
  } catch (error) {
    console.log(error);
    res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({
      status: 'error',
      error: 'Internal Server Error',
    });
  }
}
  • You can create the file under /api/ named as auth.js
  • I am just prepared a basic example at it includes Prisma (ORM), bcrypt and etc. (maybe you don’t need it)
  • Line 46: We are signing the payload with our secret (stored in the .env file)
  • Line 53: We serialize the token with some settings.
  • httpOnly means this is a HTTP only cookie, secure means HTTPS (in development just use HTTP, maxAge is for giving an expiration time)
  • sameSite: ‘strict’ : Cookies will only be sent in a first-party context and not be sent along with requests initiated by third-party websites.
  • Line 60: Setting HTTP-only cookie with HTTP response header

So how about the logout process?

import { serialize } from 'cookie';
export default function logout(req, res) {
  const { cookies } = req;
  const jwt = cookies.token;
  if (!jwt) {
    return res.status(401).json({
      status: 'error',
      error: 'Unauthorized',
    });
  }
  const serialized = serialize('token', null, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: -1,
    path: '/',
  });
  res.setHeader('Set-Cookie', serialized);
  res.status(200).json({
    status: 'success',
    message: 'Logged out',
  });
}

It is a little different but the logic is the same. We just need an endpoint like /logout and just need to set up maxAge -1 or just 0.

I just tried to explain the logic with a basic example. You can improve the security part or the logic whatever you want.

You can also my article about the usage of next-js middleware.

References

Check these articles: