Backend Architecture Series — Jocoso.cl eCommerce · #02
JWT, Refresh Tokens y 2FA: Seguridad real desde el día 1
Access tokens cortos, rotación de refresh tokens y TOTP para un ecommerce multicanal
La seguridad no puede ser un afterthought
En un ecommerce las consecuencias de una brecha son directas: cuentas comprometidas, pedidos fraudulentos, datos de tarjetas en riesgo. Diseñar la seguridad desde el día 1 no solo protege a los usuarios — protege la reputación del negocio y el cumplimiento regulatorio requerido por pasarelas como MercadoPago.
Arquitectura de tokens dual
■ Decisión Técnica
Access Token (JWT, 15 min) + Refresh Token rotado (7 días, SHA-256 hasheado en BD). Los 15 minutos minimizan la ventana de exposición si el token es interceptado. El refresh token nunca se guarda en texto plano — solo su hash SHA-256.
Refresh Token Rotation — detección de robo
Cada uso del refresh token invalida el token anterior y genera uno nuevo. Si un atacante roba el token y lo usa antes que el usuario legítimo, el segundo intento de uso detecta la colisión y revoca toda la sesión:
// application/auth/use-cases/refresh.usecase.ts
async execute(rawToken: string): Promise<TokenPair> {
const hash = this.tokenSvc.hashToken(rawToken); // SHA-256
const record = await this.refreshRepo.findByHash(hash);
if (!record || record.isRevoked || record.expiresAt < new Date()) {
throw new UnauthorizedException('Token inválido o expirado');
}
// Rotation: revoke old, issue new
await this.refreshRepo.revoke(record.id);
const newRaw = this.tokenSvc.generateRefreshToken();
const newHash = this.tokenSvc.hashToken(newRaw);
await this.refreshRepo.save({ userId: record.userId, hash: newHash, ... });
return { accessToken: this.tokenSvc.generateAccessToken(user), refreshToken: newRaw };
}RBAC — Control de acceso basado en roles
El sistema maneja tres roles: ADMIN, SELLER y CUSTOMER. La autorización se implementa con un decorador declarativo y un guard que lee el payload del JWT:
// infrastructure/security/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.get<Role[]>('roles', ctx.getHandler());
if (!roles?.length) return true;
const { user } = ctx.switchToHttp().getRequest();
return roles.includes(user.role);
}
}
// Uso en controlador:
@Get('admin/users')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN)
listUsers() { ... }2FA con TOTP — Google Authenticator
La autenticación de dos factores usa TOTP (Time-based One-Time Password) compatible con Google Authenticator y Authy. El flujo tiene tres pasos:
- Setup: el servidor genera un secreto con speakeasy y retorna QR code URI
- Verify: el usuario ingresa el código de 6 dígitos para confirmar el setup
- Login: si 2FA está habilitado, el login requiere el código TOTP además de contraseña
// infrastructure/security/totp.service.ts
@Injectable()
export class TotpService {
generateSecret(email: string) {
const secret = speakeasy.generateSecret({
name: `Jocoso.cl (${email})`,
issuer: 'Jocoso.cl',
});
return { secret: secret.base32, otpauthUrl: secret.otpauth_url };
}
verify(secret: string, token: string): boolean {
return speakeasy.totp.verify({
secret, encoding: 'base32', token, window: 1,
});
}
}bcrypt con 12 rounds
Las contraseñas se hashean con bcrypt usando 12 rondas. En hardware moderno, 12 rondas toma ~250ms — suficiente para que ataques de fuerza bruta sean prohibitivamente lentos, sin degradar la experiencia del usuario en login.
Hardening de la API
- Helmet: configura headers HTTP de seguridad (CSP, HSTS, X-Frame-Options)
- CORS restrictivo: solo orígenes permitidos en lista blanca
- ValidationPipe global: whitelist + transform — rechaza campos no declarados en DTOs
- class-validator en todos los DTOs:
IsEmail,IsStrongPassword,IsJWT
■ Trade-offs
JWT stateless vs Sessions con estado. JWT permite escalar horizontalmente sin store compartido — clave para preparar microservicios. La contrapartida es que la revocación inmediata requiere una lista negra en Redis. Para Jocoso.cl la solución es access tokens cortos (15 min): el impacto de un token robado está acotado al tiempo de expiración. Refresh tokens sí se revocan en BD en logout y en rotación.
Conclusión
La seguridad de un ecommerce no es un feature adicional — es la base que permite operar. Este diseño cubre las tres dimensiones: autenticación robusta (JWT + refresh rotation), autorización granular (RBAC) y segundo factor (TOTP). Todo implementado en la capa de infraestructura, invisible para el dominio.