DEV Community

Alahira Jeffrey Calvin
Alahira Jeffrey Calvin

Posted on • Updated on

Step by Step Guide to Authentication with JSON Web Tokens (JWT) with express and passport

Introduction

Authentication is a crucial part of web development. It is one of the ways developers ensure the security of an application. Though authentication and authorization are often used interchangeably, they are not the same thing. Authentication is simply the process of determining who a user is while authorization on the other hand is the process of determining if a user has access to a particular resource he/she is requesting.

JSON Web Tokens have become a popular choice for implementing authentication in web applications due to their simplicity, ease of implementation, and effectiveness.

Prerequisites

Before we begin, ensure you have Node.js and npm (Node Package Manager), MongoDB, and Postman installed locally. A little knowledge of the tools listed above is also required.

Project setup

  • Start by creating a directory for the project. Open your terminal and type the command mkdir passport-jwt-express-auth.

  • Navigate to the directory using the command cd passport-jwt-express-auth.

  • Initialize a node project using the command npm init -y

  • Install the dependencies by running npm install express jsonwebtoken nodemon passport passport-jwt bcrypt dotenv mongoose.

  • bcrypt would be used to hash the user passwords, jsonwebtoken would be used for signing tokens, passport-jwt would be used for retrieving and verifying tokens, nodemon would be used automatically restart the server during development, dotenv would be used to access environment variables, mongoose would be used as the MongoDB ODM and express would be used create the server.

  • Create a .env file at the root of the project to hold the project environment variables. The main environment variables we would use in the project would be JWT_SECRET, PORT, and MONGO_URI.

  • JWT_SECRET would hold the secret which our application would use to sign tokens, PORT would hold the port number in which we would serve our application, MONGO_URI would hold the link to the Mongodb database.

  • Open the package.json file and add the following.

  • "type": "module" This would enable you to use es6 imports in the project. Next, add the following under the scripts section

"start:dev": "nodemon src/app.js",
"start:prod": "node src/app.js"
Enter fullscreen mode Exit fullscreen mode

These scripts would be used to start the express server.

  • Create a folder to hold the source code using mkdir src.

  • Navigate to the folder using the command cd src and create four files and name them app.js, passport.js, model.js, and auth.js.

  • The auth.js file would hold the authentication routes, the passport.js file would hold the configurations for passport, the model.js file would hold the user model and the app.js file would hold the code to bootstrap the server.

Setting up the express server

  • Open the app.js file and type the code below.
import express from "express";
import authRouter from "./auth.js";
import dotenv from 'dotenv';

// configure dotenv to access environment variables
dotenv.config();

const PORT = process.env.PORT || 3000;

// setup express server
const server = express();

server.use(express.json());
server.use("/api/v1/auth", authRouter);

// listen for connections
server.listen(PORT, () => {
  console.log(`server is listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

The code above sets up the express server and attaches the authentication routes (we have yet to create them) to the server.

Setting up the database and schema

  • Open the model.js file and create the user schema. The schema establishes the fields and types of data to be stored in the Mongodb database. Create the user schema by typing the code below.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  }
});

const UserModel = mongoose.model('user', UserSchema);

module.exports = UserModel;
Enter fullscreen mode Exit fullscreen mode
  • The schema above specifies that the database would have an email and password field which would both have String types and be required. The unique: true property in the email field ensures that the database does not store two similar emails. The Mongoose library takes the schema and converts it to a model that would be used to perform CRUD actions later on.

  • Open the app.js file and update it with the code below to enable the server to connect to the Mongodb database when the server starts up.

import mongoose from "mongoose";

// MongoDB connection uri
const MONGO_URI =
  process.env.MONGO_URI ||
  "mongodb://127.0.0.1:27017/passport-jwt-express-auth";

// connect to MongoDB
mongoose
  .connect(MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("successfully connected to mongodb");
  })
  .catch((err) => {
    console.log(err);
  });
Enter fullscreen mode Exit fullscreen mode
  • We first imported the Mongoose library and created the MONGO_URI variable to hold the connection string of our MongoDB database which was gotten by accessing the MONGO_URI environment variable.

  • We then used the connection string to connect to the database.

Configuring passport and JWT

  • Open the passport.js file and type the following code
import { ExtractJwt, Strategy } from "passport-jwt";
import passport from "passport";
const UserModel = require("./model.js");

const opts = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: "secret",
};

passport.use(
  new Strategy(opts, async (payload, done) => {
    try {
      const user = UserModel.findById(payload.id);
      if (user) return done(null, user);
    } catch (error) {
      return done(error);
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

The code above uses the passport and the passport-jwt strategy to extract the JSON Web Token from the request header and verifies using the JWT secret which can be gotten from the environment variables. If the token is valid, the ID of the user which is gotten from the token is then used to find and return the user's details from the database.

Implementing authentication routes

  • We would implement the register route first. It is important to note that we would not be using best practices as it is out of the scope of this post.
import express from "express";
import UserModel from "./model.js";

const authRouter = express.Router();

authRouter.post("/register", async (req, res, next) => {
  try {
    const user = await UserModel.create({
      email: req.body.email,
      password: req.body.password,
    });

    return res.status(201).json({
      message: "user created",
      user: { email: user.email, id: user._id },
    });
  } catch (error) {
    console.log(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

In the register route, we simply create the user and return a response that contains a message, the user's email, and id. It is important to note that in a real-world app, the password would be hashed as well as validations performed. In the next step, we'll create the login route.

authRouter.post("/login", async (req, res, next) => {
  try {
    //check if user exists
    const userExists = await UserModel.findOne({ email: req.body.email });
    if (!userExists)
      return res.status(400).json({ message: "user does not exist" });

    // check if password is correct
    if (userExists.password !== req.body.password)
      return res.status(400).json({ message: "incorrect password" });

    // generate access token
    const accessToken = jwt
      .sign(
        {
          id: userExists._id,
        },
        "secret",
        { expiresIn: "1d" }
      )

    return res
      .status(200)
      .json({ message: "user logged in", accessToken: accessToken });
  } catch (error) {
    console.log(error);
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

In the login route, we first check to see if the user exists and then check if the user's password is correct. After that, we sign an access token using the user's id, a secret key which would be stored in the .env file as well as specify a time limit for the access token to be valid. In the next code snippet, we would create a route to return the user's profile. This is the route that would be protected using JWT.

authRouter.get("/profile", async (req, res, next) => {
  try {
    // check if user exists
    const userExists = await UserModel.findOne({ email: req.body.email });
    if (!userExists)
      return res.status(400).json({ message: "user does not exist" });

    return res
      .status(200)
      .json({ userId: userExists._id, email: userExists.email });
  } catch (error) {
    console.log(error);
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

In this route, all we simply do is to check if the user exists using the user's email and then return the user's id and email.

Protecting routes

  • To protect the route to get a user's profile, we first import passport and the passport strategy implemented earlier on into the routes file.
import passport from "passport";
import "./passport.js";
Enter fullscreen mode Exit fullscreen mode
  • In the next step, we use the passport package to authenticate the route to get a profile and ensure only authenticated users can access a particular route. We update the route by adding the line passport.authenticate("jwt", { session: false }),. Your code should be like the snippet below:
authRouter.get(
  "/profile",
  passport.authenticate("jwt", { session: false }),
  async (req, res, next) => {
   // leave as before
})
Enter fullscreen mode Exit fullscreen mode

Testing

The next step would involve testing the routes in the project.

  • Start the server by running the code npm run start:dev.
  • Use Postman to create a user by navigating to the route localhost:3000/api/v1/auth/register. Change the port number if yours is different.
  • Login by navigating to the route localhost:3000/api/v1/auth/register and passing your email and password
  • Try accessing the profile route without passing the bearer param which would hold the token of the user and an error is returned. However, if you include the bearer param with the correct access token which is provided when you login, the route returns the user's id and email.

Conclusion

In this tutorial, we learned the importance of authentication and how to implement a basic authentication system using JSON Web Tokens (JWT) and Passport in a Node.js application.

We started by setting up the project, installing necessary dependencies, and configuring our Express server to handle authentication routes. We also established a MongoDB database schema for user data and integrated Passport for JWT authentication.

It is important to note that while this tutorial provides a solid foundation, real-world authentication systems require additional features such as password hashing, input validation, error handling, and more comprehensive testing. Building upon this foundation, you can explore advanced authentication practices and further enhance the security of your web applications.

Feel free to refer to the provided code and adapt it to your specific project needs.

Top comments (0)