---
id: "ADR-007-security"
title: "Seguridad y Autenticación"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---

# ADR-006: Seguridad y Autenticación

**Estado:** Aceptado
**Fecha:** 2025-12-06
**Decisores:** Tech Lead, Arquitecto, Security Engineer
**Relacionado:** ADR-001, ADR-005

---

## Contexto

Trading Platform maneja datos financieros críticos y transacciones reales. Necesitamos:

1. **Authentication**: Identificar usuarios de forma segura
2. **Authorization**: Controlar acceso a recursos (portfolios, predictions, pagos)
3. **Data Protection**: Encriptar datos sensibles (passwords, API keys, PII)
4. **Attack Prevention**: Proteger contra brute force, XSS, CSRF, SQL injection
5. **Compliance**: Preparar para GDPR, PCI-DSS (pagos con Stripe)
6. **Audit Trail**: Logs de acciones críticas (trades, transfers, config changes)

Requisitos de Seguridad:
- Zero-trust: Never trust, always verify
- Defense in depth: Múltiples capas de seguridad
- Fail securely: Errores deben denegar acceso por defecto

---

## Decisión

### Authentication Strategy

**JWT (JSON Web Tokens) + Refresh Tokens**

#### Access Token (Short-lived)
```typescript
{
  "sub": "user_123abc",           // User ID
  "email": "user@example.com",
  "role": "premium",              // User tier
  "iat": 1701878400,              // Issued at
  "exp": 1701882000               // Expires in 1 hour
}
```

#### Refresh Token (Long-lived)
- Stored in **httpOnly cookie** (no JavaScript access)
- TTL: 7 days
- Rotated on each use (automatic refresh)
- Revocable via Redis blacklist

### Password Security

**bcrypt** with cost factor 12

```typescript
// apps/backend/src/services/auth.service.ts
import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12; // 2^12 iterations (~250ms)

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

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

### Password Requirements

```typescript
const PASSWORD_POLICY = {
  minLength: 12,
  requireUppercase: true,
  requireLowercase: true,
  requireNumber: true,
  requireSpecial: true,
  maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days
  preventReuse: 5, // Last 5 passwords
};
```

### Multi-Factor Authentication (MFA)

**TOTP (Time-based One-Time Password)** using **speakeasy**

```typescript
// apps/backend/src/services/mfa.service.ts
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

async function generateMFASecret(userId: string) {
  const secret = speakeasy.generateSecret({
    name: `Trading Platform (${userId})`,
    issuer: 'Trading Platform'
  });

  const qrCode = await QRCode.toDataURL(secret.otpauth_url);

  // Store secret encrypted in DB
  await db.user.update({
    where: { id: userId },
    data: { mfaSecret: encrypt(secret.base32) }
  });

  return { secret: secret.base32, qrCode };
}

function verifyMFAToken(secret: string, token: string): boolean {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 2 // Allow ±2 time steps (60 seconds tolerance)
  });
}
```

### Rate Limiting

**Redis-based rate limiting** (ADR-005)

```typescript
// apps/backend/src/middleware/rateLimit.ts
const RATE_LIMITS = {
  // Auth endpoints (aggressive)
  login: { max: 5, window: 60 * 15 },      // 5 attempts per 15 min
  register: { max: 3, window: 60 * 60 },   // 3 per hour
  resetPassword: { max: 3, window: 60 * 60 },

  // API endpoints (generous)
  api: { max: 100, window: 60 },           // 100 per minute
  apiPremium: { max: 500, window: 60 },    // Premium users

  // ML predictions (moderate)
  mlPredictions: { max: 20, window: 60 },  // 20 per minute
};

async function rateLimit(
  key: string,
  config: RateLimitConfig
): Promise<boolean> {
  const current = await redis.incr(`ratelimit:${key}`);

  if (current === 1) {
    await redis.expire(`ratelimit:${key}`, config.window);
  }

  return current <= config.max;
}
```

### CORS Configuration

**Strict CORS** for API

```typescript
// apps/backend/src/config/cors.ts
import cors from 'cors';

const ALLOWED_ORIGINS = process.env.NODE_ENV === 'production'
  ? ['https://app.trading.com', 'https://trading.com']
  : ['http://localhost:5173', 'http://localhost:3000'];

export const corsOptions: cors.CorsOptions = {
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true, // Allow cookies
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400, // 24 hours
};
```

### SQL Injection Prevention

**Parameterized Queries** (Prisma ORM)

```typescript
// ✅ SAFE - Prisma uses parameterized queries
const user = await db.user.findUnique({
  where: { email: userInput }
});

// ❌ NEVER DO THIS
const users = await db.$queryRaw`
  SELECT * FROM users WHERE email = ${userInput}
`; // Vulnerable to SQL injection
```

### XSS Prevention

```typescript
// apps/backend/src/middleware/security.ts
import helmet from 'helmet';
import { sanitize } from 'dompurify';

// Helmet middleware (sets security headers)
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid 'unsafe-inline' in prod
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.trading.com"],
    },
  },
  hsts: {
    maxAge: 31536000, // 1 year
    includeSubDomains: true,
    preload: true,
  },
}));

// Sanitize user input
function sanitizeInput(input: string): string {
  return sanitize(input, {
    ALLOWED_TAGS: [], // Strip all HTML
    ALLOWED_ATTR: [],
  });
}
```

### CSRF Protection

**Double Submit Cookie Pattern**

```typescript
// apps/backend/src/middleware/csrf.ts
import { randomBytes } from 'crypto';

function generateCSRFToken(): string {
  return randomBytes(32).toString('hex');
}

function csrfProtection(req: Request, res: Response, next: NextFunction) {
  // Skip for GET, HEAD, OPTIONS
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }

  const tokenFromHeader = req.headers['x-csrf-token'];
  const tokenFromCookie = req.cookies['csrf-token'];

  if (!tokenFromHeader || tokenFromHeader !== tokenFromCookie) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  next();
}
```

### Encryption

**AES-256-GCM** for sensitive data at rest

```typescript
// apps/backend/src/utils/crypto.ts
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes

export function encrypt(plaintext: string): string {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);

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

  const authTag = cipher.getAuthTag();

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

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

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

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

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

  return decrypted;
}
```

### Secure Headers

```typescript
// Set via Helmet
app.use(helmet({
  frameguard: { action: 'deny' },              // X-Frame-Options: DENY
  contentSecurityPolicy: true,                 // CSP
  hsts: { maxAge: 31536000 },                  // Strict-Transport-Security
  noSniff: true,                                // X-Content-Type-Options: nosniff
  xssFilter: true,                              // X-XSS-Protection
  referrerPolicy: { policy: 'strict-origin' }, // Referrer-Policy
}));
```

### Audit Logging

```typescript
// apps/backend/src/middleware/audit.ts
import { logger } from './logger';

const AUDITABLE_ACTIONS = [
  'LOGIN', 'LOGOUT', 'REGISTER',
  'PASSWORD_CHANGE', 'MFA_ENABLE', 'MFA_DISABLE',
  'TRADE_EXECUTED', 'FUND_TRANSFER', 'API_KEY_CREATED',
  'SUBSCRIPTION_CHANGED', 'PAYMENT_PROCESSED',
];

function auditLog(action: string, userId: string, metadata: object) {
  if (AUDITABLE_ACTIONS.includes(action)) {
    logger.info('audit_log', {
      action,
      userId,
      timestamp: new Date().toISOString(),
      ip: metadata.ip,
      userAgent: metadata.userAgent,
      metadata,
    });
  }
}

// Example usage
auditLog('TRADE_EXECUTED', user.id, {
  symbol: 'AAPL',
  quantity: 10,
  price: 150.25,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
});
```

---

## Consecuencias

### Positivas

1. **Zero Trust**: JWT stateless, cada request validado
2. **MFA Protection**: TOTP previene account takeover incluso con password leak
3. **Rate Limiting**: Brute force prácticamente imposible (5 attempts / 15 min)
4. **Defense in Depth**: Múltiples capas (bcrypt, JWT, MFA, rate limit, CORS)
5. **Audit Trail**: Compliance-ready logs de acciones críticas
6. **Encryption**: Datos sensibles encriptados at rest (AES-256-GCM)
7. **Secure Headers**: Helmet previene XSS, clickjacking, MIME sniffing

### Negativas

1. **UX Friction**: MFA agrega paso extra en login
2. **Complexity**: Refresh token rotation es complejo de implementar
3. **Performance**: bcrypt (250ms) y MFA verificación agregan latencia
4. **Token Management**: JWT blacklist requiere Redis storage
5. **Mobile Challenges**: httpOnly cookies no funcionan bien en mobile apps

### Riesgos y Mitigaciones

| Riesgo | Mitigación |
|--------|-----------|
| JWT secret leak | Rotate secrets monthly, use strong random (32+ bytes) |
| Brute force MFA | Rate limit: 5 attempts → lock account 1 hour |
| Session fixation | Regenerate session ID on login |
| MITM attacks | Enforce HTTPS only, HSTS header |
| Replay attacks | Short JWT TTL (1 hour), nonce for critical actions |
| Account enumeration | Generic error messages ("Invalid credentials") |

---

## Alternativas Consideradas

### 1. Session-Based Auth (Cookies)
- **Pros**: Simple, server-side revocation fácil
- **Contras**: Stateful, no funciona bien con mobile apps
- **Decisión**: ❌ Descartada - JWT es más flexible para multi-platform

### 2. OAuth 2.0 + Third-Party (Google, Facebook)
- **Pros**: No password management, MFA delegado
- **Contras**: Vendor dependency, no apropiado para finanzas
- **Decisión**: ⚠️ Complementario - Ofrecer como opción adicional

### 3. Argon2 para Password Hashing
- **Pros**: Más seguro que bcrypt, resistente a GPU attacks
- **Contras**: Menos maduro en Node.js ecosystem
- **Decisión**: ❌ Descartada - bcrypt es suficientemente seguro

### 4. SMS-based MFA
- **Pros**: User-friendly, no app required
- **Contras**: SIM swapping attacks, costo por SMS
- **Decisión**: ❌ Descartada - TOTP es más seguro y gratis

### 5. WebAuthn / FIDO2
- **Pros**: Phishing-resistant, passwordless future
- **Contras**: Browser support limitado, complejo de implementar
- **Decisión**: ❌ Pospuesta - Evaluar post-MVP

### 6. No Rate Limiting
- **Pros**: Menos complejidad
- **Contras**: Vulnerable a brute force y DDoS
- **Decisión**: ❌ Descartada - Rate limiting es crítico

---

## Security Checklist

### Development
- [ ] No hardcoded secrets (use .env)
- [ ] All inputs sanitized
- [ ] SQL injection tests
- [ ] XSS tests
- [ ] CSRF protection enabled
- [ ] Rate limiting configured
- [ ] Error messages don't leak info

### Deployment
- [ ] HTTPS enforced (no HTTP)
- [ ] Secrets in environment variables
- [ ] Database credentials rotated
- [ ] JWT secret rotated monthly
- [ ] Helmet middleware enabled
- [ ] CORS configured strictly
- [ ] Security headers validated

### Monitoring
- [ ] Failed login attempts tracked
- [ ] Rate limit violations alerted
- [ ] Suspicious activity flagged
- [ ] Audit logs reviewed weekly

---

## Compliance Roadmap

### GDPR (EU)
- [ ] User consent tracking
- [ ] Right to deletion (account deletion)
- [ ] Data export functionality
- [ ] Privacy policy
- [ ] Cookie consent banner

### PCI-DSS (Payments)
- [ ] Stripe handles card data (we never store cards)
- [ ] Encryption at rest and in transit
- [ ] Access controls
- [ ] Audit logging

---

## Referencias

- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [JWT Best Practices](https://datatracker.ietf.org/doc/html/rfc8725)
- [bcrypt Security](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [Helmet.js Documentation](https://helmetjs.github.io/)
- [TOTP RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)
