DEV Community

Cover image for JWT Authentication in Express.js using Cookies
Sanjokale
Sanjokale

Posted on

JWT Authentication in Express.js using Cookies

JSON Web Tokens (JWTs) are a crutial part of modern web application security. A JWT is an open standard(RFC 7519) that defines a compact and self-contained way transmitting information between parties as a JSON object.Essentially, it's a string of characters that carries information.

  • Structure: It consist of three parts, seperated by dot(.):-
  1. Header: Contains metadata about the token, such as the signing algorithm.

  2. Payload: Contains the "claims," which are statements about an entity (e.g., a user) and additional data.

  3. Signature: Used to verify that the token hasn't been tampered with.

Image description

  • Uses
  1. Authenticaiton: When a user logs in, the server can generate a JWT and send it to the client.The client then includes this JWT in subsequent requests, allowing the server to verify the user's identity without needing to store session information on the server itself (stateless authentication).

  2. Authorization: JWTs can contain information about a user's permissions, allowing the server to determine whether the user is authorized to access certain resources.

  3. Information Exchange: Because JWTs can be signed, they provide a secure way to transmit information between parties, ensuring that the information hasn't been modified.

  4. Statelessness: One of the key benefits of JWTs is that they enable stateless authentication. This means that the server doesn't need to maintain session data, which can improve scalability.  

Implementing JWT authenticaiton in an Express.js application involves several key steps:-

However, there are other ways to send the jwt to the forntend and today i will teach you how to store the jwt in a cookie.
Here I use cookies because sometimes I'm don't feel like constantly sending the jwt in the headers whenever i make a request to the Api. This is where cookies come in, you can send them whenever you make an http request without worry.

  • Install Dependencies:

npm i bcrypt cookie-parser jsonwebtoken dotenv

  • configure cookieParser in index.js
//index.js
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import dotenv from "dotenv";
import connectDB from "./db/index.js";

dotenv.config({
  path: "./.env",
});

const app = express();

const port = 8080

//app.use() is use to set the configurations and the middlewares
//middleware helps to changes to the request and the response objects
//middleware can end the request-response cycle
//middleware call the next middleware function in the stack

app.use(
  cors({
    origin: process.env.CORS_ORIGIN,
    credentials: true,
  })
);

connection();  //connection database to the server

app.use(express.json({ limit: "16kb" }));
app.use(express.urlencoded({ extended: true, limit: "16kb" })); //extended helps us to pass the nested object
app.use(cookieParser()); cookie configuration in the app

//routes imports
import userRouter from "./routes/user.routes.js";

//router declaration
app.use("/api/v1/users", userRouter);

app.listen(port, ()=>{
    console.log("server is started in port" + port)
})

Enter fullscreen mode Exit fullscreen mode
  • Create a .env file in your root directory and configure your enviroment variables
//.env

PORT = 8080
CORS_ORIGIN = *
ACCESS_TOKEN_SECRET = accesstokensecret
ACCESS_TOKEN_EXPIRY = 10d
REFRESH_TOKEN_SECRET = refreshtokensecret
REFRESH_TOKEN_EXPIRY = 10d
Enter fullscreen mode Exit fullscreen mode
  • Create a user.model.js file in models folder
//user.model.js
import mongoose, { Schema } from "mongoose";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";

const userSchema = new Schema(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
    },
    fullname: {
      type: String,
      required: true,
      trim: true,
      index: true,
    },

   password: {
      type: String,
      required: [true, "password is required"],
    },
    refreshToken: {
      type: String,
    },
  },
);

userSchema.pre("save", async function (next) {
  //here we use normal function instead of arrow function because of arrow function doesnot hold the current context in "this" keyword. here pre is the mogoose hook which is execute before the save method. this is like a middleware which is execute whenever password is changes before save in the database.
  if (!this.isModified("password")) return next();
  this.password = await bcrypt.hash(this.password, 10);  //this method is execute while the password is changes before save in the database
  next();
});

//in mongoose we can inject the custom methods to it just like that middleware
// The userSchema in your Mongoose setup plays a crucial role in defining instance methods, such as isPasswordCorrect.

userSchema.methods.isPasswordCorrect = async function (password) {
  return await bcrypt.compare(password, this.password);
  //In the context of instance methods, this refers to the specific instance of the user document. This is crucial because it allows the method to access instance-specific properties, such as this.password, which contains the hashed password for that particular user.
};

userSchema.methods.generateAccessToken = function () {  //here in the context of instance method this refers to the specific instance of the user document and we can access the instance specific properties. and here we addded the method to the userSchema so that we can access this method to the user instance.
  return jwt.sign(
    {
      _id: this._id,
      email: this.email,
      username: this.username,
      fullname: this.fullname,
    },
    process.env.ACCESS_TOKEN_SECRET,
    {
      expiresIn: process.env.ACCESS_TOKEN_EXPIRY,
    }
  );
};

userSchema.methods.generateRefreshToken = function () {
  return jwt.sign(
    {
      _id: this._id,
    },
    process.env.REFRESH_TOKEN_SECRET,  
    {
      expiresIn: process.env.REFRESH_TOKEN_EXPIRY,
    }
  );
};

export const User = mongoose.model("User", userSchema);

Enter fullscreen mode Exit fullscreen mode
  • Create a user.controller file in controller folder
//user.controller.js
import { User } from "../models/user.model.js";
import jwt from "jsonwebtoken";

const generateAccessAndRefreshTokens = async (userId) => {
  try {
    const user = await User.findById(userId);
    const accessToken = user.generateAccessToken();
    const refreshToken = user.generateAccessToken();

    user.refreshToken = refreshToken;
    await user.save({ validateBeforeSave: false }); //save the refresh token to the user document.
    //The save method in Mongoose is used to persist changes made to a document in the MongoDB database.

    return { accessToken, refreshToken };
  } catch (error) {
    throw new Error(
      "Something Went Wrong while generationg refresh and access token"
    );
  }
};

const registerUser = async (req, res) => {

const { fullname, email, password } = req.body;

  if (
    [fullname, email, password].some((field) => field?.trim() === "")
  ) {
    //The some() method is an iterative method, which means it calls a provided callbackFn function once for each element in an array, until the callbackFn returns a truthy value. If such an element is found, some() immediately returns true and stops iterating through the array. Otherwise, if callbackFn returns a falsy value for all elements, some() returns false.

    throw new Error("All fields are required");
  }

  const existedUser = await User.findOne({ email});

  if (existedUser) {
    throw new Error("Username or Email already exists");
  }

  const user = await User.create({
    fullname,
    email,
    password,
  });

  const createdUser = await User.findById(user._id).select(
    //the select method is not a native JavaScript method. It's a method provided by the Mongoose library, which is a popular ORM (Object Relational Mapping) tool for MongoDB in Node.js.
    "-password -refreshToken"
  );

  if (!createdUser) {
    throw new Error("something went wrong while registering the user");
  }

  return res
    .status(201)
    .send({ msg: "User resgisterd successfully"});
};




const loginUser = async (req, res) => {

  const { email, password } = req.body;


  if (!email) {
    throw new Error("username or password is required");
  }

  //this is the database query
  const user = await User.findOne({ email });

  //here user is mine user that i make it is the instance of User

  if (!user) {
    throw new Error( "User does not exist");
  }

  const isPasswordValid = await user.isPasswordCorrect(password); //instance method to check password is valid or invalid

  if (!isPasswordValid) {
    throw new Error("invalid user credentials");
  }

  const { accessToken, refreshToken } = await generateAccessAndRefreshTokens(
    user._id
  );

  const loggedInUser = await User.findById(user._id).select(
    "-password -refreshToken"
  );

  //send cookies
  const options = {
    httpOnly: true,
    secure: true,
  }; //this help to make cookie modifiable only from server. Generally cookies can modified from both frontend and backend

  return res
    .status(200)
    .cookie("accessToken", accessToken, options) //this line of code help to save the tokens to cookie.
    .cookie("refreshToken", refreshToken, options)
    .send(
        {
          user: loggedInUser,
          accessToken,
          refreshToken,
        },
        )
   };





const logoutUser = async (req, res) => {
  await User.findByIdAndUpdate(
    req.user._id,
    {
      $unset: {
        refreshToken: 1,
      },
    },
    {
      new: true,  //here new is for it return the data after updated.
    }
  );

  const options = {
    httpOnly: true,
    secure: true,
  };
  return res
    .status(200)
    .clearCookie("accessToken", options)  //this line of code helps to remove the cookie data 
    .clearCookie("refreshToken", options)
    .send( {msg:  "User logged Out Successfully"});
};

export {registerUser, loginUser, logoutUser}
Enter fullscreen mode Exit fullscreen mode
  • create create as auth.middleware file in the middlewares folder. This middleware will check if the request has a valid JWT in its header.
//auth.middleware.js
import { User } from "../models/user.model.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import jwt from "jsonwebtoken";

export const verifyJWT = async (req, _, next) => {
  try {
    const token =
      req.cookies?.accessToken ||
      req.header("Authorization")?.replace("Bearer ", ""); //replace method delete the substring ofstring

    if (!token) {
      throw ApiError(401, "Unauthorized request");
    }

    const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);

    const user = await User.findById(decodedToken?._id).select(
      "-password -refreshToken"
    );

    if (!user) {
      //next video discuss about frontend
      throw new Error( "Invalid Access Token");
    }

    req.user = user;  //giving user information to the req object.****
    next();
  } catch (error) {
    throw new Error( "Invalid Access Token");
  }
};

Enter fullscreen mode Exit fullscreen mode
  • Create a user.route.js file inside the routes folder
//user.route.js
import { Router } from "express";
import {
  loginUser,
  logoutUser,
  registerUser,
} from "../controllers/user.controller.js";
import { verifyJWT } from "../middlewares/auth.middleware.js";


const router = Router();

router.route("/register").post(registerUser);

router.route("/login").post(loginUser);

//secured routes --> protected router 
//The protected route uses verifyToken to ensure that only authenticated users can access it.
router.route("/logout").post(verifyJWT, logoutUser); // user can only logout if user is verifyjwt authenticated.

export default router;

Enter fullscreen mode Exit fullscreen mode

`make sure you have imort cookieParser from the "cookie-parser" and set configuration or middleware like app.use(cookieParser) in the index.js file.

This detailed explaination and code blocks give you a very good starting point for implementing JWT authentication in your Express applications.

Top comments (0)