Skip to content

Sessions vs JWT

O CSGOFlip usa Session IDs armazenados no Redis ao invés de JWT (JSON Web Tokens) para autenticação.

Por que Sessions e não JWT?

O Problema com JWT em Gambling

JWT é stateless - uma vez emitido, é válido até expirar. Isso causa problemas sérios:

Cenário: Usuário é banido por fraude

Com JWT:
1. Admin bane usuário às 14:00
2. JWT do usuário expira às 15:00
3. Usuário continua fazendo apostas por 1 HORA após ser banido!

Com Sessions:
1. Admin bane usuário às 14:00
2. Sistema deleta sessão do Redis
3. Próxima requisição: 401 Unauthorized (imediato!)

Comparativo Detalhado

AspectoSessions (Redis)JWT
Revogação✅ Instantânea❌ Impossível (sem blacklist)
Logout✅ Deleta sessão❌ Token continua válido
Logout de todos dispositivos✅ Deleta todas sessões❌ Muito complexo
Ver dispositivos conectados✅ Lista sessões❌ Impossível
Forçar re-login✅ Deleta sessão❌ Impossível
Dados sensíveis✅ No servidor❌ No token (cliente)
Tamanho✅ ~32 bytes (ID)❌ ~500+ bytes
Escalabilidade⚠️ Precisa de Redis✅ Stateless
Complexidade✅ Simples⚠️ Refresh tokens

Casos de Uso Críticos

1. Banimento de Usuário

typescript
// Com Sessions - FUNCIONA PERFEITAMENTE
async banUser(userId: bigint) {
  // 1. Marca no banco
  await this.userRepository.update(userId, { status: 'BANNED' });
  
  // 2. Revoga TODAS as sessões
  const sessions = await this.redis.keys(`session:*:user:${userId}`);
  if (sessions.length > 0) {
    await this.redis.del(...sessions);
  }
  
  // Usuário é deslogado INSTANTANEAMENTE de todos os dispositivos
}

// Com JWT - PROBLEMÁTICO
async banUser(userId: bigint) {
  await this.userRepository.update(userId, { status: 'BANNED' });
  
  // E agora? O token JWT ainda é válido!
  // Opções ruins:
  // A) Esperar expirar (inaceitável)
  // B) Blacklist (reinventa sessions)
  // C) Checar banco a cada request (mata performance)
}

2. Detecção de Comprometimento

typescript
// Usuário reporta: "Minha conta foi hackeada!"

// Com Sessions
async securityLogout(userId: bigint) {
  // Lista todas as sessões
  const sessions = await this.sessionService.getUserSessions(userId);
  
  // Mostra para o usuário
  // "Você está logado em: Chrome/Windows (SP), Safari/iPhone (RJ)"
  
  // Deleta todas e força novo login
  await this.sessionService.deleteAll(userId);
  
  // Gera nova senha/2FA
}

// Com JWT - Impossível listar "dispositivos conectados"

3. Mudança de Permissões

typescript
// Admin promove usuário para ADMIN

// Com Sessions
async promoteToAdmin(userId: bigint) {
  await this.userRepository.update(userId, { role: 'ADMIN' });
  
  // Força re-login para atualizar permissões
  await this.sessionService.deleteAll(userId);
  
  // Próximo login terá role atualizado
}

// Com JWT
async promoteToAdmin(userId: bigint) {
  await this.userRepository.update(userId, { role: 'ADMIN' });
  
  // Token antigo ainda tem role: 'USER'
  // Precisa esperar expirar ou...
  // Checar banco a cada request (mata o propósito do JWT)
}

Implementação de Sessions

Estrutura da Sessão

typescript
interface Session {
  id: string;           // ID único da sessão (32 chars)
  userId: bigint;       // ID do usuário
  steamId: string;      // Steam ID
  ip: string;           // IP de criação
  userAgent: string;    // Browser/Device
  deviceId?: string;    // Fingerprint (opcional)
  createdAt: Date;      // Quando foi criada
  lastActivity: Date;   // Última atividade
  expiresAt: Date;      // Quando expira
}

Redis Storage

typescript
// Chave: session:{sessionId}
// Valor: JSON da sessão
// TTL: 7 dias (604800 segundos)

// Índice secundário para busca por usuário:
// Chave: user_sessions:{userId}
// Valor: Set de sessionIds

SessionService

typescript
// src/infrastructure/auth/session.service.ts

@Injectable()
export class SessionService {
  private readonly SESSION_TTL = 60 * 60 * 24 * 7; // 7 dias

  constructor(private readonly redis: RedisService) {}

  async create(data: CreateSessionData): Promise<Session> {
    const sessionId = this.generateSessionId();
    
    const session: Session = {
      id: sessionId,
      userId: data.userId,
      steamId: data.steamId,
      ip: data.ip,
      userAgent: data.userAgent,
      deviceId: data.deviceId,
      createdAt: new Date(),
      lastActivity: new Date(),
      expiresAt: new Date(Date.now() + this.SESSION_TTL * 1000),
    };

    // Salva sessão
    await this.redis.setex(
      `session:${sessionId}`,
      this.SESSION_TTL,
      JSON.stringify(session),
    );

    // Adiciona ao índice do usuário
    await this.redis.sadd(
      `user_sessions:${data.userId}`,
      sessionId,
    );

    return session;
  }

  async validate(sessionId: string): Promise<Session | null> {
    const data = await this.redis.get(`session:${sessionId}`);
    
    if (!data) {
      return null;
    }

    const session = JSON.parse(data) as Session;

    // Verifica expiração
    if (new Date(session.expiresAt) < new Date()) {
      await this.delete(sessionId);
      return null;
    }

    // Atualiza última atividade
    session.lastActivity = new Date();
    await this.redis.setex(
      `session:${sessionId}`,
      this.SESSION_TTL,
      JSON.stringify(session),
    );

    return session;
  }

  async delete(sessionId: string): Promise<void> {
    const data = await this.redis.get(`session:${sessionId}`);
    
    if (data) {
      const session = JSON.parse(data) as Session;
      
      // Remove do índice do usuário
      await this.redis.srem(
        `user_sessions:${session.userId}`,
        sessionId,
      );
    }

    // Remove a sessão
    await this.redis.del(`session:${sessionId}`);
  }

  async deleteAllForUser(userId: bigint): Promise<number> {
    const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
    
    if (sessionIds.length > 0) {
      // Deleta todas as sessões
      await this.redis.del(
        ...sessionIds.map(id => `session:${id}`),
      );
      
      // Limpa o índice
      await this.redis.del(`user_sessions:${userId}`);
    }

    return sessionIds.length;
  }

  async getUserSessions(userId: bigint): Promise<Session[]> {
    const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
    
    const sessions: Session[] = [];
    
    for (const sessionId of sessionIds) {
      const data = await this.redis.get(`session:${sessionId}`);
      if (data) {
        sessions.push(JSON.parse(data));
      }
    }

    return sessions.sort(
      (a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
    );
  }

  private generateSessionId(): string {
    return crypto.randomBytes(16).toString('hex'); // 32 chars
  }
}

AuthGuard

typescript
// src/presentation/guards/auth.guard.ts

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly sessionService: SessionService,
    private readonly userRepository: IUserRepository,
    private readonly reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Verifica se é rota pública
    const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
    if (isPublic) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    
    // Busca session ID do cookie ou header
    const sessionId = this.extractSessionId(request);
    
    if (!sessionId) {
      throw new UnauthorizedException('Session required');
    }

    // Valida sessão
    const session = await this.sessionService.validate(sessionId);
    
    if (!session) {
      throw new UnauthorizedException('Invalid or expired session');
    }

    // Busca usuário
    const user = await this.userRepository.findById(session.userId);
    
    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    // Verifica se usuário está banido
    if (user.status === 'BANNED') {
      await this.sessionService.delete(sessionId);
      throw new ForbiddenException('User is banned');
    }

    // Anexa usuário ao request
    request.user = user;
    request.session = session;

    return true;
  }

  private extractSessionId(request: any): string | null {
    // 1. Tenta cookie
    const cookieSessionId = request.cookies?.['sessionId'];
    if (cookieSessionId) {
      return cookieSessionId;
    }

    // 2. Tenta header Authorization
    const authHeader = request.headers['authorization'];
    if (authHeader?.startsWith('Bearer ')) {
      return authHeader.substring(7);
    }

    return null;
  }
}

Fluxo de Autenticação

Login

Requisição Autenticada

Logout


Segurança Adicional

Detecção de IP Suspeito

typescript
async validate(sessionId: string, currentIp: string): Promise<Session | null> {
  const session = await this.getSession(sessionId);
  
  if (!session) return null;

  // Verifica se IP mudou drasticamente
  if (session.ip !== currentIp) {
    const isSameRegion = await this.geoService.isSameRegion(session.ip, currentIp);
    
    if (!isSameRegion) {
      // Log de segurança
      await this.auditService.log({
        action: 'SUSPICIOUS_IP_CHANGE',
        userId: session.userId,
        metadata: { oldIp: session.ip, newIp: currentIp },
      });

      // Opcional: invalidar sessão
      // await this.delete(sessionId);
      // return null;
    }
  }

  return session;
}

Limite de Sessões por Usuário

typescript
async create(data: CreateSessionData): Promise<Session> {
  const existingSessions = await this.getUserSessions(data.userId);
  
  // Máximo de 5 sessões simultâneas
  if (existingSessions.length >= 5) {
    // Remove a mais antiga
    const oldest = existingSessions[existingSessions.length - 1];
    await this.delete(oldest.id);
  }

  // Cria nova sessão...
}

Cookies Seguros

typescript
// Configuração do cookie de sessão
response.setCookie('sessionId', session.id, {
  httpOnly: true,      // Não acessível via JavaScript
  secure: true,        // Apenas HTTPS
  sameSite: 'lax',     // Proteção CSRF
  maxAge: 7 * 24 * 60 * 60, // 7 dias
  path: '/',
  domain: '.csgoflip.com',
});

Escalabilidade

Redis Cluster

Para alta disponibilidade, usamos Redis Cluster ou Sentinel:

typescript
// config/redis.config.ts
export const redisConfig = {
  cluster: [
    { host: 'redis-1', port: 6379 },
    { host: 'redis-2', port: 6379 },
    { host: 'redis-3', port: 6379 },
  ],
  // ou
  sentinels: [
    { host: 'sentinel-1', port: 26379 },
    { host: 'sentinel-2', port: 26379 },
    { host: 'sentinel-3', port: 26379 },
  ],
  name: 'mymaster',
};

Performance

Operações de sessão são O(1) no Redis:

OperaçãoComplexidadeLatência Típica
GET sessionO(1)< 1ms
SET sessionO(1)< 1ms
DEL sessionO(1)< 1ms
SMEMBERS user_sessionsO(n)< 5ms

Quando usar JWT?

JWT ainda é útil para:

  • APIs públicas sem necessidade de revogação
  • Microservices onde validação local é importante
  • Tokens de curta duração (< 15 minutos)
  • Autenticação entre serviços (service-to-service)

Para o CSGOFlip (gambling com dinheiro real), Sessions são obrigatórias pela necessidade de revogação instantânea.


Arquivos Fonte Relacionados

Principais Arquivos

  • src/infrastructure/auth/session.service.ts - Gerenciamento de sessões
  • src/presentation/guards/auth.guard.ts - Guard de autenticação
  • src/presentation/controllers/auth.controller.ts - Endpoints de auth
  • src/infrastructure/redis/redis.service.ts - Cliente Redis

Documentação Técnica CSGOFlip