How to Prevent Multiple Logins in NestJS using Redis cache

How to Prevent Multiple Logins in NestJS using Redis cache

Enhancing Security and Session Management by Preventing Multiple Logins in NestJS Using Redis

Preventing multiple logins for a single user can be essential for security and maintaining a smooth user experience, especially in applications where session management and user accountability are critical. In this blog, we'll explore how to prevent multiple logins in a NestJS application by invalidating previous sessions when a new login occurs.

Why Prevent Multiple Logins?

Allowing multiple logins can lead to several issues:

  • Security risks: A user can share their login credentials with others or have their account accessed from multiple locations without their knowledge.

  • Session hijacking: If a user's session is stolen, attackers can maintain access without the user knowing.

  • User experience: In some applications (e.g., banking or sensitive accounts), it’s beneficial to limit access to one session at a time.

By implementing a mechanism to prevent multiple simultaneous logins, we can enhance the security and usability of our applications.

Steps to Implement Multiple Login Prevention in NestJS

Here’s a step-by-step guide to implementing a solution that prevents multiple logins using NestJS, JWT (JSON Web Token), and a session invalidation strategy.

Prerequisites

  • Basic knowledge of NestJS and JWT authentication.

  • A NestJS project with user authentication already set up using JWT.

Step 1: Store Active User Sessions

We need to store and manage user sessions, typically in a Redis store or a database, to track which users are logged in. When a user logs in, we store the token, and if another login attempt occurs, we invalidate the previous session.

Create a Session Service

We'll create a SessionService to manage user sessions in Redis (or any in-memory data store).

npm install redis @nestjs/redis
import { Injectable } from '@nestjs/common';
import { RedisService } from 'nestjs-redis';
import { InjectRedis } from '@nestjs-modules/ioredis';

@Injectable()
export class SessionService {
  constructor(@InjectRedis() private readonly redisClient) {}

  async setSession(userId: string, token: string): Promise<void> {
    await this.redisClient.set(userId, token);
  }

  async getSession(userId: string): Promise<string> {
    return this.redisClient.get(userId);
  }

  async invalidateSession(userId: string): Promise<void> {
    await this.redisClient.del(userId);
  }
}

Here, we store the JWT token for each user in Redis. When a user logs in, we call setSession(), and when they log out or log in again, we invalidate the previous session using invalidateSession().

Step 2: Invalidate Previous Sessions on New Login

To ensure that only one session is active at a time, we need to invalidate the previous session when a new login happens. In your AuthService (or wherever you handle login), you can modify the login logic like this:

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { SessionService } from '../session/session.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
    private readonly sessionService: SessionService,
  ) {}

  async login(userId: string): Promise<{ access_token: string }> {
    // Generate a new JWT token
    const payload = { sub: userId };
    const token = this.jwtService.sign(payload);

    // Invalidate previous session
    const currentSession = await this.sessionService.getSession(userId);
    if (currentSession) {
      await this.sessionService.invalidateSession(userId);
    }

    // Store new session
    await this.sessionService.setSession(userId, token);

    return { access_token: token };
  }
}

Step 3: Verify Session Validity on Each Request

To prevent access using an old token, you must validate the active session during each request. When a request comes in with a JWT token, you can check the stored session in Redis to ensure the token is still valid.

In your JWT validation guard (or middleware), check if the token in the request matches the token stored in the session:

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { SessionService } from '../session/session.service';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private readonly sessionService: SessionService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('Token is missing');
    }

    // Decode token to get the userId
    const decoded = this.jwtService.decode(token) as { sub: string };
    const userId = decoded.sub;

    // Get the current session from Redis
    const currentSession = await this.sessionService.getSession(userId);

    // Check if the provided token matches the active session
    if (token !== currentSession) {
      throw new UnauthorizedException('Session invalidated, please log in again');
    }

    return true;
  }
}

This guard will reject requests with an invalidated token and force the user to log in again.

Step 4: Handle Logout

When a user logs out, it’s important to also invalidate their session. This prevents the token from being used after logout:

async logout(userId: string): Promise<void> {
  await this.sessionService.invalidateSession(userId);
}

Step 5: Additional Considerations

  • Token Expiry: Ensure that JWT tokens have an expiration time (exp). This will automatically invalidate tokens after a certain period.

  • Refresh Tokens: You can implement refresh tokens if you want to allow users to remain logged in for long periods, but still limit them to one active session at a time.

  • Session Management in Database: If Redis isn't available, you can use a database to store the active sessions instead. Just make sure that queries to check the active session are optimized for performance.

Conclusion
By implementing session management and JWT token validation, you can effectively prevent multiple logins in your NestJS application. This approach ensures that users can only maintain one active session at a time, enhancing both security and user experience. Using tools like Redis or a similar store helps keep track of active sessions efficiently, making it a practical solution for modern web applications.

Happy Coding!