DEV Community

Harshavardhan
Harshavardhan

Posted on

JWT Authentication Tutorial with Node JS

  • JWT stands for JSON WEB TOKEN.
  • JWTs are great way to implement authentication. It is a standard that defines a compact and self-contained way to securely transmit information between a client and a server as a JSON object.

You can find the entire code here: https://github.com/harsha-sam/jwt-auth-tutorial


How JWT works

Before JWT:

Image description


With JWT:

Image description

JWT token looks like this:
Reference: https://jwt.io/

Image description

  • JWT has three parts separated by dots (.) . JWT will be created with a secret.

    1. Header: First part denotes the hash of header (header generally consists of algorithm used for hashing and type)
    2. Payload: Second part will have hash of the payload (payload will contain user id and info, this will be decoded when we verify the JWT.
    3. Signature: Third part will contain an hash of (header + '.' + payLoad + secret). This part plays a crucial role in finding whether the user or anyone didn't tamper with the token before sending the request.
  • So, what verifying JWT will do is, it generates the third part of the hash again from the first and second parts of the JWT token sent with the request. If it matches, then we can get the payload.

  • Even if any payload or data is modified in frontend and sent to backend. JWT verify will fail because third hash going to be different if data is tampered.

  • The advantage of the JWT is we are storing the user info in token itself. So, it will work across all servers.

Let's dive into the implementation:


Initial setup and Installing libraries

  • Create a new directory and move in to the directory

  • Now, run:
    npm init - y

The above command will initialise the package.json file

  • Let's install all required dependencies:

Run:
npm i express jsonwebtoken dotenv bcrypt cors express

  • To install nodemon as a dev-dependency
    npm i —save-dev nodemon

  • Now, package.json will look something like this:

{
  "name": "jwt-auth-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
        // added devStart command
    "devStart": "nodemon server.js",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
    "dependencies": {
        "bcrypt": "^5.0.1",
        "cors": "^2.8.5",
        "dotenv": "^10.0.0",
        "express": "^4.17.1",
        "jsonwebtoken": "^8.5.1"
      },
      "devDependencies": {
        "nodemon": "^2.0.12"
      }
}
Enter fullscreen mode Exit fullscreen mode
  • Add start and devStart commands in your package.json file, if they don't exist.

Creating env file

  • Create a file with name .env in your project folder where we are going to store all our app secrets 🔒
  • Let's add our first secret APP_PORT which basically stores the port number on which our server is going to run.
  • Now, your .env file should look something like this
APP_PORT=3000
Enter fullscreen mode Exit fullscreen mode

Setting up an endpoint with express

  • Let's create our first endpoint with express in our index.js file. ( Create the file, if it doesn't exist)
// index.js
var express = require('express');
require('dotenv').config() // will config the .env file present in the directory

const PORT = process.env.APP_PORT || "8081";
const app = express();

app.get('/', (req, res) => {
  res.send("Hello !")
})

app.listen(PORT, () => {
  console.log("Listening on port", PORT);
})
Enter fullscreen mode Exit fullscreen mode
  • Let's test this endpoint with Postman

Image description

Great, seems like our endpoint is working


Setting up login route

  • Before creating a login route, let's first create a fake db that stores credentials
// index.js
var express = require('express');
require('dotenv').config() // will config the .env file present in the directory

const db = [
  {
    username: "Harsha",
    password: "hello123"
  },
  {
    username: "Sam",
    password: "hello12345"
  },
]
const POSTS = [
  {
    name: "Harsha",
    title: "Post 1",
    body: "1234"
  },
  {
    name: "Sam",
    title: "Post 2",
    body: "1234"
  },
]

const PORT = process.env.APP_PORT || "8081";
const app = express();

app.get('/', (req, res) => {
  res.send("Hello !")
})
app.get("/posts", (req, res) => {
  res.status(200).json(POSTS);
})

app.listen(PORT, () => {
  console.log("Listening on port", PORT);
})
Enter fullscreen mode Exit fullscreen mode

JWT with access token and refresh token

  • Let's create a login endpoint now, which will authenticate the user first and then generates a JWT token.
  • To generate a JWT token, we use jwt.signin(user_info, secret, {expiresIn}) method, we'll pass in user info object and a secret and expires in time, if you want to expire the token.
  • Secret token can be anything but for best practice let us generate this secret token using crypto node library as shown below

Image description

  • Add these secrets generated in .env file as ACCESS_TOKEN_SECRET and REFRESH_TOKEN_SECRET

Complete Implementation:

var express = require('express');
var bcrypt = require('bcrypt');
var jwt = require('jsonwebtoken');
require('dotenv').config()// will config the .env file present in the directory

let POSTS = [
  {
    username: "Harsha",
    title: "Post 1",
    body: "1234"
  },
  {
    username: "Harsha",
    title: "Post 2",
    body: "1234"
  },
  {
    username: "Harsha",
    title: "Post 2",
    body: "1234"
  },
  {
    username: "Sm",
    title: "Post 2",
    body: "1234"
  },
  {
    username: "no",
    title: "Post 2",
    body: "1234"
  },
]

let DB = []

// used to store refresh tokens, as we will manually expire them
let SESSIONS = []

const generateAccessToken = (user) => {
  // jwt will make sure to expire this token in 1 hour
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, {
    'expiresIn': '1h'
  })
}

const PORT = process.env.APP_PORT || "8081";
const app = express();
app.use(express.json())

// middlewares
const validateToken = async (token, tokenSecret) => {
  // returns user info, if the jwt token is valid
  return await jwt.verify(token, tokenSecret,
    (error, payload) => {
      if (error) {
      throw (error)
      }
      return payload
  })
}
const validateAccessToken = async (req, res, next) => {
  // returns user info, if the jwt token is valid
  try {
    req.user = await validateToken(req.body['accessToken'], process.env.ACCESS_TOKEN_SECRET)
    next();
  }
  catch (error) {
    res.status(401).
      json({ error: error.message || 'Invalid access token' })
  }
}

const validateRefreshToken = async (req, res, next) => {
  try {
    req.user = await validateToken(req.body['refreshToken'], process.env.REFRESH_TOKEN_SECRET)
    next();
  }
  catch (error) {
    res.status(401).
      json({ error: error.message || 'Invalid refresh token' })
  }
}

app.get("/posts", validateAccessToken, (req, res) => {
  const { username } = req.user;
  const userPosts = POSTS.filter((post) => post.username === username)
  res.json(userPosts)
})

app.post("/register", async (req, res) => {
  const { username, password } = req.body;
  let hash = "";
  const salt = await bcrypt.genSalt(12);
  hash = await bcrypt.hash(password, salt);
  DB.push({ username, passwordHash: hash })
  console.log(DB);
  res.json("Successfully registered")
})

app.post("/login", async (req, res) => {
  const { username, password } = req.body;
  for (let user of DB) {
    // authentication - checking if password is correct
    if (user.username === username && await bcrypt.compare(password, user.passwordHash)) {
      const accessToken = jwt.sign({ username: user.username }, process.env.ACCESS_TOKEN_SECRET, {
        'expiresIn': '1h'
      })
// In this implementation, refresh token doesn't have any expiration date and it will be used to generate new access token
      const refreshToken = jwt.sign({ username: user.username }, process.env.REFRESH_TOKEN_SECRET)
// We will store refresh token in db and it'll expire when the user logs out
      SESSIONS.push(refreshToken);
// sending accesstoken and refresh token in response
      res.json({ accessToken, refreshToken });
    }
  }
})

app.post('/token', validateRefreshToken, (req, res) => {
  // generating new access token, once the refresh token is valid and exists in db
  const { username } = req.user;
  if (SESSIONS.includes(req.body['refreshToken'])) {
    res.json({ accessToken: generateAccessToken({ username })})
  }
  else {
    res.status(403).json('Forbidden: refresh token is expired')
  }
})

app.delete("/logout", async (req, res) => {
  // deleting refresh token from db 
  SESSIONS = SESSIONS.filter((session) => session != req.body['refreshToken']);
  res.sendStatus(204);
})

app.get('/', (req, res) => {
  res.send("Hello !")
})
app.listen(PORT, () => {
  console.log("Listening on port", PORT);
})
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
suhakim profile image
sadiul hakim

Nice job