Create secure authentication using HTTP-only cookies in Express.js

First, install bcrypt and jsonwebtoken as dependencies and @types/bcrypt and @types/jsonwebtoken as dev dependencies in your Express.js app. We'll use bcrypt to hash passwords and jsonwebtoken to create secure tokens in our Express server.

Once installed, run openssl rand -base64 32 in your terminal to create a random string, copy the string, and create an environment variable named JWT_SECRET in your project. After that, within your /lib/utils directory, create a setCookie function with the following code:

import { Types } from 'mongoose';
import { Response } from 'express';
import jwt from 'jsonwebtoken';

export function setCookie(res: Response, _id: Types.ObjectId) {
  const jwtToken = jwt.sign({ _id }, process.env.JWT_SECRET as string, {
    expiresIn: '7d',
  });

  res.cookie('token', jwtToken, {
    path: '/',
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
    secure: process.env.NODE_ENV !== 'development',
  });
}

The above function accepts two parameters. The res parameter is the Express Response object, and the _id parameter is the user ID which we'll store in the JSON web token.

Within the function, we create a jwtToken variable by using the jwt.sign method. This method accepts three parameters. The first parameter is the information we want to store in the token, the second parameter is the JWT secret, and the third parameter is an options object. In the options object, we set expiresIn to '7d', which expires the token in a week.

After that, we use the res.cookie method to set the cookie to the response header. This method accepts three parameters. The first parameter is the cookie name, the second parameter is the cookie content, and the third parameter is an options object.

The options object is important as we set the security parameters here. Within this object, we set the value of path to /, which defines the scope of the cookie. Next, we set the value of httpOnly to true, which makes the cookie inaccessible by client-side JavaScript. After that, we set the value of sameSite to strict, which means that the browser only sends the cookie with requests from the cookie's origin server. Next, we set the value of maxAge to seven days to match our token expiration. Finally, we set the value of secure to true when the Node Environment is 'production'.

Now, within your routes directory, create a user.ts file with the following code:

import { Router } from 'express';

const router = Router();

export default router;

Then, navigate to your app.ts file, import the user router, and add it as the middleware like the following code:

import User from './routes/user';

app.use('/users', User);

Now, get back to the user router and create a /register route by adding the following code:

import bcrypt from 'bcrypt';
import User from '../models/user';
import { setCookie } from '../lib/utils';

router.post('/register', async (req, res) => {
  const { name, email, password } = req.body;
  if (!name || !email || !password) {
    console.log('Name, email, or password is missing');
    res.status(400);
    throw new Error('Name, email, or password is missing');
  }

  try {
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    const response = await User.create({
      name,
      email,
      password: hashedPassword,
    });
    const user = response.toObject();

    setCookie(res, user._id);
    res.status(201).json(user);
  } catch (err) {
    console.log(err);
    throw err;
  }
});

In this code, within the controller function, we destructure the name, email, and password from the request body and check if all three fields are provided.

After that, we create a salt which is required to hash the password. Next, we hash the password by using the bcrypt.hash method. After that, we create a user in the database, convert the response to an object, set the cookie to the response header, and send the user with the response.

Now, let's create a /login route with the following code:

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) {
    console.log('Email or password is missing');
    res.status(400);
    throw new Error('Email or password is missing');
  }

  try {
    const user = await User.findOne({ email });
    if (!user) {
      console.log('Invalid credentials');
      res.status(403);
      throw new Error('Invalid credentials');
    }

    const isCorrectPassword = await bcrypt.compare(password, user.password);
    if (!isCorrectPassword) {
      console.log('Invalid credentials');
      res.status(403);
      throw new Error('Invalid credentials');
    }

    setCookie(res, user._id);
    res.status(200).json(user);
  } catch (err) {
    console.log('Invalid credentials');
    res.status(403);
    throw new Error('Invalid credentials');
  }
});

In the above code, within the controller function, we destructure email and password from the request body and check if both fields are provided. Next, we do a database query to find the user with the provided email address. If no user is found, we throw an error. Otherwise, we compare the provided password with the hashed password using the bcrypt.compare method. If the passwords don't match, we throw an error. Otherwise, we use the setCookie function to set the cookie to the response header and send the user with the response.

Finally, we need a route to log out a user. So, let's create a /logout route with the following code:

router.post('/logout', async (req, res) => {
  res
    .clearCookie('token', {
      path: '/',
      maxAge: 0,
      httpOnly: true,
      sameSite: 'strict',
      secure: process.env.NODE_ENV !== 'development',
    })
    .end();
});

In the above code, we use the res.clearCookie method to remove a cookie from the response header. This method accepts two parameters. The first parameter is the name of the cookie you want to remove. The second parameter is an options object. The options object must be identical to the options object we've used in the setCookie function. Except, we set the value of the maxAge to '0' here which invalidates the cookie. Finally, we chain the end method to end the response.

That's it! Your Express app now has a secure authentication system. Check out the next article of this series, Protect API routes using the auth middleware in Express.js to learn about creating an auth middleware to parse cookies and protecting API routes in your Express server. Cheers!

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!