JWT Authentication Tutorial with Node JS
Tags: webdev, javascript, jwt
Published: 2021-12-28 16:45:56

- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6umrqrdcgqt49eqmfali.png)

---
With JWT:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yo5jxzcxiaxtr24r7ppy.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gzemlem1hw45e6596o2l.png)

- 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:

```jsx
{
  "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"
	  }
}
```

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

## 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)

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

- Let's test this endpoint with `Postman`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q3rygz9zalansisphywj.png)

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

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

---

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kzfmgbc2wnwgaw3zaaed.png)

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

Complete Implementation:
```jsx
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);
})
```
---