# ET-AUTH-005: Especificación Técnica - Seguridad

**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** ✅ Implementado
**Épica:** [OQI-001](../_MAP.md)

---

## Resumen

Esta especificación detalla las medidas de seguridad implementadas en el sistema de autenticación de OrbiQuant IA.

---

## Capas de Seguridad

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    SECURITY LAYERS                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ LAYER 1: PERIMETER                                               │   │
│  │  • Rate Limiting                                                 │   │
│  │  • WAF (Web Application Firewall)                               │   │
│  │  • DDoS Protection (Cloudflare)                                 │   │
│  │  • IP Blacklisting                                              │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ LAYER 2: TRANSPORT                                               │   │
│  │  • TLS 1.3 only                                                 │   │
│  │  • HSTS (HTTP Strict Transport Security)                        │   │
│  │  • Certificate Pinning (mobile)                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ LAYER 3: APPLICATION                                             │   │
│  │  • JWT RS256 Tokens                                             │   │
│  │  • CSRF Protection                                              │   │
│  │  • Input Validation (Zod)                                       │   │
│  │  • Output Encoding                                              │   │
│  │  • Security Headers                                             │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ LAYER 4: DATA                                                    │   │
│  │  • Password Hashing (bcrypt)                                    │   │
│  │  • Token Encryption (AES-256-GCM)                               │   │
│  │  • PII Encryption                                               │   │
│  │  • Database Encryption at Rest                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Rate Limiting

### Configuración

```typescript
// apps/backend/src/core/middleware/rate-limiter.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Rate limiter general
export const generalRateLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minuto
  max: 100, // 100 requests por minuto
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({
    prefix: 'rl:general:',
    client: redisClient,
  }),
  message: {
    success: false,
    error: {
      code: 'RATE_LIMIT_EXCEEDED',
      message: 'Demasiadas solicitudes, intenta más tarde',
    },
  },
});

// Rate limiter estricto para auth
export const authRateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // 5 intentos
  standardHeaders: true,
  store: new RedisStore({
    prefix: 'rl:auth:',
    client: redisClient,
  }),
  keyGenerator: (req) => {
    // Por IP + email
    return `${req.ip}:${req.body?.email || 'unknown'}`;
  },
  handler: (req, res) => {
    // Log intento sospechoso
    securityLogger.warn('Rate limit exceeded', {
      ip: req.ip,
      email: req.body?.email,
      path: req.path,
    });

    res.status(429).json({
      success: false,
      error: {
        code: 'TOO_MANY_ATTEMPTS',
        message: 'Demasiados intentos. Espera 15 minutos.',
        retryAfter: 900,
      },
    });
  },
});

// Rate limiter para OTP
export const otpRateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 3,
  keyGenerator: (req) => req.body?.phone || req.ip,
  store: new RedisStore({
    prefix: 'rl:otp:',
    client: redisClient,
  }),
});
```

### Límites por Endpoint

| Endpoint | Límite | Ventana | Key |
|----------|--------|---------|-----|
| `/auth/login` | 5 | 15 min | IP + email |
| `/auth/register` | 3 | 1 hora | IP |
| `/auth/phone/send` | 3 | 15 min | phone |
| `/auth/forgot-password` | 3 | 1 hora | IP + email |
| `/auth/2fa/verify` | 5 | 15 min | IP + userId |
| General API | 100 | 1 min | IP |

---

## Password Hashing

### Implementación

```typescript
import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12; // ~300ms en hardware moderno

export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  return bcrypt.compare(password, hash);
}
```

### Política de Contraseñas

```typescript
import { z } from 'zod';

export const passwordSchema = z
  .string()
  .min(8, 'Mínimo 8 caracteres')
  .max(128, 'Máximo 128 caracteres')
  .regex(/[A-Z]/, 'Debe contener al menos una mayúscula')
  .regex(/[a-z]/, 'Debe contener al menos una minúscula')
  .regex(/[0-9]/, 'Debe contener al menos un número')
  .regex(/[!@#$%^&*(),.?":{}|<>]/, 'Debe contener al menos un carácter especial');

// Validación adicional contra lista negra
const COMMON_PASSWORDS = new Set([
  'password123',
  '12345678',
  'qwerty123',
  // ... más passwords comunes
]);

export function isPasswordAllowed(password: string, email: string): boolean {
  const lowerPassword = password.toLowerCase();

  // No puede ser igual al email
  if (lowerPassword === email.toLowerCase()) {
    return false;
  }

  // No puede estar en lista negra
  if (COMMON_PASSWORDS.has(lowerPassword)) {
    return false;
  }

  // No puede contener partes del email
  const emailParts = email.split('@')[0].toLowerCase();
  if (lowerPassword.includes(emailParts) && emailParts.length > 3) {
    return false;
  }

  return true;
}
```

---

## Token Encryption

### Encriptación de OAuth Tokens

```typescript
import crypto from 'crypto';

const ENCRYPTION_KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY!, 'hex');
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;

export function encryptToken(token: string): string {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);

  let encrypted = cipher.update(token, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  // Format: iv:authTag:encrypted
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}

export function decryptToken(encryptedToken: string): string {
  const [ivHex, authTagHex, encrypted] = encryptedToken.split(':');

  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');

  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}
```

### Encriptación de 2FA Secret

```typescript
export function encrypt2FASecret(secret: string): string {
  // Usar la misma función de encriptación
  return encryptToken(secret);
}

export function decrypt2FASecret(encrypted: string): string {
  return decryptToken(encrypted);
}
```

---

## Security Headers

### Configuración Helmet

```typescript
import helmet from 'helmet';

export const securityHeaders = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // Para OAuth popups
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'", 'https://api.orbiquant.com'],
      fontSrc: ["'self'", 'https://fonts.gstatic.com'],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'self'", 'https://accounts.google.com'], // OAuth frames
    },
  },
  crossOriginEmbedderPolicy: false, // Para OAuth
  crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
  crossOriginResourcePolicy: { policy: 'cross-origin' },
  dnsPrefetchControl: { allow: true },
  frameguard: { action: 'sameorigin' },
  hsts: {
    maxAge: 31536000, // 1 año
    includeSubDomains: true,
    preload: true,
  },
  ieNoOpen: true,
  noSniff: true,
  originAgentCluster: true,
  permittedCrossDomainPolicies: { permittedPolicies: 'none' },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xssFilter: true,
});
```

### Headers Personalizados

```typescript
export const customSecurityHeaders = (req, res, next) => {
  // Prevenir clickjacking adicional
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');

  // Cache control para datos sensibles
  if (req.path.startsWith('/auth/')) {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
  }

  // Permissions policy
  res.setHeader(
    'Permissions-Policy',
    'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
  );

  next();
};
```

---

## Input Validation

### Sanitización

```typescript
import { z } from 'zod';
import sanitizeHtml from 'sanitize-html';

// Sanitizar strings
const sanitizedString = z.string().transform((val) =>
  sanitizeHtml(val, {
    allowedTags: [], // Sin HTML
    allowedAttributes: {},
  })
);

// Email validation
const emailSchema = z
  .string()
  .email('Email inválido')
  .max(255)
  .toLowerCase()
  .trim();

// Phone validation (E.164)
const phoneSchema = z
  .string()
  .regex(/^\+[1-9]\d{1,14}$/, 'Formato de teléfono inválido');

// Prevenir injection
const safeIdSchema = z
  .string()
  .uuid('ID inválido');
```

### Validación de Request Body

```typescript
// Middleware de validación
export const validate = (schema: z.ZodSchema) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      req.body = await schema.parseAsync(req.body);
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          success: false,
          error: {
            code: 'VALIDATION_ERROR',
            message: 'Datos inválidos',
            details: error.errors.map((e) => ({
              field: e.path.join('.'),
              message: e.message,
            })),
          },
        });
      }
      next(error);
    }
  };
};
```

---

## Account Lockout

### Implementación

```typescript
const LOCKOUT_THRESHOLD = 5;
const LOCKOUT_DURATION_MS = 15 * 60 * 1000; // 15 minutos

export async function handleFailedLogin(userId: string): Promise<void> {
  const user = await db.query(
    'SELECT failed_login_attempts, locked_until FROM users WHERE id = $1',
    [userId]
  );

  const attempts = user.rows[0].failed_login_attempts + 1;

  if (attempts >= LOCKOUT_THRESHOLD) {
    // Bloquear cuenta
    await db.query(
      `UPDATE users
       SET failed_login_attempts = $1,
           locked_until = NOW() + INTERVAL '${LOCKOUT_DURATION_MS / 1000} seconds'
       WHERE id = $2`,
      [attempts, userId]
    );

    // Notificar al usuario
    await sendSecurityAlert(userId, 'account_locked');

    // Log de seguridad
    securityLogger.warn('Account locked due to failed attempts', {
      userId,
      attempts,
    });
  } else {
    await db.query(
      'UPDATE users SET failed_login_attempts = $1 WHERE id = $2',
      [attempts, userId]
    );
  }
}

export async function resetFailedAttempts(userId: string): Promise<void> {
  await db.query(
    'UPDATE users SET failed_login_attempts = 0, locked_until = NULL WHERE id = $1',
    [userId]
  );
}

export async function isAccountLocked(userId: string): Promise<boolean> {
  const result = await db.query(
    'SELECT locked_until FROM users WHERE id = $1',
    [userId]
  );

  const lockedUntil = result.rows[0]?.locked_until;
  return lockedUntil && new Date(lockedUntil) > new Date();
}
```

### Progresión de Bloqueo

```typescript
const LOCKOUT_PROGRESSION = [
  { threshold: 5, duration: 15 * 60 * 1000 },    // 5 intentos: 15 min
  { threshold: 10, duration: 60 * 60 * 1000 },   // 10 intentos: 1 hora
  { threshold: 15, duration: 24 * 60 * 60 * 1000 }, // 15 intentos: 24 horas
];

function getLockoutDuration(attempts: number): number {
  for (let i = LOCKOUT_PROGRESSION.length - 1; i >= 0; i--) {
    if (attempts >= LOCKOUT_PROGRESSION[i].threshold) {
      return LOCKOUT_PROGRESSION[i].duration;
    }
  }
  return 0;
}
```

---

## Security Logging

### Logger Configuration

```typescript
import winston from 'winston';

export const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'auth-security' },
  transports: [
    new winston.transports.File({
      filename: 'logs/security.log',
      level: 'warn',
    }),
    new winston.transports.File({
      filename: 'logs/security-audit.log',
      level: 'info',
    }),
  ],
});

// Eventos a logear
export enum SecurityEvent {
  LOGIN_SUCCESS = 'LOGIN_SUCCESS',
  LOGIN_FAILED = 'LOGIN_FAILED',
  LOGOUT = 'LOGOUT',
  PASSWORD_CHANGED = 'PASSWORD_CHANGED',
  TWO_FA_ENABLED = 'TWO_FA_ENABLED',
  TWO_FA_DISABLED = 'TWO_FA_DISABLED',
  ACCOUNT_LOCKED = 'ACCOUNT_LOCKED',
  TOKEN_REUSE_DETECTED = 'TOKEN_REUSE_DETECTED',
  SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
  NEW_DEVICE_LOGIN = 'NEW_DEVICE_LOGIN',
  NEW_LOCATION_LOGIN = 'NEW_LOCATION_LOGIN',
}

export function logSecurityEvent(
  event: SecurityEvent,
  data: {
    userId?: string;
    ip?: string;
    userAgent?: string;
    details?: Record<string, unknown>;
  }
): void {
  securityLogger.info({
    event,
    ...data,
    timestamp: new Date().toISOString(),
  });
}
```

---

## CSRF Protection

### Implementación

```typescript
import { doubleCsrf } from 'csrf-csrf';

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET!,
  cookieName: '__Host-csrf',
  cookieOptions: {
    httpOnly: true,
    sameSite: 'strict',
    secure: true,
    path: '/',
  },
  getTokenFromRequest: (req) => req.headers['x-csrf-token'] as string,
});

// Aplicar a rutas que modifican datos
app.use('/api/v1/auth', doubleCsrfProtection);

// Endpoint para obtener token
app.get('/api/v1/csrf-token', (req, res) => {
  res.json({ token: generateToken(req, res) });
});
```

---

## Anomaly Detection

### Detección de Acceso Sospechoso

```typescript
interface LoginContext {
  userId: string;
  ip: string;
  userAgent: string;
  location: LocationInfo;
  timestamp: Date;
}

export async function detectAnomalies(
  context: LoginContext
): Promise<string[]> {
  const anomalies: string[] = [];

  // Obtener historial de logins
  const history = await getLoginHistory(context.userId, 30); // últimos 30 días

  // 1. Nueva ubicación (país)
  const knownCountries = new Set(history.map((h) => h.location?.countryCode));
  if (!knownCountries.has(context.location.countryCode)) {
    anomalies.push('new_country');
  }

  // 2. Nuevo dispositivo
  const knownDevices = new Set(history.map((h) => h.deviceFingerprint));
  const currentFingerprint = generateFingerprint(context.userAgent);
  if (!knownDevices.has(currentFingerprint)) {
    anomalies.push('new_device');
  }

  // 3. IP en blacklist
  if (await isIpBlacklisted(context.ip)) {
    anomalies.push('blacklisted_ip');
  }

  // 4. Velocidad imposible (viaje)
  const lastLogin = history[0];
  if (lastLogin) {
    const travelSpeed = calculateTravelSpeed(
      lastLogin.location,
      context.location,
      lastLogin.timestamp,
      context.timestamp
    );
    if (travelSpeed > 1000) {
      // > 1000 km/h
      anomalies.push('impossible_travel');
    }
  }

  // 5. Horario inusual
  const hour = context.timestamp.getHours();
  const usualHours = calculateUsualHours(history);
  if (!usualHours.includes(hour)) {
    anomalies.push('unusual_time');
  }

  return anomalies;
}

export async function handleAnomalies(
  userId: string,
  anomalies: string[],
  context: LoginContext
): Promise<void> {
  if (anomalies.includes('blacklisted_ip')) {
    // Bloquear inmediatamente
    throw new ForbiddenError('Access denied');
  }

  if (anomalies.includes('impossible_travel')) {
    // Posible cuenta comprometida
    await revokeAllSessions(userId);
    await sendSecurityAlert(userId, 'impossible_travel_detected');
    throw new UnauthorizedError('Security check required');
  }

  if (anomalies.includes('new_country') || anomalies.includes('new_device')) {
    // Notificar pero permitir
    await sendNewLoginNotification(userId, context);
  }

  // Log todas las anomalías
  logSecurityEvent(SecurityEvent.SUSPICIOUS_ACTIVITY, {
    userId,
    ip: context.ip,
    details: { anomalies },
  });
}
```

---

## Secure Token Generation

```typescript
import crypto from 'crypto';

// Token de verificación de email (64 chars hex)
export function generateVerificationToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// OTP numérico (6 dígitos)
export function generateOTP(): string {
  return crypto.randomInt(100000, 999999).toString();
}

// Backup codes (8 chars alphanumeric)
export function generateBackupCodes(count: number = 10): string[] {
  return Array.from({ length: count }, () =>
    crypto.randomBytes(4).toString('hex').toUpperCase()
  );
}

// State token para OAuth
export function generateStateToken(): string {
  return crypto.randomBytes(16).toString('base64url');
}

// Session ID
export function generateSessionId(): string {
  return crypto.randomUUID();
}
```

---

## Referencias

- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/)
