Login App with CSRF protection - Implement authentication in Node.js using JWT access token and refresh token - Part 2
In this article, we will show you how to implement authentication in Node.js using JWT access token and refresh token. As we have already discussed about the implementation flow of the authentication a.k.a secure login app with CSRF protection in the previous article. So we will cover only Node.js implementation in this second part of the series.
Node.js API Authentication With JWT, Add Login Using the Authorization Code Flow, Refresh Tokens With JWT Authentication, JWT Refresh Token for Multiple Devices, token based authentication in node js example, bearer token build a node.js api authentication with jwt tutorial, step by step explanation of the authentication in Node js using JWT access token, refresh token and CSRF token. JWT authentication in backend technologies like laravel, php, etc, Requesting access tokens and authorization codes. Secure authentication in node js using JWT access token, refresh token, CSRF protection and XSS protection.
We had already discussed the authentication with Node.js in the previous articles where we explained the basic way to manage the authentication using JWT token and moreover here we will learn about the security.
Checkout more articles on Node.js
- How to deploy a react app to node server
- How to enable CORS for multiple domains in Node.js
- Socket.IO – How to implement Socket.IO in Node.js
- File Upload in Node.js
We planned to divide this article into three parts.
- Part 1 – Understanding authentication using JWT access token and refresh token with CSRF protection
- Part 2 – Implement authentication in Node.js using JWT token with CSRF protection (You are here…)
- Part 3 – Implement authentication in ReactJS using REST API with CSRF protection
Way to create secure REST API in Node.js using JWT token with CSRF protection
- Create simple REST API in Node.js
- Install npm dependencies
- Define the environment variables
- Manage general utility
- Create API for user sign in
- Create API for user sign out
- Create API to verify token
- Create API to get user list
- Implement middleware to validate the token
- Output
1. Create simple REST API in Node.js
As we are planning to create a REST API in Node.js so we should have an initial setup of the Node.js application. Checkout this link for your reference.
Below is the file structure of the application that you should prefer.
File Structure - Clue Mediator
2. Install npm dependencies
We should have the following dependencies in the Node application.
- dotenv - It helps us to load the `.env` variables into `process.env`. With the help of it we can access those variables throughout the application.
- cors- To access resources from the different server, we have to enable the Cross-Origin Resource Sharing (CORS) request.
- jsonwebtoken- We’ll use this package to create access token and refresh token by passing the primary JSON data and secret key.
- body-parser- It used to parse the incoming request and make it available under req.body property.
- cookie-parser - This package is used to parse the cookies and also used to enable the signed cookie support by passing a secret key.
- rand-token - Additionally we will use this package to create a random token for CSRF protection.
- moment - Here we’ll use the moment npm package to set the expiry date of the token.
- ms - Use this package to convert the time format to milliseconds.
Run the following command to install the required dependencies.
npm i dotenv cors jsonwebtoken body-parser cookie-parser rand-token moment ms
After successful installation, We will use these packages in several files for different functionalities. For now your `server.js` file should look like this and later on we’ll see the `utils.js` file.
server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const app = express();
const port = process.env.PORT || 4000;
// enable CORS
app.use(cors({
origin: 'http://localhost:3000', // url of the frontend application
credentials: true // set credentials true for secure httpOnly cookie
}));
// parse application/json
app.use(bodyParser.json());
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
// use cookie parser for secure httpOnly cookie
app.use(cookieParser(process.env.COOKIE_SECRET));
app.listen(port, () => {
console.log('Server started on: ' + port);
});
In above code, we have added only those packages that are used in this file only and rest of the packages we will add it in `utils.js` file.
If you noticed that we have set `http://localhost:3000` to origin in configuration of the CORS package that means we will allow only this url to access our API. Also we will use secure HttpOnly cookies for authentication so we need to set `credentials: true` in the same configuration.
For secure HttpOnly cookies, we have to use a secret key for cookie parsing. Here we are assigning the cookie secret from the environment file that we covered in the next topic.
3. Define the environment variables
Let’s try to create a `.env` file to define the environment variables and access it through `process.env`.
.env
JWT_SECRET=ABCDEF$123
ACCESS_TOKEN_LIFE=15m
REFRESH_TOKEN_LIFE=30d
COOKIE_SECRET=XYZABC$123
NODE_ENV=development
Following keys to use in the project.
- JWT_SECRET - Use it to create JWT access token and refresh token.
- ACCESS_TOKEN_LIFE - Define the life of the access token so we can use it to set the expiry time of the access token. We have considered the 15 mins (15m).
- REFRESH_TOKEN_LIFE - Same as the access token we will define the life of the refresh token. It should be longer than the access token so we have considered the 30 days (30d).
- COOKIE_SECRET - We’ll use this secret key for cookie parsing as we discussed in the above topic.
- NODE_ENV - It’s used to set the environment of the project.
To use all these variables, we have to add `require('dotenv').config();` at the top of the `server.js` file that we already added in above `server.js` file code.
4. Manage general utility
Now it’s time to create the several functions that help us to create an authentication API. We have listed the following utilities.
-
generateToken
This function will be used to create an access token, CSRF token and expiry time with the help of the jsonwebtoken, rand-token and moment npm packages.
In the first step, we will create a plain object to generate an access token. We will also create a CSRF token in the same function so that can be used to create a private key for the access token.
Let’s create a private key by combining the CSRF or XSRF token and JWT secret key that is defined in the `.env` file. CSRF token will help us to identify the real user when someone calls the API from the frontend side.
So now we are able to create an access token with the help of the plain object and the private key. Also we will set the life of the access token.
At last we have to create a variable that contains the expiry time of the token which will pass in the API response.
const jwt = require('jsonwebtoken');
const moment = require('moment');
const randtoken = require('rand-token');
const ms = require(‘ms’);
// generate tokens and return it
function generateToken(user) {
//1. Don't use password and other sensitive fields
//2. Use the information that are useful in other parts
if (!user) return null;
const u = {
userId: user.userId,
name: user.name,
username: user.username,
isAdmin: user.isAdmin
};
// generat xsrf token and use it to generate access token
const xsrfToken = randtoken.generate(24);
// create private key by combining JWT secret and xsrf token
const privateKey = process.env.JWT_SECRET + xsrfToken;
// generate access token and expiry date
const token = jwt.sign(u, privateKey, { expiresIn: process.env.ACCESS_TOKEN_LIFE });
// expiry time of the access token
const expiredAt = moment().add(ms(process.env.ACCESS_TOKEN_LIFE), 'ms').valueOf();
return {
token,
expiredAt,
xsrfToken
}
}
We’ll call this function to create an access token and CSRF token before it’s expired. For us we have to generate tokens in every 15 mins.
-
generateRefreshToken
We will create another function that will return the refresh token using JWT. We will consider the user id, JWT secret and expiry time to create a refresh token.
// generate refresh token
function generateRefreshToken(userId) {
if (!userId) return null;
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: process.env.REFRESH_TOKEN_LIFE });
}
Here we will not use the XSRF token as a private key to generate refresh token because XSRF and access token are required to generate in short time but refresh token have a longer life than access token.
-
verifyToken
Let’s create a function that will be used to verify the both access and refresh token. We will use `xsrfToken` as optional parameters to create a private key for both tokens.
// verify access token and refresh token
function verifyToken(token, xsrfToken, cb) {
const privateKey = process.env.JWT_SECRET + xsrfToken;
jwt.verify(token, privateKey, cb);
}
We will pass the empty string for `xsrfToken` to verify the refresh token. We should have a callback function to process the further request that will be received via the third parameter.
-
getCleanUser
As per the previous article of the authentication REST API, We also need a simple function that returns the basic user information so we call pass it to the user in the API response.
// return basic user details
function getCleanUser(user) {
if (!user) return null;
return {
userId: user.userId,
name: user.name,
username: user.username,
isAdmin: user.isAdmin
};
}
-
handleResponse
Let’s create a function that helps us to create the response based on status code and some other required details.
// handle the API response
function handleResponse(req, res, statusCode, data, message) {
let isError = false;
let errorMessage = message;
switch (statusCode) {
case 204:
return res.sendStatus(204);
case 400:
isError = true;
break;
case 401:
isError = true;
errorMessage = message || 'Invalid user.';
clearTokens(req, res);
break;
case 403:
isError = true;
errorMessage = message || 'Access to this resource is denied.';
clearTokens(req, res);
break;
default:
break;
}
const resObj = data || {};
if (isError) {
resObj.error = true;
resObj.message = errorMessage;
}
return res.status(statusCode).json(resObj);
}
-
clearTokens
At last, we need a function that helps us to clear the tokens from the memory and cookie.
Here we defined the variable `refreshTokens` to manage the CSRF or XSRF token that links with refresh token in the form of the object. You can also manage it on redis server.
Also we will have a cookie option to create a secure HttpOnly cookie for refresh token and the same option will be used to remove token from the cookie. You can set the domain as well.
To access a signed cookie, we have to use the `signedCookies` object of `req`.
const dev = process.env.NODE_ENV !== 'production';
// refresh token list to manage the xsrf token
const refreshTokens = {};
// cookie options to create refresh token
const COOKIE_OPTIONS = {
// domain: "localhost",
httpOnly: true,
secure: !dev,
signed: true
};
// clear tokens from cookie
function clearTokens(req, res) {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
delete refreshTokens[refreshToken];
res.clearCookie('XSRF-TOKEN');
res.clearCookie('refreshToken', COOKIE_OPTIONS);
}
Let’s combine all code together and see how it looks.
utils.js
const jwt = require('jsonwebtoken');
const moment = require('moment');
const randtoken = require('rand-token');
const ms = require('ms');
const dev = process.env.NODE_ENV !== 'production';
// refresh token list to manage the xsrf token
const refreshTokens = {};
// cookie options to create refresh token
const COOKIE_OPTIONS = {
// domain: "localhost",
httpOnly: true,
secure: !dev,
signed: true
};
// generate tokens and return it
function generateToken(user) {
//1. Don't use password and other sensitive fields
//2. Use the information that are useful in other parts
if (!user) return null;
const u = {
userId: user.userId,
name: user.name,
username: user.username,
isAdmin: user.isAdmin
};
// generat xsrf token and use it to generate access token
const xsrfToken = randtoken.generate(24);
// create private key by combining JWT secret and xsrf token
const privateKey = process.env.JWT_SECRET + xsrfToken;
// generate access token and expiry date
const token = jwt.sign(u, privateKey, { expiresIn: process.env.ACCESS_TOKEN_LIFE });
// expiry time of the access token
const expiredAt = moment().add(ms(process.env.ACCESS_TOKEN_LIFE), 'ms').valueOf();
return {
token,
expiredAt,
xsrfToken
}
}
// generate refresh token
function generateRefreshToken(userId) {
if (!userId) return null;
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: process.env.REFRESH_TOKEN_LIFE });
}
// verify access token and refresh token
function verifyToken(token, xsrfToken = '', cb) {
const privateKey = process.env.JWT_SECRET + xsrfToken;
jwt.verify(token, privateKey, cb);
}
// return basic user details
function getCleanUser(user) {
if (!user) return null;
return {
userId: user.userId,
name: user.name,
username: user.username,
isAdmin: user.isAdmin
};
}
// handle the API response
function handleResponse(req, res, statusCode, data, message) {
let isError = false;
let errorMessage = message;
switch (statusCode) {
case 204:
return res.sendStatus(204);
case 400:
isError = true;
break;
case 401:
isError = true;
errorMessage = message || 'Invalid user.';
clearTokens(req, res);
break;
case 403:
isError = true;
errorMessage = message || 'Access to this resource is denied.';
clearTokens(req, res);
break;
default:
break;
}
const resObj = data || {};
if (isError) {
resObj.error = true;
resObj.message = errorMessage;
}
return res.status(statusCode).json(resObj);
}
// clear tokens from cookie
function clearTokens(req, res) {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
delete refreshTokens[refreshToken];
res.clearCookie('XSRF-TOKEN');
res.clearCookie('refreshToken', COOKIE_OPTIONS);
}
module.exports = {
refreshTokens,
COOKIE_OPTIONS,
generateToken,
generateRefreshToken,
verifyToken,
getCleanUser,
handleResponse,
clearTokens
}
5. Create API for user sign in
Before we create an API, we can create a static list of the users that used to validate the user’s credential. In your case it should be a database.
// list of the users to be consider as a database for example
const userList = [
{
userId: "123",
password: "clue",
name: "Clue",
username: "clue",
isAdmin: true
},
{
userId: "456",
password: "mediator",
name: "Mediator",
username: "mediator",
isAdmin: true
},
{
userId: "789",
password: "123456",
name: "Clue Mediator",
username: "cluemediator",
isAdmin: true
}
]
We will also add all dependencies in `server.js` file from the `utils.js` file.
const {
refreshTokens, COOKIE_OPTIONS, generateToken, generateRefreshToken,
getCleanUser, verifyToken, clearTokens, handleResponse,
} = require('./utils');
Let’s create an API for sign-in where users can get authenticated by passing the username and password. On successful authentication, we will return the access token and expiry time along with the user details.
// validate user credentials
app.post('/users/signin', function (req, res) {
const user = req.body.username;
const pwd = req.body.password;
// return 400 status if username/password is not exist
if (!user || !pwd) {
return handleResponse(req, res, 400, null, "Username and Password required.");
}
const userData = userList.find(x => x.username === user && x.password === pwd);
// return 401 status if the credential is not matched
if (!userData) {
return handleResponse(req, res, 401, null, "Username or Password is Wrong.");
}
// get basic user details
const userObj = getCleanUser(userData);
// generate access token
const tokenObj = generateToken(userData);
// generate refresh token
const refreshToken = generateRefreshToken(userObj.userId);
// refresh token list to manage the xsrf token
refreshTokens[refreshToken] = tokenObj.xsrfToken;
// set cookies
res.cookie('refreshToken', refreshToken, COOKIE_OPTIONS);
res.cookie('XSRF-TOKEN', tokenObj.xsrfToken);
return handleResponse(req, res, 200, {
user: userObj,
token: tokenObj.token,
expiredAt: tokenObj.expiredAt
});
});
In the above code we have added the validation for user credentials and create a plain object from user data. Also we are creating the access token, refresh token and CSRF/XSRF token with the help of the previously created function.
We will store the XSRF token in the variable `refreshTokens` that link with the refresh token in the form of the object. Also we will set XSRF and refresh tokens into the cookie so we can pass it along with the API response.
In the subsequent private requests, we will use all of the tokens (access token, refresh token and XSRF token) for verification when it is required.
6. Create API for user sign out
Now let’s create an API to manage the user logout. In this API, we have to simply call a function to clear tokens.
// handle user logout
app.post('/users/logout', (req, res) => {
clearTokens(req, res);
return handleResponse(req, res, 204);
});
7. Create API to verify token
Let’s have another API to verify the token. This API will be used to manage the silent authentication. Please refer to the link to get more idea about the silent authentication.
// verify the token and return new tokens if it's valid
app.post('/verifyToken', function (req, res) {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
if (!refreshToken) {
return handleResponse(req, res, 204);
}
// verify xsrf token
const xsrfToken = req.headers['x-xsrf-token'];
if (!xsrfToken || !(refreshToken in refreshTokens) || refreshTokens[refreshToken] !== xsrfToken) {
return handleResponse(req, res, 401);
}
// verify refresh token
verifyToken(refreshToken, '', (err, payload) => {
if (err) {
return handleResponse(req, res, 401);
}
else {
const userData = userList.find(x => x.userId === payload.userId);
if (!userData) {
return handleResponse(req, res, 401);
}
// get basic user details
const userObj = getCleanUser(userData);
// generate access token
const tokenObj = generateToken(userData);
// refresh token list to manage the xsrf token
refreshTokens[refreshToken] = tokenObj.xsrfToken;
res.cookie('XSRF-TOKEN', tokenObj.xsrfToken);
// return the token along with user details
return handleResponse(req, res, 200, {
user: userObj,
token: tokenObj.token,
expiredAt: tokenObj.expiredAt
});
}
});
});
In the above code, we are trying to get the refresh token from the cookies. If it’s not exist then we simply return the no content success response.
In the next step, we have to verify the XSRF token to prevent the CSRF attack.
After validating XSRF token, we will try to verify the refresh token with the help of the predefined function. Here we passed the xsrfToken as an empty string into the function because we have generated refresh token without XSRF token.
On successful validation, we have to return the user details, access token and expiry time into the API response.
No need to generate a new refresh token in this API request.
8. Create API to get user list
In this API, We have to created a private API that will return the list of the users. Without an access token you can not get this list. We have added `authMiddleware` middleware to validate the route. In the next point, we will explain about the middleware.
// get list of the users
app.get('/users/getList', authMiddleware, (req, res) => {
const list = userList.map(x => {
const user = { ...x };
delete user.password;
return user;
});
return handleResponse(req, res, 200, { random: Math.random(), userList: list });
});
Additionally, we have removed the password from the list before sending it via API response.
9. Implement middleware to validate the token
The middleware is the key part of the article to verify the private routes. It used to verify the access token and CSRF token from the request header.
// middleware that checks if JWT token exists and verifies it if it does exist.
// In all private routes, this helps to know if the request is authenticated or not.
const authMiddleware = function (req, res, next) {
// check header or url parameters or post parameters for token
var token = req.headers['authorization'];
if (!token) return handleResponse(req, res, 401);
token = token.replace('Bearer ', '');
// get xsrf token from the header
const xsrfToken = req.headers['x-xsrf-token'];
if (!xsrfToken) {
return handleResponse(req, res, 403);
}
// verify xsrf token
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
if (!refreshToken || !(refreshToken in refreshTokens) || refreshTokens[refreshToken] !== xsrfToken) {
return handleResponse(req, res, 401);
}
// verify token with secret key and xsrf token
verifyToken(token, xsrfToken, (err, payload) => {
if (err)
return handleResponse(req, res, 401);
else {
req.user = payload; //set the user to req so other routes can use it
next();
}
});
}
If we will not receive the token then we can consider as unauthorized user. If a token exists then try to get the XSRF token and verify it to avoid CSRF attack. After that we need to verify the token with the help of XSRF token.
If a user gets authenticated using access token and CSRF token then attach the user object in the same request so we can get the object in next routes.
10. Output
We have already provided the `.env` and `utils.js` files so now let’s combine all of the above code together and see the `server.js` file.
server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const {
refreshTokens, COOKIE_OPTIONS, generateToken, generateRefreshToken,
getCleanUser, verifyToken, clearTokens, handleResponse,
} = require('./utils');
const app = express();
const port = process.env.PORT || 4000;
// list of the users to be consider as a database for example
const userList = [
{
userId: "123",
password: "clue",
name: "Clue",
username: "clue",
isAdmin: true
},
{
userId: "456",
password: "mediator",
name: "Mediator",
username: "mediator",
isAdmin: true
},
{
userId: "789",
password: "123456",
name: "Clue Mediator",
username: "cluemediator",
isAdmin: true
}
]
// enable CORS
app.use(cors({
origin: 'http://localhost:3000', // url of the frontend application
credentials: true // set credentials true for secure httpOnly cookie
}));
// parse application/json
app.use(bodyParser.json());
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));
// use cookie parser for secure httpOnly cookie
app.use(cookieParser(process.env.COOKIE_SECRET));
// middleware that checks if JWT token exists and verifies it if it does exist.
// In all private routes, this helps to know if the request is authenticated or not.
const authMiddleware = function (req, res, next) {
// check header or url parameters or post parameters for token
var token = req.headers['authorization'];
if (!token) return handleResponse(req, res, 401);
token = token.replace('Bearer ', '');
// get xsrf token from the header
const xsrfToken = req.headers['x-xsrf-token'];
if (!xsrfToken) {
return handleResponse(req, res, 403);
}
// verify xsrf token
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
if (!refreshToken || !(refreshToken in refreshTokens) || refreshTokens[refreshToken] !== xsrfToken) {
return handleResponse(req, res, 401);
}
// verify token with secret key and xsrf token
verifyToken(token, xsrfToken, (err, payload) => {
if (err)
return handleResponse(req, res, 401);
else {
req.user = payload; //set the user to req so other routes can use it
next();
}
});
}
// validate user credentials
app.post('/users/signin', function (req, res) {
const user = req.body.username;
const pwd = req.body.password;
// return 400 status if username/password is not exist
if (!user || !pwd) {
return handleResponse(req, res, 400, null, "Username and Password required.");
}
const userData = userList.find(x => x.username === user && x.password === pwd);
// return 401 status if the credential is not matched
if (!userData) {
return handleResponse(req, res, 401, null, "Username or Password is Wrong.");
}
// get basic user details
const userObj = getCleanUser(userData);
// generate access token
const tokenObj = generateToken(userData);
// generate refresh token
const refreshToken = generateRefreshToken(userObj.userId);
// refresh token list to manage the xsrf token
refreshTokens[refreshToken] = tokenObj.xsrfToken;
// set cookies
res.cookie('refreshToken', refreshToken, COOKIE_OPTIONS);
res.cookie('XSRF-TOKEN', tokenObj.xsrfToken);
return handleResponse(req, res, 200, {
user: userObj,
token: tokenObj.token,
expiredAt: tokenObj.expiredAt
});
});
// handle user logout
app.post('/users/logout', (req, res) => {
clearTokens(req, res);
return handleResponse(req, res, 204);
});
// verify the token and return new tokens if it's valid
app.post('/verifyToken', function (req, res) {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
if (!refreshToken) {
return handleResponse(req, res, 204);
}
// verify xsrf token
const xsrfToken = req.headers['x-xsrf-token'];
if (!xsrfToken || !(refreshToken in refreshTokens) || refreshTokens[refreshToken] !== xsrfToken) {
return handleResponse(req, res, 401);
}
// verify refresh token
verifyToken(refreshToken, '', (err, payload) => {
if (err) {
return handleResponse(req, res, 401);
}
else {
const userData = userList.find(x => x.userId === payload.userId);
if (!userData) {
return handleResponse(req, res, 401);
}
// get basic user details
const userObj = getCleanUser(userData);
// generate access token
const tokenObj = generateToken(userData);
// refresh token list to manage the xsrf token
refreshTokens[refreshToken] = tokenObj.xsrfToken;
res.cookie('XSRF-TOKEN', tokenObj.xsrfToken);
// return the token along with user details
return handleResponse(req, res, 200, {
user: userObj,
token: tokenObj.token,
expiredAt: tokenObj.expiredAt
});
}
});
});
// get list of the users
app.get('/users/getList', authMiddleware, (req, res) => {
const list = userList.map(x => {
const user = { ...x };
delete user.password;
return user;
});
return handleResponse(req, res, 200, { random: Math.random(), userList: list });
});
app.listen(port, () => {
console.log('Server started on: ' + port);
});
Now let’s check these APIs in postman. Below we have provided you a collection of the APIs. If you don’t get an idea then read the description in the postman API collection.
Output - Login App with CSRF protection - Implement authentication in Node.js using JWT access token and refresh token - Clue Mediator
That’s it for today. Thank you for reading.
In the next article, we’ll implement authentication in ReactJS using REST API with CSRF protection.
Like share and follow us. Happy Coding..!!