Thought leadership from the most innovative tech companies, all in one place.

Securing Node.js APIs with JSON Web Tokens (JWT)

Learn how to secure your REST APIs with JWT.

Node.js API Authentication with JWT.

Your Node.js API allows your application to communicate with other software applications, transferring crucial and sensitive data. This is why the security of your Node.js API is crucial to prevent unauthorized access. An insecure API can be an easy target, leaving your application vulnerable to various attacks like API Token and Credential Compromise, Denial of Service (DoS) and Distributed Denial of Service (DDoS), Cross-Site Request Forgery (CSRF), and Man-in-the-Middle (MitM) Attacks out of many others.

To prevent this kind of attack, an effective method that can be used to secure your Node.js APIs is through the utilization of JSON Web Tokens (JWT).

What is a JSON Web Token (JWT)?

A JSON Web Token (JWT) is a concise and self-contained method of transmitting information between parties in the form of a JSON object. It comprises of three components; a header, a payload, and a signature. The header provides metadata about the token, including the algorithm used for its signing. The payload contains the desired claims or data to be transmitted such as user ID or role. The signature is generated by applying a key to the header and payload using a hashing algorithm like HMAC SHA256.

JSON Web Token process.

To ensure that a JWT is valid it can be verified by decoding the token and comparing the signature with the key. This process guarantees that the token hasn't been altered and originates from a trusted source. Moreover, JWTs can grant access to protected resources like APIs by validating claims within the payload such, as expiration time or scope.

Why use JWT for API authentication?

JWT (JSON Web Tokens) have the advantage of being stateless meaning they don't require any server-side session storage. All the necessary information to authenticate the user is contained within the token itself. This does not reduce the server load. Also enhances scalability and performance.

Another benefit of JWT is its portability. It can be utilized across domains and platforms relying on a shared secret key or a public/private key pair. This allows for origin authentication and facilitates single sign-on scenarios.

Moreover, JWT offers flexibility in terms of encoding data or claims. As long as it's JSON compatible any type of data can be encoded within the token. This adaptability enables customization to align with your application's needs and security requirements.

In this article, I'll guide you through the process of building a Node.js application. This application will have two APIs; one, for creating and logging in users and another for retrieving user information. For this guide you'll be using the following tools and libraries: Express as the web framework MongoDB as the database, Mongoose as the object data modeling (ODM) library, bcrypt for password hashing and Passport for authentication middleware. Additionally, you'll use two Passport strategies; JWT. The local strategy will handle user registration and login by providing a JSON Web Token (JWT) upon authentication. The JWT strategy will verify the token's authenticity.

Setting up the Project

Start by setting up your Node.js project and install the required dependencies.

Initialize a new Node.js project and install the required dependencies

To start a Node.js project use the npm init command. It will ask you for project details, like the name, version, description and more. If you wish you can simply press enter to accept the default values. Afterward, a package.json file will be generated which includes all the information and dependencies for your project.

Next, you need to install the following dependencies using the npm install command:

  • express: A web framework for Node.js that provides features such as routing, middleware, error handling, etc.
  • mongoose: An ODM library for MongoDB that allows you to define schemas, models, and queries for your data.
  • passport: An authentication middleware for Node.js that supports various strategies, such as local, JWT, OAuth, etc.
  • passport-local: A Passport strategy for authenticating users with a username and password.
  • passport-jwt: A Passport strategy for authenticating users with a JSON Web Token.
  • jsonwebtoken: A library for creating and verifying JSON Web Tokens.
  • bcrypt: A library for hashing and comparing passwords.

Install them by running the following command:

$ npm install express mongoose passport passport-local passport-jwt jsonwebtoken bcrypt

This will create a node_modules folder that contains the installed modules, and update the package.json file with the dependencies.

Set up the database schema and model for users using MongoDB and Mongoose

Create a new directory:

$ mkdir model.

Change directory:

$ cd model

Create a new file:

$ nano model.js

To store and manage your user data, in this guide, you will use MongoDB and Mongoose, a library that provides a layer of abstraction over MongoDB and allows you to define schemas and models for your data.

To connect to your MongoDB database, you need to specify the connection string in your code. You will use the mongoose.connect method to do so:

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/jwt-auth', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

This will create a connection object that you can use to interact with your database. You can also listen to the open and error events to handle the connection status:

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {
  console.log('Connected to MongoDB');
});

Next, use the mongose.Schema and mongose.model methods to define a schema and model for your users:

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  }
});

const User = mongoose.model('User', userSchema);

This will create a user schema that has two fields: username and password. The username field is required, unique, and of type string. The password field is also required and of type string. The user model is a constructor that creates user documents based on the user schema.

Create a helper function for hashing user passwords using bcrypt

Now, use bcrypt to secure your user passwords. To use bcrypt, you need to require it in your code. Optional but recommended, you can also define a salt rounds constant, which is a parameter that determines the complexity and security of the hashing process. The higher the salt rounds, the longer it takes to hash the password, but also the harder it is to break. A common value for salt rounds is 10.

const bcrypt = require('bcrypt');
const saltRounds = 10;

Next, using the bcrypt.hash method, create a helper function that takes a plain text password and returns a hashed password. This takes the password, the salt rounds, and a callback function as arguments. The callback function receives an error and a hashed password as parameters:

function hashPassword(password) {
  return new Promise((resolve, reject) => {
    bcrypt.hash(password, saltRounds, (err, hash) => {
      if (err) {
        reject(err);
      } else {
        resolve(hash);
      }
    });
  });
}

This will create a helper function that returns a promise that resolves with the hashed password or rejects with the error. Use this function to hash the user passwords before saving them to the database.

Implementing Local Strategy

Next, you will use Passport to handle the user registration and login. Passport is a middleware that simplifies the authentication process for Node.js applications. You will also use the local strategy to authenticate users with a username and password stored in your database.

Define a local strategy for Passport using passport-local

Create a new directory:

$ mkdir auth

Change to new directory:

$ cd auth

Create a new file:

$ nano auth.js

You need to require the local strategy module, which is a plugin for Passport that implements the local authentication logic:

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

Next, using the passport.use method to define a local strategy for Passport. This method takes a strategy name and a configuration object as arguments. The configuration object has two properties: usernameField and passwordField, which specify the names of the fields in the request body that contain the username and password. Also pass a callback function as the third argument, which receives the username, password, and a done function as parameters. The done function is used to indicate the result of the authentication attempt:

passport.use('local', new LocalStrategy({
  usernameField: 'username',
  passwordField: 'password'
}, function(username, password, done) {
  // Your authentication logic here
}));

In the callback function write your authentication logic. This will be used to find the user by the username in the database, comparing the password with the hashed password, and returning the appropriate response:

passport.use('local', new LocalStrategy({
  usernameField: 'username',
  passwordField: 'password'
}, function(username, password, done) {
  User.findOne({ username: username }, function(err, user) {
    if (err) {
      return done(err);
    }
    if (!user) {
      return done(null, false, { message: 'Incorrect username' });
    }
    bcrypt.compare(password, user.password, function(err, res) {
      if (err) {
        return done(err);
      }
      if (!res) {
        return done(null, false, { message: 'Incorrect password' });
      }
      return done(null, user);
    });
  });
}));

This will create a local strategy for Passport that finds the user by the username, compares the password with the hashed password, and returns the user object if successful, or an error or a false value if not. The third argument of the done function can also contain a message that can be displayed to the user in case of a failure.

Create a signup route that validates the user input and creates a new user in the database

Create a new directory:

$ mkdir routes

Change to new directory:

$ cd routes

Create a new file:

$ nano routes.js

To allow users to register in your application, create a signup route that validates the user input and creates a new user in the database. Use Express to create a route handler that responds to a POST request to the /signup endpoint:

const express = require('express');
const app = express();

app.use(express.json());

app.post('/signup', function (req, res) {
  // Validate the user input
  if (!req.body.username || !req.body.password) {
    return res.status(400).json({ message: 'Username and password are required' });
  }
  if (req.body.password.length < 8) {
    return res.status(400).json({ message: 'Password must be at least 8 characters long' });
  }

  // Hash the password
  hashPassword(req.body.password)
    .then(hash => {
      // Create a new user
      const user = new User({
        username: req.body.username,
        password: hash
      });

      // Save the user to the database
      user.save(function (err, user) {
        if (err) {
          return res.status(500).json({ message: 'Error saving user' });
        }
        // Return a success response
        return res.status(201).json({ message: 'User created successfully' });
      });
    })
    .catch(err => {
      // Handle any hashing errors
      return res.status(500).json({ message: 'Error hashing password' });
    });
});

This will create a signup route that validates the user input, hashes the password, creates a new user, and saves it to the database. It also returns a success or an error response accordingly.

Create a login route that authenticates the user using the local strategy and returns a JWT if successful

To allow users to login in to your application, you need to create a login route that authenticates the user using the local strategy and returns a JWT if successful. Use Express to create a route handler that responds to a POST request to the /login endpoint. You can also use the express.json middleware to parse the request body as JSON:

const express = require('express');
const app = express();
const jwt = require('jsonwebtoken');
const secretKey = 'your-secret-key-here';

app.use(express.json());

app.post('/login', function(req, res) {
  // Authenticate the user using the local strategy
  passport.authenticate('local', function(err, user, info) {
    if (err) {
      return res.status(500).json({ message: 'Error authenticating user' });
    }
    if (!user) {
      return res.status(401).json({ message: info.message });
    }
    // Create a JWT using the user ID
    const token = jwt.sign({ id: user._id }, secretKey, { expiresIn: '1h' });
    // Return the JWT to the user
    return res.json({ token: token });
  })(req, res);
});

});

This will create a login route that authenticates the user using the local strategy, creates a JWT using the user ID, and returns the JWT to the user. It also returns an error or a failure message accordingly.

Implementing JWT Strategy

Now to the main reason of this guide, to protect your APIs and grant access only to authenticated users, using the JWT strategy, which allows you to authenticate users with a JSON Web Token.

Define a JWT strategy for Passport using passport-jwt

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;

Next, you define a JWT strategy for Passport using the passport.use method:

const secretKey = 'your-secret-key-here';

passport.use('jwt', new JwtStrategy({
  jwtFromRequest: req => req.headers.authorization.split(' ')[1],
  secretOrKey: secretKey
}, function(payload, done) {
  // Your authentication logic here
}));

Next, write your authentication logic In the callback function. Use the User.findById method to find the user, and the done function to return a response:

passport.use('jwt', new JwtStrategy({
  jwtFromRequest: req => req.headers.authorization.split(' ')[1],
  secretOrKey: secretKey
}, function(payload, done) {
  User.findById(payload.id, function(err, user) {
    if (err) {
      return done(err);
    }
    if (!user) {
      return done(null, false, { message: 'User not found' });
    }
    return done(null, user);
  });
}));

This will create a JWT strategy for Passport that extracts the JWT from the authorization header, verifies the signature using the secret key, and finds the user by the ID in the payload. It also returns the user object if successful, or an error or a false value if not.

Create a middleware function for verifying the JWT and attaching the user to the request object

Create a middleware function that verifies the JWT and attaches the user to the request object using the passport.authenticate method to use the JWT strategy in your routes:

function verifyJwt(req, res, next) {
  passport.authenticate('jwt', { session: false }, function(err, user, info) {
    if (err) {
      return next(err);
    }
    if (!user) {
      return res.status(401).json({ message: info.message });
    }
    req.user = user;
    next();
  })(req, res, next);
}

This will create a middleware function that verifies the JWT using the JWT strategy, and attaches the user to the request object if successful, or returns an error or a failure message if not. It also invokes the next middleware or route handler in the chain.

Create a secure route that requires the JWT and returns the user information

Create a new file:

$ nano secure-routes.js

Now create a secure route that requires the JWT and returns the user information, and create a route handler that responds to a GET request to the /user endpoint with Express. You also need to use the verifyJwt middleware function to protect the route. Then use the req.user object to access the user information, and the res.json method to return the user information:

const express = require('express');
const app = express();

app.get('/user', verifyJwt, function(req, res) {
  // Return the user information
  return res.json({ user: req.user });
});

This will create a secure route that requires the JWT and returns the user information. It also uses the verifyJwt middleware function to verify the JWT and attach the user to the request object.

Join Them Together

Create a new file:

$ nano app.js

Then add the following:

const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const bodyParser = require('body-parser');

const UserModel = require('./model/model');

mongoose.connect('mongodb://127.0.0.1:27017/passport-jwt', { useMongoClient: true });
mongoose.connection.on('error', error => console.log(error) );
mongoose.Promise = global.Promise;

require('./auth/auth');

const routes = require('./routes/routes');
const secureRoute = require('./routes/secure-routes');

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));

app.use('/', routes);

// Plug in the JWT strategy as a middleware so only verified users can access this route.
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);

// Handle errors.
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.json({ error: err });
});

app.listen(3000, () => {
  console.log('Server started.')
});

Start the application:

$ node app.js

You will see a "Server started" message.

Conclusion

In this article, you have learned how to use JSON Web Tokens (JWT) to secure your Node.js APIs. You have also built a simple application that demonstrates how to implement JWT authentication using Passport.

Thanks for reading.




Continue Learning