Skip to content

Gestão de Sessões

As sessões do CSGOFlip são armazenadas no Redis com suporte a múltiplos dispositivos, rastreamento de IP e revogação instantânea.

Estrutura da Sessão

typescript
interface Session {
  id: string;           // ID único (32 caracteres hex)
  userId: bigint;       // ID do usuário
  steamId: string;      // Steam ID (para referência rápida)
  ip: string;           // IP de criação
  userAgent: string;    // Browser/Device info
  deviceId?: string;    // Fingerprint do dispositivo
  createdAt: Date;      // Quando foi criada
  lastActivity: Date;   // Última atividade
  expiresAt: Date;      // Quando expira (7 dias)
}

Armazenamento no Redis

┌─────────────────────────────────────────────────────────────┐
│                         REDIS                                │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  session:{sessionId}                                         │
│  ├── Value: JSON da sessão                                  │
│  └── TTL: 604800 segundos (7 dias)                          │
│                                                              │
│  user_sessions:{userId}                                      │
│  └── Set de sessionIds do usuário                           │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Operações

Criar Sessão

typescript
async create(data: CreateSessionData): Promise<Session> {
  const sessionId = crypto.randomBytes(16).toString('hex');
  
  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() + 7 * 24 * 60 * 60 * 1000),
  };

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

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

  return session;
}

Validar Sessão

typescript
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 (sliding expiration)
  session.lastActivity = new Date();
  await this.redis.setex(
    `session:${sessionId}`,
    604800,
    JSON.stringify(session),
  );

  return session;
}

Deletar Sessão (Logout)

typescript
async delete(sessionId: string): Promise<void> {
  const data = await this.redis.get(`session:${sessionId}`);
  
  if (data) {
    const session = JSON.parse(data) as Session;
    await this.redis.srem(`user_sessions:${session.userId}`, sessionId);
  }

  await this.redis.del(`session:${sessionId}`);
}

Logout de Todos os Dispositivos

typescript
async deleteAllForUser(userId: bigint): Promise<number> {
  const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
  
  if (sessionIds.length > 0) {
    await this.redis.del(
      ...sessionIds.map(id => `session:${id}`),
    );
    await this.redis.del(`user_sessions:${userId}`);
  }

  return sessionIds.length;
}

Listar Sessões do Usuário

typescript
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()
  );
}

Múltiplos Dispositivos

O usuário pode estar logado em vários dispositivos simultaneamente:

typescript
// Exemplo de sessões de um usuário
const sessions = await sessionService.getUserSessions(userId);

// Resultado:
// [
//   { id: 'abc123', userAgent: 'Chrome/120 Windows', ip: '177.x.x.x', lastActivity: '2024-01-15 14:30' },
//   { id: 'def456', userAgent: 'Safari/17 iPhone', ip: '189.x.x.x', lastActivity: '2024-01-15 10:00' },
//   { id: 'ghi789', userAgent: 'Firefox/121 Linux', ip: '200.x.x.x', lastActivity: '2024-01-14 22:15' },
// ]

Limite de Sessões

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

  // Cria nova sessão...
}

Detecção de Atividade Suspeita

Mudança de IP

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

  // IP mudou?
  if (session.ip !== currentIp) {
    // Verifica se é mesmo país/região
    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: {
          sessionId,
          originalIp: session.ip,
          newIp: currentIp,
        },
      });

      // Opcional: notifica usuário
      await this.notificationService.send(session.userId, {
        type: 'SECURITY_ALERT',
        title: 'Login de novo local detectado',
        message: `Detectamos acesso de ${currentIp}. Foi você?`,
      });
    }
  }

  return session;
}

Device Fingerprinting

typescript
// Frontend envia fingerprint do dispositivo
const deviceId = await generateDeviceFingerprint();

// Backend valida
async validateDevice(session: Session, deviceId: string): boolean {
  if (session.deviceId && session.deviceId !== deviceId) {
    // Device mudou - possível session hijacking
    await this.auditService.log({
      action: 'DEVICE_MISMATCH',
      userId: session.userId,
      metadata: {
        expectedDevice: session.deviceId,
        receivedDevice: deviceId,
      },
    });
    
    return false;
  }
  
  return true;
}
typescript
// Configuração segura do cookie de sessão
response.setCookie('sessionId', session.id, {
  httpOnly: true,       // Não acessível via JavaScript
  secure: true,         // Apenas HTTPS (produção)
  sameSite: 'lax',      // Proteção CSRF
  maxAge: 604800,       // 7 dias em segundos
  path: '/',
  domain: '.csgoflip.com', // Subdomínios inclusos
});

Endpoints da API

GET /auth/sessions

Lista sessões do usuário logado:

json
{
  "sessions": [
    {
      "id": "abc123...",
      "ip": "177.xxx.xxx.xxx",
      "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0",
      "createdAt": "2024-01-10T14:30:00Z",
      "lastActivity": "2024-01-15T16:45:00Z",
      "isCurrent": true
    },
    {
      "id": "def456...",
      "ip": "189.xxx.xxx.xxx", 
      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Safari/604.1",
      "createdAt": "2024-01-08T09:15:00Z",
      "lastActivity": "2024-01-14T22:30:00Z",
      "isCurrent": false
    }
  ]
}

DELETE /auth/sessions/:id

Revoga uma sessão específica:

typescript
@Delete('sessions/:id')
async revokeSession(
  @Param('id') sessionId: string,
  @CurrentUser() user: User,
) {
  const session = await this.sessionService.getById(sessionId);
  
  // Verifica que sessão pertence ao usuário
  if (session?.userId !== user.id) {
    throw new ForbiddenException('Not your session');
  }

  await this.sessionService.delete(sessionId);
  
  return { message: 'Session revoked' };
}

POST /auth/logout-all

Revoga todas as sessões:

typescript
@Post('logout-all')
async logoutAll(@CurrentUser() user: User) {
  const count = await this.sessionService.deleteAllForUser(user.id);
  
  return { 
    message: 'All sessions revoked',
    sessionsRevoked: count,
  };
}

Redis Cluster

Para alta disponibilidade:

typescript
// config/redis.config.ts
export const redisConfig = {
  // Redis Sentinel para failover automático
  sentinels: [
    { host: 'sentinel-1.internal', port: 26379 },
    { host: 'sentinel-2.internal', port: 26379 },
    { host: 'sentinel-3.internal', port: 26379 },
  ],
  name: 'mymaster',
  
  // Ou Redis Cluster para escala horizontal
  cluster: [
    { host: 'redis-1.internal', port: 6379 },
    { host: 'redis-2.internal', port: 6379 },
    { host: 'redis-3.internal', port: 6379 },
  ],
};

Arquivos Fonte

Principais Arquivos

  • src/infrastructure/auth/session.service.ts - Serviço de sessões
  • src/presentation/controllers/auth.controller.ts - Endpoints
  • src/infrastructure/redis/redis.service.ts - Cliente Redis

Documentação Técnica CSGOFlip