Contents
Summary
This article will show you how to prevent concurrent login in a web application using Redis. We had a business need to prevent concurrent logins for the same user. If a user tries to log in from another device, the first device should be logged out.
We were already using Redis as a cache in our application. So we decided to use Redis to implement this feature.
Introduction
Let’s see our current implementation of the login API. We are using JWT for authentication and authorization. When a user logs in, we generate a JWT token. We attach it to the session, store it in Redis, and send it to the client. The client will send this token in the header for every request to the server. We will validate this token in the server and allow the user to access the resources.
For session management we are using express-session
npm package, so it takes care of generating session id and storing it in redis with expiry. On logout, the token will be removed from the Redis cache. Also these sessions will expire after a certain time period.
Code Snippet for login API
router.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email, password });
if (!user) {
throw new Error("Invalid credentials");
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "1d",
});
req.session.token = token;
res.json({ user });
});
Prevent Concurrent Logins
Map user id to token in Redis
So we decided to make use of existing implementation to prevent concurrent logins. We will store the token in Redis with the key as the user id and value as the token. When a user logs in, we will check if the user already has a token in Redis.
If the token is available, we will delete the token from Redis. We will then send a message to the client to logout. If the token is not available, we will store the token in Redis and send it to the client. On logout, we will delete the token from Redis.
Code Snippet for login API with concurrent login prevention
router.post("/login", async (req, res) => {
...
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "1d",
});
req.session.token = token;
// map user id to token in redis
const expiresAt = 24 * 60 * 60; // 1 day
redis.set(user.id, req.sessionID, "EX", expiresAt);
...
});
Remove existing token from Redis
When a user logs in, we will check if the user already has a token in Redis. If the token is present, we will delete the token from Redis and send a message to the client to logout.
Code Snippet for login API with concurrent login prevention
router.post("/login", async (req, res) => {
...
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "1d",
});
// remove existing token from redis
const existingSessionId = await redis.get(user.id);
if (existingSessionId) {
redis.del(existingSessionId);
// send message to client to logout
logger.info("User logged in from another device, logging out from previous device");
}
req.session.token = token;
// map user id to token in redis
const expiresAt = 24 * 60 * 60; // 1 day
redis.set(user.id, req.sessionID, "EX", expiresAt);
...
});
Allowing Multiple Sessions with a Limit
In certain business cases, users may need to stay logged in on multiple devices simultaneously, but with a restriction on the number of concurrent sessions. To handle this, we can modify our session management approach by utilizing a different Redis data structure.
Instead of mapping user IDs to a single session using Redis strings, we can use a Redis list or set to store multiple session IDs for a user. This allows us to maintain a list of active sessions and provides flexibility in removing sessions, whether it’s the oldest or the newest one.
However, using this method comes with a trade-off: we lose the ability to easily set an expiry on individual sessions. To address this, we would need to implement a background process or cron job to periodically clean up expired sessions from each user’s list.
Logout API
We did not make any changes to the logout API. We will delete the token from the Redis cache on logout.
Code Snippet for logout API
router.post("/logout", async (req, res) => {
// remove token from redis
req.session.destroy();
res.json({ message: "Logged out successfully" });
});
Conclusion
In this article, we explored how to prevent concurrent logins in a web application using Redis. By mapping user IDs to session tokens in Redis, we ensured that logging in from a new device would log the user out from the previous session. This provides a simple way to handle single-session requirements.
Additionally, we saw how to extend this functionality to allow multiple concurrent sessions, with a limit on the number of active sessions per user. By switching to a Redis list or set, we can track multiple sessions while retaining control over which sessions to invalidate. However, this approach requires handling session expiration manually, through a background job or cron.
This implementation provides flexibility in managing user sessions, from preventing concurrent logins to supporting multiple sessions with customizable limits.