Published: June 17 2020
Last updated: July 02 2020

Node.js + MongoDB API - JWT Authentication with Refresh Tokens

Tutorial built with Node.js and MongoDB

Other versions available:

In this tutorial we'll go through an example of how to implement JWT (JSON Web Token) authentication with refresh tokens in a Node.js + MongoDB API.

For an extended example that includes email sign up, verification, forgot password and user management (CRUD) functionality see Node + Mongo - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password.

The example API has the following endpoints/routes to demonstrate authenticating with JWT, refreshing and revoking tokens, and accessing secure routes:

  • /users/authenticate - public route that accepts HTTP POST requests containing a username and password in the body. If the username and password are correct then a JWT authentication token and the user details are returned in the response body, and a refresh token cookie (HTTP Only) is returned in the response headers.
  • /users/refresh-token - public route that accepts HTTP POST requests with a refresh token cookie. If the cookie exists and the refresh token is valid then a new JWT authentication token and the user details are returned in the response body, a new refresh token cookie (HTTP Only) is returned in the response headers and the old refresh token is revoked.
  • /users/revoke-token - secure route that accepts HTTP POST requests containing a refresh token either in the body or in a cookie, if both are present the token in the body is used. If the refresh token is valid and active then it is revoked and can no longer be used to refresh JWT tokens.
  • /users - secure route that accepts HTTP GET requests and returns a list of all the users in the application if the HTTP Authorization header contains a valid JWT token. If there is no auth token or the token is invalid then a 401 Unauthorized response is returned.
  • /users/{id} - secure route that accepts HTTP GET requests and returns the details of the user with the specified id.
  • /users/{id}/refresh-tokens - secure route that accepts HTTP GET requests and returns a list of all refresh tokens (active and revoked) of the user with the specified id.
  • /api-docs - swagger documentation for the api


MongoDB and Mongoose ODM

MongoDB is the database used by the api for storing user and refresh token data, and the Mongoose ODM (Object Data Modeling) library is used to interact with MongoDB, including defining the schemas for collections, connecting to the database and performing all CRUD operations. For more info on Mongoose see https://mongoosejs.com/.

The tutorial project is available on GitHub at https://github.com/cornflourblue/node-mongo-jwt-refresh-tokens-api.

Update History:

  • 02 Jul 2020 - Updated to express-jwt version 6.0.0 to fix security vulnerability
  • 17 Jun 2020 - Built with Node.js and MongoDB


Tutorial Contents


Tools required to run the Node.js + MongoDB JWT Example Locally

To develop and run Node.js + MongoDB applications locally, download and install the following:

  • Node.js & npm - includes the Node.js JavaScript runtime and npm package manager
  • MongoDB - the MongoDB database server
  • Visual Studio Code - code editor that runs on Windows, Mac and Linux


Running the Node + Mongo JWT with Refresh Tokens API Locally

  1. Run MongoDB, instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
  2. Download or clone the project source code from https://github.com/cornflourblue/node-mongo-jwt-refresh-tokens-api
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Start the api by running npm start (or npm run start:dev to start with nodemon) from the command line in the project root folder, you should see the message Server listening on port 4000, and you can view the Swagger API documentation at http://localhost:4000/api-docs.
  5. Follow the instructions below to test with Postman or hook up with an example Angular application.

Before running in production

Before running in production also make sure that you update the secret property in the config.json file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).


Testing the API with Postman

Postman is a great tool for testing APIs, you can download it at https://www.getpostman.com/.

Below are instructions on how to use Postman to authenticate a user to get a JWT token and refresh token from the api, refresh and revoke tokens, and retrieve user details from secure routes using JWT.

How to authenticate a user with Postman

To authenticate a user to get a JWT token and refresh token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/users/authenticate.
  4. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  5. Enter a JSON object containing the test username and password in the "Body" textarea:
    {
        "username": "test",
        "password": "test"
    }
  6. Click the "Send" button, you should receive a "200 OK" response with the user details including a JWT token in the response body and a refresh token in the response cookies.

Here's a screenshot of Postman after the request is sent and the user has been authenticated:

And this is the response cookies tab with the refresh token:


How to refresh a token with Postman

This step can only be done after the above authenticate step because a valid refresh token cookie is required.

To use a refresh token cookie to get a new JWT token and a new refresh token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the refresh token route of your local API - http://localhost:4000/users/refresh-token.
  4. Click the "Send" button, you should receive a "200 OK" response with the user details including a JWT token in the response body and a refresh token in the response cookies. Make a copy of the JWT token value because we'll be using it in the next steps to make authenticated requests.

Here's a screenshot of Postman after the request is sent and the token has been refreshed:

And this is the response cookies tab with the new refresh token:


How to make an authenticated request to retrieve all users

To make an authenticated request to get all users with the JWT token from the previous step, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the users route of your local API - http://localhost:4000/users.
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous step into the "Token" field.
  5. Click the "Send" button, you should receive a "200 OK" response containing a JSON array with all the user records in the system (just the one test user in the example).

Here's a screenshot of Postman after making an authenticated request to get all users:


How to retrieve all of a user's refresh tokens

To get all refresh tokens for a user including active and revoked tokens, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the users route of your local API - http://localhost:4000/users/1/refresh-tokens.
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate (or refresh token) step into the "Token" field.
  5. Click the "Send" button, you should receive a "200 OK" response containing a JSON array with all the test user's refresh tokens. Make a copy of the last token value (the active token) because we'll use it in the next step to revoke the token.

Here's a screenshot of Postman after making an authenticated request to get all refresh tokens for the test user:


How to revoke a token with Postman

To revoke a refresh token so it can no longer be used to generate JWT tokens, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/users/revoke-token.
  4. Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate (or refresh token) step into the "Token" field.
  5. Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
  6. Enter a JSON object containing the active refresh token from the previous step in the "Body" textarea, e.g:
    {
        "token": "ENTER THE ACTIVE REFRESH TOKEN HERE"
    }
  7. Click the "Send" button, you should receive a "200 OK" response with the message Token revoked.

NOTE: You can also revoke the token in the refreshToken cookie with the /users/revoke-token route, to revoke the refresh token cookie simply send the same request with an empty body.

Here's a screenshot of Postman after making the request and the token has been revoked:

 


Running an Angular app with the JWT Refresh Tokens API

For full details about the example Angular  application see the post Angular 9 - JWT Authentication with Refresh Tokens. But to get up and running quickly just follow the below steps.

  1. Download or clone the Angular 9 tutorial code from https://github.com/cornflourblue/angular-9-jwt-refresh-tokens
  2. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  3. Remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file.
  4. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the Angular example application and it should be hooked up with the Node + Mongo JWT Refresh Tokens API that you already have running.
 

Node.js + MongoDB API Project Structure

The tutorial project is structured into feature folders (users) and non-feature / shared component folders (_helpers, _middleware). Shared component folders contain code that can be used by multiple features and other parts of the application, and are prefixed with an underscore _ to group them together and make it easy to differentiate between feature folders and non-feature folders.

The example only contains a single (users) feature at the moment, but could be easily extended with other features by copying the users folder and following the same pattern.

Click any of the below links to jump down to a description of each file along with its code:

 

Helpers Folder

Path: /_helpers

The helpers folder contains all the bits and pieces that don't fit into other folders but don't justify having a folder of their own.

 

Create Test User Helper

Path: /_helpers/create-test-user.js

The create test user helper function is executed when the api starts (from server.js) to create a user in the MongoDB database for testing the api.

const bcrypt = require('bcryptjs');
const db = require('./db');
const Role = require('./role');

module.exports = createTestUser;

async function createTestUser() {
    // create test user if the db is empty
    if ((await db.User.countDocuments({})) === 0) {
        const user = new db.User({
            firstName: 'Test',
            lastName: 'User',
            username: 'test',
            passwordHash: bcrypt.hashSync('test', 10),
            role: Role.Admin
        });
        await user.save();
    }
}
 

Mongo Database Wrapper

Path: /_helpers/db.js

The MongoDB wrapper connects to MongoDB using Mongoose and exports an object containing all of the database model objects in the application (currently only User and RefreshToken). It provides an easy way to access any part of the database from a single point.

It also contains the isValidId() utility function to enable checking if an id is a valid Mongo ObjectId before attempting to run a query.

const config = require('config.json');
const mongoose = require('mongoose');
const connectionOptions = { useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false };
mongoose.connect(process.env.MONGODB_URI || config.connectionString, connectionOptions);
mongoose.Promise = global.Promise;

module.exports = {
    User: require('users/user.model'),
    RefreshToken: require('users/refresh-token.model'),
    isValidId
};

function isValidId(id) {
    return mongoose.Types.ObjectId.isValid(id);
}
 

Role Object / Enum

Path: /_helpers/role.js

The role object defines the all the roles in the example application, I created it to use like an enum to avoid passing roles around as strings, so instead of 'Admin' and 'User' we can use Role.Admin and Role.User.

module.exports = {
    Admin: 'Admin',
    User: 'User'
}
 

Swagger API Docs Route Handler (/api-docs)

Path: /_helpers/swagger.js

The Swagger docs route handler uses the Swagger UI Express module to serve auto-generated Swagger UI documentation based on the swagger.yaml file from the /api-docs path of the api. The route handler is bound to the /api-docs path in the main server.js file.

For more info on swagger-ui-express see https://www.npmjs.com/package/swagger-ui-express.

const express = require('express');
const router = express.Router();
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const swaggerDocument = YAML.load('./swagger.yaml');

router.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

module.exports = router;
 

Express.js Middleware Folder

Path: /_middleware

The middleware folder contains Express.js middleware functions that can be used by different routes / features within the application.

 

Authorize Middleware

Path: /_middleware/authorize.js

The authorize middleware can be added to any route to restrict access to the route to authenticated users with specified roles. If the roles parameter is omitted (i.e. authorize()) then the route will be accessible to all authenticated users regardless of role. It is used by the users controller to restrict access to user details routes and the revoke token route.

The authorize function returns an array containing two middleware functions:

  • The first (jwt({ ... })) authenticates the request by validating the JWT access token in the "Authorization" header of the http request. On successful authentication a user object is attached to the req object that contains the data from the JWT token, which in this example includes the user id (req.user.id).
  • The second authorizes the request by checking that the authenticated user still exists and is authorized to access the requested route based on their role. The second middleware function also attaches the role property and the ownsToken method to the req.user object so they can be accessed by controller functions.

If either authentication or authorization fails then a 401 Unauthorized response is returned.

const jwt = require('express-jwt');
const { secret } = require('config.json');
const db = require('_helpers/db');

module.exports = authorize;

function authorize(roles = []) {
    // roles param can be a single role string (e.g. Role.User or 'User') 
    // or an array of roles (e.g. [Role.Admin, Role.User] or ['Admin', 'User'])
    if (typeof roles === 'string') {
        roles = [roles];
    }

    return [
        // authenticate JWT token and attach user to request object (req.user)
        jwt({ secret, algorithms: ['HS256'] }),

        // authorize based on user role
        async (req, res, next) => {
            const user = await db.User.findById(req.user.id);

            if (!user || (roles.length && !roles.includes(user.role))) {
                // user no longer exists or role not authorized
                return res.status(401).json({ message: 'Unauthorized' });
            }

            // authentication and authorization successful
            req.user.role = user.role;
            const refreshTokens = await db.RefreshToken.find({ user: user.id });
            req.user.ownsToken = token => !!refreshTokens.find(x => x.token === token);
            next();
        }
    ];
}
 

Global Error Handler Middleware

Path: /_middleware/error-handler.js

The global error handler is used catch all errors and return an error response, it removes the need for duplicated error handling code throughout the application. It's configured as middleware in the main server.js file.

By convention errors of type 'string' are treated as custom (app specific) errors, this simplifies the code for throwing custom errors since only a string needs to be thrown (e.g. throw 'Invalid token'). Further to this if a custom error ends with the words 'not found' a 404 response code is returned, otherwise a standard 400 response is returned. See the user service for some examples of custom errors thrown by the api, errors are caught in the users controller for each route and passed to the next function which passes them to this global error handler.

module.exports = errorHandler;

function errorHandler(err, req, res, next) {
    switch (true) {
        case typeof err === 'string':
            // custom application error
            const is404 = err.toLowerCase().endsWith('not found');
            const statusCode = is404 ? 404 : 400;
            return res.status(statusCode).json({ message: err });
        case err.name === 'ValidationError':
            // mongoose validation error
            return res.status(400).json({ message: err.message });
        case err.name === 'UnauthorizedError':
            // jwt authentication error
            return res.status(401).json({ message: 'Unauthorized' });
        default:
            return res.status(500).json({ message: err.message });
    }
}
 

Validate Request Middleware

Path: /_middleware/validate-request.js

The validate request middleware function validates the body of a request against a Joi schema object.

It used by schema middleware functions in controllers to validate the request against the schema for a specific route (e.g. authenticateSchema in the users controller).

module.exports = validateRequest;

function validateRequest(req, next, schema) {
    const options = {
        abortEarly: false, // include all errors
        allowUnknown: true, // ignore unknown props
        stripUnknown: true // remove unknown props
    };
    const { error, value } = schema.validate(req.body, options);
    if (error) {
        next(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
    } else {
        req.body = value;
        next();
    }
}
 

Users Feature Folder

Path: /users

The users folder contains all code that is specific to the users feature of the api.

 

Mongoose User Model

Path: /users/user.model.js

The user model uses Mongoose to define the schema for the users collection in the MongoDB database. The exported Mongoose model object gives full access to perform CRUD (create, read, update, delete) operations on users in MongoDB, see the user service below for examples of it being used (via the db helper).

The schema defines the properties in MongoDB for user records, the virtual properties are convenience properties available on the mongoose model that don't get persisted to MongoDB.

schema.set('toJSON', { ... }); configures which user properties are included when converting MongoDB records to JSON objects:

  • virtuals: true includes the Mongoose virtual properties in the output JSON, including an id property which is a copy of the MongoDB _id property.
  • versionKey: false excludes the Mongoose version key (__v).
  • transform: function (doc, ret) { ... } removes the MongoDB _id and passwordHash properties when converting records to JSON.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const schema = new Schema({
    firstName: { type: String, required: true },
    lastName: { type: String, required: true },
    username: { type: String, unique: true, required: true },
    passwordHash: { type: String, required: true },
    role: { type: String, required: true }
});

schema.set('toJSON', {
    virtuals: true,
    versionKey: false,
    transform: function (doc, ret) {
        // remove these props when object is serialized
        delete ret._id;
        delete ret.passwordHash;
    }
});

module.exports = mongoose.model('User', schema);
 

Mongoose Refresh Token Model

Path: /users/refresh-token.model.js

The refresh token model uses Mongoose to define the schema for the refreshtokens collection in the MongoDB database. The exported Mongoose model object gives full access to perform CRUD (create, read, update, delete) operations on refresh tokens in MongoDB, see the user service below for examples of it being used (via the db helper).

The schema defines the properties in MongoDB for refresh token records, each token references the user that it belongs to via the user ref property.

The virtual properties are convenience properties available on the mongoose model that don't get persisted to MongoDB.

schema.set('toJSON', { ... }); configures which refresh token properties are included when converting MongoDB records to JSON objects:

  • virtuals: true includes the Mongoose virtual properties in the output JSON.
  • versionKey: false excludes the Mongoose version key (__v).
  • transform: function (doc, ret) { ... } removes some unneeded properties when converting refresh token records to JSON.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const schema = new Schema({
    user: { type: Schema.Types.ObjectId, ref: 'User' },
    token: String,
    expires: Date,
    created: { type: Date, default: Date.now },
    createdByIp: String,
    revoked: Date,
    revokedByIp: String,
    replacedByToken: String
});

schema.virtual('isExpired').get(function () {
    return Date.now() >= this.expires;
});

schema.virtual('isActive').get(function () {
    return !this.revoked && !this.isExpired;
});

schema.set('toJSON', {
    virtuals: true,
    versionKey: false,
    transform: function (doc, ret) {
        // remove these props when object is serialized
        delete ret._id;
        delete ret.id;
        delete ret.user;
    }
});

module.exports = mongoose.model('RefreshToken', schema);
 

User Service

Path: /users/user.service.js

The user service contains the core logic for authentication, generating JWT and refresh tokens, refreshing and revoking tokens, and fetching user data. The service encapsulates all interaction with the mongoose user models and exposes a simple set of methods which are used by the users controller.

The top of the file contains the exported service object with just the method names to make it easy to see all the methods at a glance, the rest of the file contains the implementation functions for each service method, followed by local helper functions.

const config = require('config.json');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require("crypto");
const db = require('_helpers/db');

module.exports = {
    authenticate,
    refreshToken,
    revokeToken,
    getAll,
    getById,
    getRefreshTokens
};

async function authenticate({ username, password, ipAddress }) {
    const user = await db.User.findOne({ username });

    if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
        throw 'Username or password is incorrect';
    }

    // authentication successful so generate jwt and refresh tokens
    const jwtToken = generateJwtToken(user);
    const refreshToken = generateRefreshToken(user, ipAddress);

    // save refresh token
    await refreshToken.save();

    // return basic details and tokens
    return { 
        ...basicDetails(user),
        jwtToken,
        refreshToken: refreshToken.token
    };
}

async function refreshToken({ token, ipAddress }) {
    const refreshToken = await getRefreshToken(token);
    const { user } = refreshToken;

    // replace old refresh token with a new one and save
    const newRefreshToken = generateRefreshToken(user, ipAddress);
    refreshToken.revoked = Date.now();
    refreshToken.revokedByIp = ipAddress;
    refreshToken.replacedByToken = newRefreshToken.token;
    await refreshToken.save();
    await newRefreshToken.save();

    // generate new jwt
    const jwtToken = generateJwtToken(user);

    // return basic details and tokens
    return { 
        ...basicDetails(user),
        jwtToken,
        refreshToken: newRefreshToken.token
    };
}

async function revokeToken({ token, ipAddress }) {
    const refreshToken = await getRefreshToken(token);

    // revoke token and save
    refreshToken.revoked = Date.now();
    refreshToken.revokedByIp = ipAddress;
    await refreshToken.save();
}

async function getAll() {
    const users = await db.User.find();
    return users.map(x => basicDetails(x));
}

async function getById(id) {
    const user = await getUser(id);
    return basicDetails(user);
}

async function getRefreshTokens(userId) {
    // check that user exists
    await getUser(userId);

    // return refresh tokens for user
    const refreshTokens = await db.RefreshToken.find({ user: userId });
    return refreshTokens;
}

// helper functions

async function getUser(id) {
    if (!db.isValidId(id)) throw 'User not found';
    const user = await db.User.findById(id);
    if (!user) throw 'User not found';
    return user;
}

async function getRefreshToken(token) {
    const refreshToken = await db.RefreshToken.findOne({ token }).populate('user');
    if (!refreshToken || !refreshToken.isActive) throw 'Invalid token';
    return refreshToken;
}

function generateJwtToken(user) {
    // create a jwt token containing the user id that expires in 15 minutes
    return jwt.sign({ sub: user.id, id: user.id }, config.secret, { expiresIn: '15m' });
}

function generateRefreshToken(user, ipAddress) {
    // create a refresh token that expires in 7 days
    return new db.RefreshToken({
        user: user.id,
        token: randomTokenString(),
        expires: new Date(Date.now() + 7*24*60*60*1000),
        createdByIp: ipAddress
    });
}

function randomTokenString() {
    return crypto.randomBytes(40).toString('hex');
}

function basicDetails(user) {
    const { id, firstName, lastName, username, role } = user;
    return { id, firstName, lastName, username, role };
}
 

Express.js Users Controller

Path: /users/users.controller.js

The users controller defines all /users routes for the api, the route definitions are grouped together at the top of the file and the implementation functions are below, followed by local helper functions. The controller is bound to the /users path in the main server.js file.

Routes that require authorization include the middleware function authorize() and optionally specify a role (e.g. authorize(Role.Admin), if a role is specified then the route is restricted to users in that role, otherwise the route is restricted to all authenticated users regardless of role. The auth logic is located in the authorize middleware.

The route functions revokeToken, getById and getRefreshTokens include an extra custom authorization check to prevent non-admin users from accessing users other than their own. So regular users (Role.User) have access to their own account but not to others, and admin users (Role.Admin) have full access to all accounts.

Routes that require schema validation include a middleware function with the naming convention <route>Schema (e.g. authenticateSchema). Each schema validation function defines a schema for the request body using the Joi (@hapi/joi) library and calls validateRequest(req, next, schema) to ensure the request body is valid. If validation succeeds the request continues to the next middleware function (the route function), otherwise an error is returned with details of why validation failed. For more info about Joi schema validation see https://hapi.dev/module/joi/

Express is the web server used by the api, it's one of the most popular web application frameworks for Node.js. For more info see https://expressjs.com/.

const express = require('express');
const router = express.Router();
const Joi = require('@hapi/joi');
const validateRequest = require('_middleware/validate-request');
const authorize = require('_middleware/authorize')
const Role = require('_helpers/role');
const userService = require('./user.service');

// routes
router.post('/authenticate', authenticateSchema, authenticate);
router.post('/refresh-token', refreshToken);
router.post('/revoke-token', authorize(), revokeTokenSchema, revokeToken);
router.get('/', authorize(Role.Admin), getAll);
router.get('/:id', authorize(), getById);
router.get('/:id/refresh-tokens', authorize(), getRefreshTokens);

module.exports = router;

function authenticateSchema(req, res, next) {
    const schema = Joi.object({
        username: Joi.string().required(),
        password: Joi.string().required()
    });
    validateRequest(req, next, schema);
}

function authenticate(req, res, next) {
    const { username, password } = req.body;
    const ipAddress = req.ip;
    userService.authenticate({ username, password, ipAddress })
        .then(({ refreshToken, ...user }) => {
            setTokenCookie(res, refreshToken);
            res.json(user);
        })
        .catch(next);
}

function refreshToken(req, res, next) {
    const token = req.cookies.refreshToken;
    const ipAddress = req.ip;
    userService.refreshToken({ token, ipAddress })
        .then(({ refreshToken, ...user }) => {
            setTokenCookie(res, refreshToken);
            res.json(user);
        })
        .catch(next);
}

function revokeTokenSchema(req, res, next) {
    const schema = Joi.object({
        token: Joi.string().empty('')
    });
    validateRequest(req, next, schema);
}

function revokeToken(req, res, next) {
    // accept token from request body or cookie
    const token = req.body.token || req.cookies.refreshToken;
    const ipAddress = req.ip;

    if (!token) return res.status(400).json({ message: 'Token is required' });

    // users can revoke their own tokens and admins can revoke any tokens
    if (!req.user.ownsToken(token) && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    userService.revokeToken({ token, ipAddress })
        .then(() => res.json({ message: 'Token revoked' }))
        .catch(next);
}

function getAll(req, res, next) {
    userService.getAll()
        .then(users => res.json(users))
        .catch(next);
}

function getById(req, res, next) {
    // regular users can get their own record and admins can get any record
    if (req.params.id !== req.user.id && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    userService.getById(req.params.id)
        .then(user => user ? res.json(user) : res.sendStatus(404))
        .catch(next);
}

function getRefreshTokens(req, res, next) {
    // users can get their own refresh tokens and admins can get any user's refresh tokens
    if (req.params.id !== req.user.id && req.user.role !== Role.Admin) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    userService.getRefreshTokens(req.params.id)
        .then(tokens => tokens ? res.json(tokens) : res.sendStatus(404))
        .catch(next);
}

// helper functions

function setTokenCookie(res, token)
{
    // create http only cookie with refresh token that expires in 7 days
    const cookieOptions = {
        httpOnly: true,
        expires: new Date(Date.now() + 7*24*60*60*1000)
    };
    res.cookie('refreshToken', token, cookieOptions);
}
 

Api Config

Path: /config.json

The api config file contains configuration data for the api, it includes the connectionString to the MongoDB database and the secret used for signing and verifying JWT tokens.

IMPORTANT: The secret property is used to sign and verify JWT tokens for authentication, change it with your own random string to ensure nobody else can generate a JWT with the same secret to gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).

{
    "connectionString": "mongodb://localhost/node-mongo-jwt-refresh-tokens-api",
    "secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies which get installed when you run npm install.

The scripts section contains scripts that are executed by running the command npm run <script name>, the start script can also be run with the shortcut command npm start.

The start script starts the api normally using node, and the start:dev script starts the api in development mode using nodemon which automatically restarts the server when a file is changed.

For more info see https://docs.npmjs.com/files/package.json.

{
    "name": "node-mongo-jwt-refresh-tokens-api",
    "version": "1.0.0",
    "description": "Node.js + MongoDB API - JWT Authentication with Refresh Tokens",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": "https://github.com/cornflourblue/node-mongo-jwt-refresh-tokens-api.git"
    },
    "scripts": {
        "start": "node ./server.js",
        "start:dev": "nodemon ./server.js"
    },
    "dependencies": {
        "@hapi/joi": "^17.1.1",
        "bcryptjs": "^2.4.3",
        "body-parser": "^1.19.0",
        "cookie-parser": "^1.4.5",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "express-jwt": "^6.0.0",
        "jsonwebtoken": "^8.5.1",
        "mongodb": "^3.5.7",
        "mongoose": "^5.9.21",
        "rootpath": "^0.1.2",
        "swagger-ui-express": "^4.1.4",
        "yamljs": "^0.3.0"
    },
    "devDependencies": {
        "nodemon": "^2.0.3"
    }
}
 

Server Startup File

Path: /server.js

The server.js file is the entry point into the Node api, it configures application middleware, binds controllers to routes and starts the Express web server for the api.

require('rootpath')();
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const errorHandler = require('_middleware/error-handler');

// create test user in db on startup if required
const createTestUser = require('_helpers/create-test-user');
createTestUser();

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

// allow cors requests from any origin and with credentials
app.use(cors({ origin: (origin, callback) => callback(null, true), credentials: true }));

// api routes
app.use('/users', require('./users/users.controller'));

// swagger docs route
app.use('/api-docs', require('_helpers/swagger'));

// global error handler
app.use(errorHandler);

// start server
const port = process.env.NODE_ENV === 'production' ? (process.env.PORT || 80) : 4000;
app.listen(port, () => {
    console.log('Server listening on port ' + port);
});
 

Swagger API Documentation

Path: /swagger.yaml

The Swagger YAML file describes the entire Node API using the OpenAPI Specification format, it includes descriptions of all routes and HTTP methods on each route, request and response schemas, path parameters, and authentication methods.

The YAML documentation is used by the swagger.js helper to automatically generate and serve interactive Swagger UI documentation on the /api-docs route of the api. To preview the Swagger UI documentation without running the api simply copy and paste the below YAML into the swagger editor at https://editor.swagger.io/.

openapi: 3.0.0
info:
  title: Node.js + MongoDB API - JWT Authentication with Refresh Tokens
  version: 1.0.0

servers:
  - url: http://localhost:4000
    description: Local development server

paths:
  /users/authenticate:
    post:
      summary: Authenticate user credentials and return a JWT token and a cookie with a refresh token
      operationId: authenticate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
                  example: "jason"
                password:
                  type: string
                  example: "pass123"
              required:
                - username
                - password
      responses:
        "200":
          description: User details, a JWT access token and a refresh token cookie
          headers:
            Set-Cookie:
              description: "`refreshToken`"
              schema:
                type: string
                example: refreshToken=51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550; Path=/; Expires=Tue, 16 Jun 2020 09:14:17 GMT; HttpOnly
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  username:
                    type: string
                    example: "jason"
                  role:
                    type: string
                    example: "Admin"
                  jwtToken:
                    type: string
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWIxMmUxOTdlMDZhNzZjY2RlZmMxMjEiLCJpZCI6IjVlYjEyZTE5N2UwNmE3NmNjZGVmYzEyMSIsImlhdCI6MTU4ODc1ODE1N30.xR9H0STbFOpSkuGA9jHNZOJ6eS7umHHqKRhI807YT1Y"
        "400":
          description: The username or password is incorrect
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Username or password is incorrect"
  /users/refresh-token:
    post:
      summary: Use a refresh token to generate a new JWT token and a new refresh token
      description: The refresh token is sent and returned via cookies.
      operationId: refreshToken
      parameters:
        - in: cookie
          name: refreshToken
          description: The `refreshToken` cookie
          schema:
            type: string
            example: 51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550
      responses:
        "200":
          description: User details, a JWT access token and a new refresh token cookie
          headers:
            Set-Cookie:
              description: "`refreshToken`"
              schema:
                type: string
                example: refreshToken=51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550; Path=/; Expires=Tue, 16 Jun 2020 09:14:17 GMT; HttpOnly
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  username:
                    type: string
                    example: "jason"
                  role:
                    type: string
                    example: "Admin"
                  jwtToken:
                    type: string
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWIxMmUxOTdlMDZhNzZjY2RlZmMxMjEiLCJpZCI6IjVlYjEyZTE5N2UwNmE3NmNjZGVmYzEyMSIsImlhdCI6MTU4ODc1ODE1N30.xR9H0STbFOpSkuGA9jHNZOJ6eS7umHHqKRhI807YT1Y"
        "400":
          description: The refresh token is invalid, revoked or expired
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
  /users/revoke-token:
    post:
      summary: Revoke a refresh token
      description: Admin users can revoke the tokens of any user, regular users can only revoke their own tokens.
      operationId: revokeToken
      security:
        - bearerAuth: []
      parameters:
        - in: cookie
          name: refreshToken
          description: The refresh token can be sent in a cookie or the post body, if both are sent the token in the body is used.
          schema:
            type: string
            example: 51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                token:
                  type: string
                  example: "51872eca5efedcf424db4cf5afd16a9d00ad25b743a034c9c221afc85d18dcd5e4ad6e3f08607550"
      responses:
        "200":
          description: The refresh token was successfully revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Token revoked"
        "400":
          description: The refresh token is invalid
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Invalid token"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
  /users:
    get:
      summary: Get a list of all users
      description: Restricted to admin users.
      operationId: getAllUsers
      security:
        - bearerAuth: []
      responses:
        "200":
          description: An array of all users
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      example: "5eb12e197e06a76ccdefc121"
                    firstName:
                      type: string
                      example: "Jason"
                    lastName:
                      type: string
                      example: "Watmore"
                    username:
                      type: string
                      example: "jason"
                    role:
                      type: string
                      example: "Admin"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
  /users/{id}:
    parameters:
      - in: path
        name: id
        description: User id
        required: true
        example: "5eb12e197e06a76ccdefc121"
        schema:
          type: string
    get:
      summary: Get a single user by id
      description: Admin users can access any user record, regular users are restricted to their own user record.
      operationId: getUserById
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Details of the specified user
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    example: "5eb12e197e06a76ccdefc121"
                  firstName:
                    type: string
                    example: "Jason"
                  lastName:
                    type: string
                    example: "Watmore"
                  username:
                    type: string
                    example: "jason"
                  role:
                    type: string
                    example: "Admin"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
  /users/{id}/refresh-tokens:
    parameters:
      - in: path
        name: id
        description: User id
        required: true
        example: "5eb12e197e06a76ccdefc121"
        schema:
          type: string
    get:
      summary: Get a list of all refresh tokens (active and revoked) of the user with the specified id
      description: Admin users can access any user's refresh tokens, regular users are restricted to their own refresh tokens.
      operationId: getRefreshTokens
      security:
        - bearerAuth: []
      responses:
        "200":
          description: An array of refresh tokens for the specified user id
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    token:
                      type: string
                      example: "79ea9a5e825da7c27d30839c89295071842f2a44b22e917aaf795126f4486509d8511c6fdedb6f1e"
                    expires:
                      type: string
                      example: "2020-06-24T03:29:13.871Z"
                    created:
                      type: string
                      example: "2020-06-17T03:29:13.871Z"
                    createdByIp:
                      type: string
                      example: "127.0.0.1"
                    isExpired:
                      type: boolean
                      example: false
                    isActive:
                      type: boolean
                      example: true
                    revoked:
                      type: string
                      example: "2020-06-17T03:29:13.871Z"
                    revokedByIp:
                      type: string
                      example: "127.0.0.1"                    
                    replacedByToken:
                      type: string
                      example: "a01d3818db64961742f249beeded65739e9c3d1019570ea48ea820d274eac607043a6cbefd23c297"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  responses:
    UnauthorizedError:
      description: Access token is missing or invalid, or the user does not have access to perform the action
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: "Unauthorized"
    NotFoundError:
      description: Not Found
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: "Not Found"

 


Need Some NodeJS Help?

Search fiverr for freelance NodeJS developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by