Step by Step Guide to Authentication with JSON Web Tokens (JWT) with express and passport
Tags: express, security
Published: 2023-10-04 11:42:36

## 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"
```

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}`);
});
```

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;
```

- 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);
  });
```

- 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);
    }
  })
);
```

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);
  }
});
```

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);
  }
});
```

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);
  }
});
```

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";
```

- 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
})
```

## 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](https://github.com/alahirajeffrey/passport-jwt-express-auth) and adapt it to your specific project needs.