How To Implement JWT Authentication On Node

If you're new to the world of Node.js developers, chances are you'll be interested in learning how to implement stateless JWT token authentication. The majority of the tutorials that I've found online end up making things overcomplicated, while a Node.js JWT authentication example should be a very straightforward process.

In this article, I'll walk you through the process of implementing Node js JWT authentication. I'll use Express.js as my web framework, and I'm assuming you already know the fundamentals of the framework.

Another thing I'd like to clarify is that I won't get into database queries in this article. Instead, I’ll use an array as my data source. Feel free to swap that out with whatever database management system you’re using.

Before we see the JWT authentication example of implementing in Node.js, first, you’ll need Node.js installed on your computer to follow along this tutorial. If you don’t have that already, navigate to https://nodejs.org/ and download the latest version. If you’re on Linux, you can download the NodeSource Node.js Binaries from GitHub.

Open up your terminal inside any folder on your computer and initialize a new project by executing the npm init -y command. Now open the project directory using your favorite code editor. I’ll be using Visual Studio Code as my editor.

Project Initialization

You’ll need to install a few packages in the project now. To do so, execute the following command:

npm install express bcrypt jsonwebtoken && npm install --save-dev nodemon

express is the web framework, which by the way has many options with Node.js like building apps with MongoDB aggregation pipeline, bcrypt will be used for encrypting the password, and jsonwebtoken will be used to generate new JWT. nodemon will provide auto reload facilities upon code changes.

Open up the package.json file and change the value of main to app.js from index.js. Then under scripts, change test to start and its corresponding command to nodemon app.js accordingly.

The package.json file should now look as follows:

{
  "name": "nodeauth",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.19"
  }
}

Now, create a new file named app.js in the project folder and put the following lines of code in it:

const bcrypt = require('bcrypt');
const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
const port = 3000;

app.use(express.json());

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

app.listen(port, () => {
  console.log(`app running on http://127.0.0.1:${port}`)
});

Start the application by executing the npm start command and visit http://127.0.0.1:3000 to see the Hello, World! message on your screen.

Register Route

Let’s deal with the process of registration first. First, right after the line const port = 3000;, create a new array named users:

const bcrypt = require('bcrypt');
const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
const port = 3000;

const users = [];

// ...

Now, create a new POST route in your API as follows:

// ...

app.use(express.json());

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

app.post('register', async (req, res) => {
    // registration route
});

// ...

Whenever a user makes a POST request to this route with their email address and a password, you’ll have to save them in your data storage.

//...

app.post('register', async (req, res) => {
    try {
        if (users.some(user => user.email === req.body.email)) {
            const err = new Error('Email Taken!')
            err.status = 400;
            throw err;
        }

        const user = {
            email: req.body.email,
            password: await bcrypt.hash(req.body.password, 12),
        }

        users.push(user);

        res.status(201).json({
          status: 'success',
          message: 'User Registered!',
          data: {
            user: {
                email: user.email,
            },
          },
        });
      } catch (err) {
        res.status(err.status).json({
            status: 'fail',
            message: err.message,
          });
      }
});

// ...

First, you’ll have to check whether the user already exists or not. If it does, then you’ll have to return a 400 error with whatever message you see fit. If the user doesn’t exist, you’ll go on and create a new user. When creating a new user, you can not save the password varbatim. The bcrypt.hash() method will encrypt the password, so even if your data storage gets hacked, the passwords will be safe.

Login Route

The login route is similar to the register route in many ways. You’ll use the same library to check the submitted password from the user against the saved hash and generate a new JWT using the jsonwebtoken library.

Create a new login route in your API as follows:

//...

app.post('login', async (req, res) => {
    try {
        const user = users.find(user => user.email === req.body.email);
        if (!user) {
            const err = new Error('User Not Found!')
            err.status = 400;
            throw err;
        } else if (await bcrypt.compare(req.body.password, user.password)) {
            const tokenPayload = {
              email: user.email,
            };
            const accessToken = jwt.sign(tokenPayload, 'SECRET');
            res.status(201).json({
                status: 'success',
                message: 'User Logged In!',
                data: {
                  accessToken,
                },
              });
        } else {
            const err = new Error('Wrong Password!');
            err.status = 400;
            throw err;
          }
      } catch (err) {
        res.status(err.status).json({
            status: 'fail',
            message: err.message,
          });
      }
});

// ...

When the user sends off their email and password to this route, you’ll first have to check whether a user with that email address exists or not. If not, send an error response.

Otherwise, you’ll have to compare the submitted password against the password hash you’ve saved on your data storage. The bcrypt.compare() method can do that for you.

If the password matches, you can then use the jwt.sign() method to generate a new JWT. Generating a new JWT requires a token payload and a signing key. I’ve used SECRET as the signing key here, but make sure to use something super secure here.

The payload needs to carry the identity of the user. You’ll later use this identity to recognize the user in protected requests.

Authenticate Middleware

Now that you’ve implemented the login and register functionality, it's time to implement the authentication guard, so that you can protect sensitive routes from unauthenticated access. To do so, create a new file called authenticate.js on the project folder and put the following code in it:

const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    res.status(401).json({
        status: 'fail',
        message: 'Unauthorized!',
      });
  }
  const token = authHeader.split(' ')[1];
  try {
    const user = jwt.verify(token, 'SECRET');
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({
        status: 'fail',
        message: 'Unauthorized!',
      });
  }
};

This middleware will read the authorization header on user requests. In case you didn’t know, this is the header used for transferring JWT tokens to the server.

authorization: Bearer <JWT TOKEN>

The majority of the REST clients out there support sending the authorization header by default. You’ll have to do the research for the REST client you’re using.

If the middleware finds this header, the value of the header will be split into two, and the JWT token will be extracted. Now, the jwt.verify(token, 'SECRET'); method call can verify whether this token is valid or not. If valid, the middleware will save the user information in an object called user inside the req object and pass the request to the next middleware in the stack by calling the next() method. Otherwise, an error response will be returned.

Protecting Routes

Finally, let's create a protected route to test out the middleware as follows:

// ...

const auth = require('./authenticate');

//...

app.get('profile', auth, (req, res) => {
    res.status(200).json({
        status: 'success',
        message: 'Logged In User Information.',
        data: {
          user: {
              email: req.user.email,
          },
        },
      });
});

// ...

If a logged-in user tries to access this route using a valid token, the user information will be available within the req.user object. This middleware just returns that information if found.

Conclusion

Authentication in any application is one of the most sensitive parts, and the same goes for Nodejs JWT authentication. Nevertheless, since with Node.js you can do many things such as convert Buffer to String or even create Node CLI tool, this goes to show that you can also implement JWT authentication.

Implementing authentication flawlessly is a very hard task, and that’s why companies like Auth0 provides paid identity services.

I haven’t discussed the ideal way of structuring your API project. If you’re interested in learning that, feel free to check out this repository on GitHub.

FAQs

Q: How to handle token refreshment for JWT in Node.js?
Implement token refreshment in Node.js by issuing a short-lived access token and a longer-lived refresh token. When the access token expires, use the refresh token to securely request a new access token from the server without requiring the user to log in again.
Q: What are the best practices for storing and managing JWT secrets securely?
Store JWT secrets securely by using environment variables, avoiding hard-coded secrets in the codebase. Use encrypted secrets management services for production. Rotate secrets periodically and ensure they are accessible only by the application that needs them.  
Q: Can JWT authentication be integrated with OAuth2 for more complex authentication flows?
Yes, JWT authentication can be integrated with OAuth2 to support more complex authentication flows. This enhances security and flexibility in access control mechanisms.  
Farhan Hasin Chowdhury
Farhan Hasin Chowdhury
Senior Software Engineer

Farhan is a passionate full-stack developer and author. He's a huge fan of the open-source mindset and loves sharing his knowledge with the community.

Expertise
  • Laravel
  • MySQL
  • Vue.js
  • Node.js
  • AWS
  • DigitalOcean
  • Kubernetes
  • AWS RDS
  • MongoDB
  • Python
  • Elastic Beanstalk
  • AWS S3
  • AWS CloudFront
  • Redis
  • Express.js
  • Amazon EC2
  • PostgreSQL
  • FastAPI
  • GitLab CI/CD
  • JavaScript
  • PHP
  • +16

Ready to start?

Get in touch or schedule a call.