Authentication
How to create login activity tracker using JWT in NodeJs
Manage Login Activity of User
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
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 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.
Blacklist Token Model
This model will be used to store the blacklisted tokens after logout.
Sequelize Connection file
In this file, we will connect the database with our project using the database credentials.
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
}
}
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.
Token Utils
Before moving to login route lets understand the generateToken and sendToken function in token.utils.js
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);
}
Login Route
In the login route, we will take user credentials and generate and send the token to the user.
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.
Middlewares
Before moving on to create the User Login Activity route first let’s understand the authentication and blacklist middlewares.
AuthenticateToken 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.
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.
Links
- Github Repository : https://github.com/Divyansh12/Node-js-Auth
- Trial Front-End App : https://node-js-auth.netlify.app/
- Frontend App Repository: https://github.com/Divyansh12/Node-js-Auth-Frontend