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:

typescript
// 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:

typescript
// 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
typescript
// 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.