close up shot of keyboard buttons
Photo by Miguel Á. Padriñán on Pexels.com

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.

ALSO READ  Top 8 free web Hosting sites

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.

ALSO READ  What Matters To Google: Ranking Factors in 2019

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.

ALSO READ  What Are the Basic Data Types in TypeSript?

This implementation provides flexibility in managing user sessions, from preventing concurrent logins to supporting multiple sessions with customizable limits.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.