Authentication

How to manage login activity using JWT in NodeJs

Manage Login Activity of User

Divyansh Agarwal
The Startup

--

About JWT

JWT (JSON Web Tokens) is a stateless way of handling authentication in our app. For each login request, the server generates a token and sends it to the front-end where it is stored and used to authenticate every other request.

But since the JWT is stateless it (should not be) is not stored in any database or storage. So if a user wants to logout from a particular device or logout from all the devices, he cannot logout using the traditional way of authentication using JWT. What if I tell you there is a way to solve this problem without changing the stateless nature of JWT, and without even using any secondary storage like Redis.

Problem with using Redis for storing tokens

Redis is an open-source, in-memory data structure store, which is generally used as a database, cache, and message broker. So to implement logout from all device functionality, the token must be blacklisted and since the JWT is stateless it is not recommended to store the token in the database. So here comes the Redis which acts as an intermediate data store in which the user’s token is stored and when the user wants to logout from all the devices, the backend just gets all the user’s tokens and blacklists them. So the biggest issue in using Redis is that in this method the token is stored in a common data store that effects the stateless nature of the token. Also, it is difficult to keep track of which token is used to login with which device for a user.

Problem with using Session authentication

Although implementing logout from all devices functionality using a session is an easy task and there are many problems related to Session Authentication. First, the session id must be stored in a cookie in the browser which could bring unreliability to the authentication mechanism. Second, unlike token-based authentication, the session-based authentication is not stateless. Sessions vs Token Authentication can be a hot topic to discuss but we are not here to discuss that.

Solution

So to overcome all these problems we could create a unique token_id for every token created and send it along with the token in its payload along with the user id and store this token_id in a separate UserLogins Table which contains the login information like IP address and user-agent and two boolean fields token_deleted to check if the token is marked deleted or not and logged_out field to check if the user logged out from the device or not. So when a user wants to logout from a particular device then frontend just have to pass the id UserLogin instance in the URL and the token of that token_id will be marked as deleted and logged_out will be set as true and the user will not be able to login using that token. Hence the stateless nature of token will be maintained along with the security and reliability of token-authentication.

Let’s Begin!

Let’s start with implementing basic authentication function,i.e, Register, Login, Logout. Let’s first initialize the node project using npm init.

Final Folder Structure will look like this :

├───.env
├───.gitignore
├───app.js
├───package-lock.json
├───package.json
├───sequelize.js
├───middlewares
│ ├───authenticateToken.js
│ └───blacklistToken.js
├───models
│ ├───BlacklistToken.js
│ ├───User.js
│ └───User_Login.js
├───routes
│ ├───loginUser.js
│ ├───registerUser.js
│ └───user_logins.js
└───utils
└───token.utils.js

Install the following dependencies

npm i express bcrypt bcryptjs cors dotenv morgan jsonwebtoken sequelize sequelize-cli pg helmet

package.json

package.json

Let’s start with creating the app.js file.

In app.js we declare our express server and declare all the routes for different functions.

App.js

Models

So after initializing the node app let’s create models for our app.

User Model
This model will store the details of user like name, email, username, password (hash).

User Model

User Logins Model

This model will store the details of user logins. If a user, login from a device then it will generate an instance of the user login model.

User Logins Model

Blacklist Token Model

This model will be used to store the blacklisted tokens after logout.

Blacklist Token Model

Sequelize Connection file

In this file, we will connect the database with our project using the database credentials.

Sequelize Connection File

These dialect options are used to connect to a database service hosted on a cloud since they require SSL to be true.

dialectOptions: {
"ssl": {
"require": true,
"rejectUnauthorized": false
}
}
Environment File for setting Database

Routes

Register Route

Now in the register route, we want to take the user details and password then store it in the database and issue a login token to the user.

Register Route

Token Utils

Before moving to login route lets understand the generateToken and sendToken function in token.utils.js

Token Utils

Now, let’s understand this code. First, we generate the token Id of the jwt using custom-id package. In customId function, we will create token using user_id and current date and time to make it unique every time (or you can use UUID directly).

const token_id = await customId({
user_id : req.auth.id,
date : Date.now(),
randomLength: 4
});

Then we retrieve the user’s IP address from request headers.

var ip = (req.headers['x-forwarded-for'] || '')   
.split(',').pop().trim() ||
req.connection.remoteAddress||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;

Then identify all the logins of which token is not deleted and have the same IP address and user-agent as the incoming request. And then mark all the tokens that are associated with the same device as deleted by setting the token_deleted=true.

const user_logins=await User_Login.findAll({where:{ user_id:   
req.auth.id ,token_deleted:false, ip_address:ip, device:
req.headers["user-agent"]}});
user_logins.forEach(async(login) => {
if(login){
login.token_deleted=true;
await login.save()
}
});

Then create the User_Login object using user_id, token_id, ip_address, and user-agent since we want to create a new token after every new login on the same device. Then, sign the jwt in which payload should contain the user_id and token_id to identify the user.

const token = await User_Login.create({
user_id : req.auth.id,
token_id : token_id,
token_secret : token_secret ,
ip_address : ip ,
device : req.headers["user-agent"]
});
const token_user = { id:req.auth.id , token_id: token_id };
const accessToken = await jwt.sign(token_user,
process.env.ACCESS_TOKEN_SECRET);

Finally, send the response containing the token along with the user object.

sendToken: function(req, res) {          
const responseObject = { auth: true,
token: req.token,
message: 'user found & logged in'
};
return res.status(200).json(responseObject);
}
Final Environment File

Login Route

In the login route, we will take user credentials and generate and send the token to the user.

Login Route

Logout Route

In this route, we will take the user’s token and blacklist it and then store it in the database. Although this might reduce the stateless nature of jwt but it is only used when the user logs out and so this token will not be used by the user again. Hence nature of this token will not matter and would not affect the user’s security.

Logout Route

Middlewares

Before moving on to create the User Login Activity route first let’s understand the authentication and blacklist middlewares.

AuthenticateToken Middleware

Authenticate Token Middleware

So when any protected route will be accessed by the frontend app then it will have to send the accessToken provided to it at the time of login in the Authorization Headers as a Bearer token.

Blacklist.findOne({ where: {token: token } })     
.then((found) => {
if (found){
details={
"Status":"Failure",
"Details":'Token blacklisted. Cannot use this token.'
}
return res.status(401).json(details);
}
else {
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET,
async (err, payload) => {
if (err)
return res.sendStatus(403);
if(payload){
const login = await User_Login.findOne({where:{
user_id : payload.id, token_id: payload.token_id}})
if(login.token_deleted==true){
const blacklist_token = Blacklist.create({
token:token
});
return res.sendStatus(401)
}
}
req.user = payload;
next();
});
}
});

In this function, we will first find if the token is present in the BlacklistToken table if present it will send 401 Unauthorized code along with the token blacklisted message.

jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, 
async (err, payload) => {
if (err)
return res.sendStatus(403);
if(payload){
const login = await User_Login.findOne({where:{
user_id : payload.id, token_id: payload.token_id}})
if(login.token_deleted==true){
const blacklist_token = Blacklist.create({
token:token
});
return res.sendStatus(401)
}
}
req.user = payload;
next();
});

Then if the token is not present in the table we will verify the token with jwt.verify() with Access_Token_Secret if the token is not verified then the middleware will send 403 Forbidden code. Then we will check for payload and find the UserLogin instance corresponding to the user and token_id and if the token_deleted is true in that instance then it will be first added to the Blacklisted Tokens table and then the server will send 401 Unauthorized code. If everything goes right then the req.user will be set equal to payload object.

BlacklistToken Middleware

This middleware will only be used when the user uses the logout route. And have a simple functionality to add the token to Blacklist Token table when called and mark token_deleted=true and logged_out=true in UserLogin table for the given token.

Blacklist Token Middleware

Manage Login Activity Routes

In these routes we will display the various logins to the user, allow him to logout from the individual device, logout from all devices, logout from all but not current.

Manage Login Activity Routes

--

--

Divyansh Agarwal
The Startup

I am an Innovator with lots of ideas in my mind to improve the world for better! Know more about me at https://divyanshagarwal.info .