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
| Aspecto | Sessions (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 sessionIdsSessionService
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ção | Complexidade | Latência Típica |
|---|---|---|
| GET session | O(1) | < 1ms |
| SET session | O(1) | < 1ms |
| DEL session | O(1) | < 1ms |
| SMEMBERS user_sessions | O(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õessrc/presentation/guards/auth.guard.ts- Guard de autenticaçãosrc/presentation/controllers/auth.controller.ts- Endpoints de authsrc/infrastructure/redis/redis.service.ts- Cliente Redis
