Implementing JWT Auth with NodeJS

Implementing JWT Auth with NodeJS

ยท

4 min read

JWT is a particularly useful technology for API authentication and server-to-server authorization.

JWT (JSON Web tokens) is a great way to implement authentication in your applications, often it is being treated as a silver bullet for almost every backend project.

JWT Advantages:

  • You can create and verify the tokens on the fly without a need to store them ever in the database.

  • Simple implementation and faster development time.

  • Can be used across services and specifically be isolated in an authorization micro-service that can create these tokens. The rest of your microservices can just have the public key to verify the signature of the tokens.

Lets implement a simple node server by creating a folder and typing in the command

npm init -y

We will be needing some packages to install such as jsonwebtoken, bcrypt , cors , dotenv , express , mongoose , uuid . by entering the command.

npm i jsonwebtoken bcrypt cors dotenv express mongoose uuid

Environment Setup :

PORT = {port number}
MONGO_URI= {your mongo uri}
JWT_Secret= {jwt secret}
JWT_Refresh_Secret= {jwt refresh secret}

Setup node express server :

Here the User model is simple user schema and Token model has all tokens generated as refresh tokens.

const express = require('express')
require('dotenv').config()
require('mongoose').connect(process.env.MONGO_URI)
const cors = require('cors')
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");

// import models
const User = require("./models/User");
const Token = require("./models/Token");

const app = express();
app.use(express.json())
app.use(cors())

app.post('/register',async (req, res) => {
  try {
    const isUser = await User.findOne({ email: req.body.email });
    if (isUser)
      return res.status(400).send({
        success: false,
        message: "User already exist",
      });
    const data = {
      ...req.body,
      password: await bcrypt.hash(req.body.password, 10),
    };
    const newUser = new User(data);
    const user = await newUser.save();
    const access_token = jwt.sign({ _id: user._id }, process.env.JWT_Secret, {
      expiresIn: "30m",
    });
    const refresh_token = jwt.sign(
      { _id: user._id },
      process.env.JWT_Refresh_Secret
    );
    const refToken = new Token({
      token: refresh_token,
    });
    await refToken.save();
    res.send({
      success: true,
      user,
      access_token,
      refresh_token,
    });
  } catch (err) {
    res.status(400).send({
      success: false,
      message: err.message,
    });
  }
})

app.post('/login',async (req, res) => {
  try {
    const user = await User.findOne({
      $or: [{ email: req.body.text }, { username: req.body.text }],
    }).select("+password");
    if (!user)
      return res.status(401).send({
        success: false,
        message: "User doesn't exist",
      });
    const isMatch = await bcrypt.compare(req.body.password, user.password);
    if (!isMatch)
      return res.status(401).send({
        success: false,
        message: "Wrong password",
      });
    const access_token = jwt.sign({ _id: user._id }, process.env.JWT_Secret, {
      expiresIn: "30m",
    });
    const refresh_token = jwt.sign(
      { _id: user._id },
      process.env.JWT_Refresh_Secret
    );
    const refToken = new Token({
      token: refresh_token,
    });
    await refToken.save();
    user.password = undefined;
    res.send({
      success: true,
      user,
      access_token,
      refresh_token,
    });
  } catch (err) {
    res.status(400).send({
      success: false,
      message: err.message,
    });
  }
})

app.post('/token',async (req, res) => {
  try {
    const { token } = req.body;
    if (!token)
      return res.status(400).send({
        success: false,
        message: "No Refresh Token Provided",
      });
    const isToken = await Token.findOne({ token });
    if (!isToken)
      return res.status(400).send({
        success: false,
        message: "Invalid Refresh Token",
      });
    const decode = jwt.verify(token, process.env.JWT_Refresh_Secret);
    const accessToken = jwt.sign({ _id: decode._id }, process.env.JWT_Secret, {
      expiresIn: "30m",
    });
    res.send({
      access_token: accessToken,
    });
  } catch (err) {
    res.status(400).send({
      success: false,
      message: err.message,
    });
  }
})

app.post('/logout',async (req, res) => {
  try {
    const { token } = req.body;
    res.send(await Token.deleteOne({ token }));
  } catch (err) {
    res.status(400).send({
      success: false,
      message: err.message,
    });
  }
})

app.listen(process.env.PORT, () => {
    console.log(`Server running at port : ${process.env.PORT}`);
})

Refresh tokens never have a expiry date but access token can have a expiry time based on requirement.

Once a user is signed the access token and refresh token are sent back to user so the next request header contains those tokens.

We only save refresh tokens in db and never save access token. This helps in verifying a refresh token when expired.

When a access token is expired the api request is made to create a new one.

Hence the renewal process takes refresh token and verifies does the refresh token is valid and if valid it renews the acces token and send the new renewed token to client. This way if access token is ever into wrong hands then it wont be for long as it has a expiry and cannot renew it with fake refresh token.

ย