ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
483 lines
13 KiB
Markdown
483 lines
13 KiB
Markdown
---
|
|
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)
|