JWT authentication in nodejs with example

jwt authentication in nodejs thumbnail

In this tutorial, we will be looking at how to add JWT authentication in a nodejs application. By definition, JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties.

So let’s not waste any more time and get started.

Agenda

The agenda for this tutorial will be to create a basic register/login API and use JWT authentication to protect/guard specific routes in our nodejs API. And for that, we will be creating 3 routes register, login, and get a user.

Initialize our project and install dependencies

First of all, let’s create a new folder and open a terminal in our favourite code editor

npm init -y
npm install express dotenv http-errors jsonwebtoken lodash simple-json-db uuid

Set environment variables

After that let’s create a “.env” file inside our project’s root directory and paste the below values as per your choice

JWT_ACCESS_TOKEN_SECRET=”YOUR_SECRET_OF_YOUR_CHOICE”

JWT_EXPIRY=”EXPRY_TIME_ACCORDINGLY”

JSON_DB_PATH=./db.json

# this secret will be used to create our token, must be strong enough
JWT_ACCESS_TOKEN_SECRET=kjh7jg6$hgjh
# time after which our token will expire, 1m is 1 minute.
JWT_EXPIRY=1m
# path for our json database(dummy database)
JSON_DB_PATH=./db.json

Setup directory structure

Now we have created 4 directories for different purposes.

JWT/
┣ controllers/
┃ ┣ auth.js
┃ ┗ user.js
┣ middlewares/
┃ ┗ auth.js
┣ routes/
┃ ┣ auth.js
┃ ┗ user.js
┣ utils/
┃ ┗ jwt.js
┣ .dccache
┣ .env
┣ app.js
┣ bun.lockb
┣ db.json
┣ package-lock.json
┣ package.json
┗ rest.http

Create our app’s entry file

Now that we are ready with the directories, we are good to go.

Firstly, create an app.js file inside our root directory.

This will be our app’s entry file. As we can see we have imported some routes which we will see later in the tutorial. But first, copy-paste the below code into our app.js

require('dotenv').config()
const express = require('express')

const app = express()

const authRoutes = require('./routes/auth')
const userRoutes = require('./routes/user')

// middlewares

app.use(express.json())

app.use('/', authRoutes)
app.use('/user', userRoutes)

// error handling
app.use((err, req, res, next) => {
  res.status(err.status || 500).send({
    error: {
      status: err.status || 500,
      message: err.message
    }
  })
})
app.listen(process.env.PORT || 3000, () => {
  console.log('server is running', process.env.PORT || 3000)
})

process.on('SIGINT', async () => {
  process.exit(1)
})

Create our controllers

After that, we will be creating our controllers.

Inside our controller directory, we will be creating 2 controller files auth.js and user.js

auth.js: which contains logic for our register and login functionality.

const { v4: uuid } = require('uuid')
const creatError = require('http-errors')
const JSONdb = require('simple-json-db')
const db = new JSONdb(process.env.JSON_DB_PATH, { asyncWrite: true })
const { signJwtToken } = require('../utils/jwt')
/**
 * Access token delivery handler
 */
const tokenHandler = async (user) => {
  try {
    // generate token
    const accessToken = await signJwtToken(user, {
      secret: process.env.JWT_ACCESS_TOKEN_SECRET,
      expiresIn: process.env.JWT_EXPIRY
    })
    return Promise.resolve(accessToken)
  } catch (error) {
    return Promise.reject(error)
  }
}

// handles register
exports.register = async (req, res, next) => {
  try {
    const { email, password } = req.body
    // this is just a demo code and not for production
    db.set(email, JSON.stringify({ id: uuid(), username: `Demo username ${uuid()}`, password }))
    res.status(201)
    res.send('Account created successfully')
  } catch (error) {
    next(error)
  }
}

// handles login
exports.login = async (req, res, next) => {
  try {
    const { email, password } = req.body

    const userData = db.get(email)

    if (!userData) throw creatError.NotFound()
    
    const { id, username, password: dbPassword } = JSON.parse(userData)

    if (!(id && (password === dbPassword))) throw creatError.Unauthorized()
    
    const token = await tokenHandler({ id, username, email })
    res.send(token)
  } catch (error) {
    next(error)
  }
}

Remember we are storing the user registration data in a JSON database and so we will also need some way to retrieve it. So let’s create our user.js controller file.

user.js: contains logic for retrieving users from our JSON database.

const creatError = require('http-errors')
const JSONdb = require('simple-json-db')
const db = new JSONdb(process.env.JSON_DB_PATH, { asyncWrite: true })
exports.getUser = async (req, res, next) => {
  try {
    // checking for any error occurance
    const { email } = req.payload
    if (!email) throw creatError.Unauthorized()
    
    const userData = db.get(email)

    if (!userData) throw creatError.NotFound()

    // creating user as json
    const userDataObj = JSON.parse(userData)

    // remove the password key before sending it to client
    delete userDataObj.password

    res.status(200).send(userDataObj)
  } catch (error) {
    next(error)
  }
}

Create utilities required for JWT authentication in nodejs

Once we are done with our controllers we need to create a jwt.js file inside our utils directory which will hold the functionality of signing and verifying our JWT tokens.

const jwt = require('jsonwebtoken')
const creatError = require('http-errors')
module.exports = {
  signJwtToken: (data, { secret, expiresIn }) => {
    return new Promise((resolve, reject) => {
     
      // options also takes in algorithm (default: HS256)
      // Another algorithm we can use is RS256 which require a pair of pvt/pub key
      const options = {
        expiresIn,
        algorithm: 'HS256'
      }

      jwt.sign(data, secret, options, (err, token) => {
        if (err) return reject(creatError.InternalServerError())
        resolve(token)
      })
    })
  },
  // verify tokens
  verifyJwtToken: ({ token, secret }) => {
    return new Promise((resolve, reject) => {

      jwt.verify(token, secret, { algorithms: ["HS256"] }, (err, payload) => {
        if (err) {
          const message = err.name === 'TokenExpiredError' ? err.message : 'Unauthorized'
          return reject(creatError.NotAcceptable(message))
        }
        resolve(payload)
      })
    })
  }
}

When using jwt.sign method the default algorithm in options is HS256 if one is not provided. This is important from a security perspective because when using jwt.verify we also need to pass in the same algorithm(HS256 in this case) as an array.

Note: Please make sure to only ever have 1 algorithm in the array passed under options in jwt.verify to prevent Algorithm confusion attacks

Create middlewares

Next, we need a middleware that will check if the user requesting a protected route has a valid JWT token.

So let’s create a file named auth.js in our middleware directory and within that, we will have a function named accessTokenValidator which as the name suggests checks for token validity and return an error in case the token is not a valid one.

require('dotenv').config()

const _getKeyValue = require('lodash/get')
const { verifyJwtToken } = require('../utils/jwt')

module.exports = {
  accessTokenValidator: async (req, res, next) => {
    try {
      let token = _getKeyValue(req.headers, 'authorization', null)

      if (!token) throw creatError.Unauthorized()
      token = token.split(' ')[1]
      req.payload = await verifyJwtToken({ token, secret: process.env.JWT_ACCESS_TOKEN_SECRET })
      next()
    } catch (error) {
      next(error)
    }
  }
}

Create routes

Now that we are done with all the required functionality we have to create routes for our users and for that, we have two files inside the routes directory namely

  • auth.js: handles register/login routes
  • user.js: protected routes that handle retrieving user’s data

Following is the code for the auth.js route file

const express = require('express')
const router = express.Router()

const { register, login } = require('../controllers/auth')

router.post('/register', register)
router.post('/login', login)

module.exports = router

and below one is our user.js route file.

As you can see our route /info is a protected one and that means it can only be accessed with a valid JWT token.

const express = require('express')
const router = express.Router()

const { getUser } = require('../controllers/user')
const { accessTokenValidator } = require('../middlewares/auth')
// accessToken validator middleware

router.get('/info', accessTokenValidator, getUser)

module.exports = router

Testing our JWT authentication in our nodejs app

Now we are all set with the coding section so, we are left only with the Testing part and for that, we will be using VS-code extension rest-client

Now the vs-code extension “rest-client” needs a “rest.http” file inside the root directory of our project containing all our requests.

So, inside the “rest.http” file we have.


POST http://localhost:3000/register
Content-Type: application/json

{
    "email": "[email protected]",
    "password": "Qwerty12!"
}

###login
POST http://localhost:3000/login
Content-Type: application/json 

{
    "email": "[email protected]",
    "password": "Qwerty12!"
}

### get user data(protected route)
GET http://localhost:3000/user/info 
Authorization: Bearer 

Last but not least we start the server using

node app.js

Let’s start by registering a new user by requesting our register route. If our user was successfully created then our response will look something like this.

account creation
Response after account creation

After we have a user let’s try login. in with the user details we just created. This will return us a JWT token which we can use later to request a JWT-protected route.

response from the server on login
Response from the server on login

Note: currently we have set an expiry time of 1 minute so our token will only be valid for 1 minute. Please try changing it to something like 1hr in case you see an invalid token or something.

Now to use the token we need to pass our token in the header of our request.

Authorization: Bearer “Token_value”

For example:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImU0NTVjMzg5LWVkMDktNGQxMS1hOWNhLTk0MjFmMzA2MWQ1YyIsInVzZXJuYW1lIjoiRGVtbyB1c2VybmFtZSA5ZjA0NmE5MS1lZmI2LTQxYzQtODhjZi01MTExNGExMmY0MDkiLCJlbWFpbCI6InByYWRlZXBAZ21haWwuY29tIiwiaWF0IjoxNjMwNzMxNTU4LCJleHAiOjE2MzA3MzE2MTh9.hjTzdNU1jdvUJq3cIt7-VDJLgMwP8tKqTV_uRIgCiFs

If our token is valid we must have our user data returned as below

jwt authenticated user data response
User data response

Improvements

Currently, our JWT token expires in 1 minute and once it expires a user will be technically logged out and needs to log in again to continue making requests. This is an issue as a client needs to log in repeatedly after a time interval. Fortunately, there is a solution to this problem using refresh tokens which we will cover in another tutorial.

Problems with JWT

  • When a user logs out, he/she isn’t technically logged out until the token expires.
  • It is very hard to revoke tokens.
  • Between the time a token is issued and expires, data in the token has the possibility to become stale.
  • JWT is not encrypted so data in it is visible. So it’s not advisable to store any sensitive data in the JWT token. Although this problem can be overcome by using JWE(JSON Web Encryption)

Conclusion

Walah! we have successfully learned how to add JWT authentication in a nodejs application. I hope, I managed to make you understand the basic idea behind the registration and login using JWT. If you people find it useful please don’t forget to appreciate it in the comment section.

If there is any doubt related to this post, please ask in the comment section. Also, don’t forget to share your feedback.

THANK YOU!

4 thoughts on “JWT authentication in nodejs with example

  1. This was really helpfull to me to create my first JWT based node app. I just wanted to know why did you use lodash module in your code? what is the benefit of using that module?

    1. Glad that this article was helpful. Lodash has been used just for convenience purposes.

      the below code snippet


      let token = _getKeyValue(req.headers, 'authorization', null)

      can be replaced with the below one


      let token = null

      if (authorization' in req.headers) {
      token = req.headers["authorization"]
      }

      but the former one is a bit cleaner than the latter. That’s why we choose to use lodash/get but it’s entirely optional.

      Cheers

Leave a Reply

Back To Top