Node.js + MongoDB API - JWT Authentication with Refresh Tokens
Tutorial built with Node.js and MongoDB
Other versions available:
- .NET: .NET 6.0, 5.0, ASP.NET Core 3.1
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 Node.js + MongoDB applications
- Running the example API locally
- Testing the API with Postman
- Running an Angular 9 app with the Node + Mongo API
- Node.js + MongoDB API project structure
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
- Run MongoDB, instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
- Download or clone the project source code from https://github.com/cornflourblue/node-mongo-jwt-refresh-tokens-api
- Install all required npm packages by running
npm install
ornpm i
from the command line in the project root folder (where the package.json is located). - Start the api by running
npm start
(ornpm run start:dev
to start with nodemon) from the command line in the project root folder, you should see the messageServer listening on port 4000
, and you can view the Swagger API documentation athttp://localhost:4000/api-docs
. - 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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/users/authenticate
. - Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- Enter a JSON object containing the test username and password in the "Body" textarea:
{ "username": "test", "password": "test" }
- 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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the refresh token route of your local API -
http://localhost:4000/users/refresh-token
. - 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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the users route of your local API -
http://localhost:4000/users
. - 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.
- 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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the users route of your local API -
http://localhost:4000/users/1/refresh-tokens
. - 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.
- 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:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/users/revoke-token
. - 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.
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON".
- 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" }
- 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.
- Download or clone the Angular 9 tutorial code from https://github.com/cornflourblue/angular-9-jwt-refresh-tokens
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Remove or comment out the line below the comment
// provider used to create fake backend
located in the/src/app/app.module.ts
file. - 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
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
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
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
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
)
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
The middleware folder contains Express.js middleware functions that can be used by different routes / features within the application.
Authorize Middleware
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 auser
object is attached to thereq
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 therole
property and theownsToken
method to thereq.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
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
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
The users folder contains all code that is specific to the users feature of the api.
Mongoose User Model
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 anid
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
andpasswordHash
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
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
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
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
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
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
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
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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!