Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
25 KiB
25 KiB
| id | title | type | status | priority | epic | project | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ET-INV-007 | Seguridad y Validaciones | Technical Specification | Done | Alta | OQI-004 | trading-platform | 1.0.0 | 2025-12-05 | 2026-01-04 |
ET-INV-007: Seguridad y Validaciones
Epic: OQI-004 Cuentas de Inversión Versión: 1.0 Fecha: 2025-12-05 Responsable: Requirements-Analyst
1. Descripción
Define las medidas de seguridad, validaciones y controles para el módulo de cuentas de inversión:
- Autenticación y autorización
- Validación de datos de entrada
- KYC (Know Your Customer) básico
- Límites de transacciones
- Prevención de fraude
- Auditoría y logging
- Encriptación de datos sensibles
2. Arquitectura de Seguridad
┌─────────────────────────────────────────────────────────────────┐
│ Security Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Request Layer │ │
│ │ - Rate Limiting │ │
│ │ - CORS │ │
│ │ - Helmet (Security Headers) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Authentication Layer │ │
│ │ - JWT Validation │ │
│ │ - Token Refresh │ │
│ │ - Session Management │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Authorization Layer │ │
│ │ - Role-Based Access Control (RBAC) │ │
│ │ - Resource Ownership Verification │ │
│ │ - Action Permissions │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Validation Layer │ │
│ │ - Input Sanitization │ │
│ │ - Business Rules Validation │ │
│ │ - Amount Limits │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Fraud Detection │ │
│ │ - Suspicious Activity Detection │ │
│ │ - Velocity Checks │ │
│ │ - Anomaly Detection │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Audit & Logging │ │
│ │ - Activity Logs │ │
│ │ - Security Events │ │
│ │ - Compliance Reporting │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3. Autenticación y Autorización
3.1 Auth Middleware
// src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { AppError } from '../utils/errors';
import { logger } from '../utils/logger';
interface JwtPayload {
user_id: string;
email: string;
role: string;
exp: number;
}
// Extender Request type
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
role: string;
};
}
}
}
/**
* Middleware de autenticación JWT
*/
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AppError('No authentication token provided', 401);
}
const token = authHeader.substring(7);
const secret = process.env.JWT_SECRET!;
// Verificar token
const decoded = jwt.verify(token, secret) as JwtPayload;
// Adjuntar usuario al request
req.user = {
id: decoded.user_id,
email: decoded.email,
role: decoded.role,
};
// Log de acceso
logger.debug('User authenticated', {
user_id: req.user.id,
endpoint: req.path,
method: req.method,
});
next();
} catch (error: any) {
if (error.name === 'TokenExpiredError') {
logger.warn('Expired token', { path: req.path });
return next(new AppError('Token expired', 401));
}
if (error.name === 'JsonWebTokenError') {
logger.warn('Invalid token', { path: req.path });
return next(new AppError('Invalid token', 401));
}
next(error);
}
};
/**
* Middleware para requerir rol de admin
*/
export const requireAdmin = (
req: Request,
res: Response,
next: NextFunction
): void => {
if (!req.user) {
throw new AppError('Authentication required', 401);
}
if (req.user.role !== 'admin') {
logger.warn('Unauthorized admin access attempt', {
user_id: req.user.id,
role: req.user.role,
path: req.path,
});
throw new AppError('Admin access required', 403);
}
next();
};
/**
* Middleware para verificar ownership de recurso
*/
export const requireOwnership = (resourceType: 'account' | 'withdrawal') => {
return async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
if (!req.user) {
throw new AppError('Authentication required', 401);
}
const resourceId = req.params.id;
// Verificar ownership según tipo de recurso
// Esto se debe implementar en el repository
const repository = new InvestmentRepository();
let isOwner = false;
if (resourceType === 'account') {
const account = await repository.getAccountById(resourceId);
isOwner = account?.user_id === req.user.id;
} else if (resourceType === 'withdrawal') {
const withdrawal = await repository.getWithdrawalRequestById(resourceId);
isOwner = withdrawal?.user_id === req.user.id;
}
if (!isOwner && req.user.role !== 'admin') {
logger.warn('Unauthorized resource access attempt', {
user_id: req.user.id,
resource_type: resourceType,
resource_id: resourceId,
});
throw new AppError('Access denied', 403);
}
next();
} catch (error) {
next(error);
}
};
};
4. Validaciones de Negocio
4.1 Investment Validator Service
// src/services/validation/investment-validator.service.ts
import { InvestmentRepository } from '../../modules/investment/investment.repository';
import { AppError } from '../../utils/errors';
import { logger } from '../../utils/logger';
export class InvestmentValidatorService {
private repository: InvestmentRepository;
constructor() {
this.repository = new InvestmentRepository();
}
/**
* Valida que el usuario pueda crear una cuenta
*/
async validateAccountCreation(userId: string, productId: string): Promise<void> {
// Verificar KYC completado
const kycStatus = await this.checkKYCStatus(userId);
if (!kycStatus.completed) {
throw new AppError('KYC verification required before investing', 400);
}
// Verificar límite de cuentas por usuario
const accountCount = await this.repository.getAccountCountByUser(userId);
const MAX_ACCOUNTS = parseInt(process.env.MAX_ACCOUNTS_PER_USER || '10');
if (accountCount >= MAX_ACCOUNTS) {
throw new AppError(`Maximum ${MAX_ACCOUNTS} accounts allowed`, 400);
}
// Verificar que no exista cuenta duplicada
const existingAccount = await this.repository.getAccountByUserAndProduct(
userId,
productId
);
if (existingAccount) {
throw new AppError('Account already exists for this product', 409);
}
}
/**
* Valida un depósito
*/
async validateDeposit(
userId: string,
accountId: string,
amount: number
): Promise<void> {
// Verificar cuenta existe y pertenece al usuario
const account = await this.repository.getAccountById(accountId);
if (!account) {
throw new AppError('Account not found', 404);
}
if (account.user_id !== userId) {
throw new AppError('Access denied', 403);
}
if (account.status !== 'active') {
throw new AppError('Account is not active', 409);
}
// Verificar monto mínimo
const MIN_DEPOSIT = parseFloat(process.env.MIN_DEPOSIT_AMOUNT || '50');
if (amount < MIN_DEPOSIT) {
throw new AppError(`Minimum deposit is $${MIN_DEPOSIT}`, 400);
}
// Verificar monto máximo diario
const dailyTotal = await this.repository.getDailyDepositTotal(userId);
const MAX_DAILY = parseFloat(process.env.MAX_DAILY_DEPOSIT || '50000');
if (dailyTotal + amount > MAX_DAILY) {
throw new AppError(`Daily deposit limit of $${MAX_DAILY} exceeded`, 400);
}
// Verificar límite del producto
const product = await this.repository.getProductById(account.product_id);
if (product.max_investment) {
const totalInvested = account.total_deposited + amount;
if (totalInvested > product.max_investment) {
throw new AppError(
`Maximum investment for this product is $${product.max_investment}`,
400
);
}
}
// Velocity check - máximo 5 depósitos por hora
const recentDeposits = await this.repository.getRecentDeposits(userId, 3600);
if (recentDeposits.length >= 5) {
throw new AppError('Too many deposits. Please try again later', 429);
}
}
/**
* Valida un retiro
*/
async validateWithdrawal(
userId: string,
accountId: string,
amount: number
): Promise<void> {
// Verificar cuenta
const account = await this.repository.getAccountById(accountId);
if (!account) {
throw new AppError('Account not found', 404);
}
if (account.user_id !== userId) {
throw new AppError('Access denied', 403);
}
if (account.status !== 'active') {
throw new AppError('Account is not active', 409);
}
// Verificar balance suficiente
if (amount > account.current_balance) {
throw new AppError('Insufficient balance', 400);
}
// Verificar monto mínimo de retiro
const MIN_WITHDRAWAL = parseFloat(process.env.MIN_WITHDRAWAL_AMOUNT || '50');
if (amount < MIN_WITHDRAWAL) {
throw new AppError(`Minimum withdrawal is $${MIN_WITHDRAWAL}`, 400);
}
// Verificar que no haya solicitud de retiro pendiente
const pendingWithdrawal = await this.repository.getPendingWithdrawalByAccount(
accountId
);
if (pendingWithdrawal) {
throw new AppError('There is already a pending withdrawal request', 409);
}
// Verificar límite diario de retiros
const dailyWithdrawals = await this.repository.getDailyWithdrawalTotal(userId);
const MAX_DAILY_WITHDRAWAL = parseFloat(
process.env.MAX_DAILY_WITHDRAWAL || '25000'
);
if (dailyWithdrawals + amount > MAX_DAILY_WITHDRAWAL) {
throw new AppError(
`Daily withdrawal limit of $${MAX_DAILY_WITHDRAWAL} exceeded`,
400
);
}
// Verificar lock period (ej: no retiros en primeros 30 días)
const LOCK_PERIOD_DAYS = parseInt(process.env.ACCOUNT_LOCK_PERIOD_DAYS || '0');
if (LOCK_PERIOD_DAYS > 0) {
const accountAge = Date.now() - new Date(account.opened_at).getTime();
const lockPeriodMs = LOCK_PERIOD_DAYS * 24 * 60 * 60 * 1000;
if (accountAge < lockPeriodMs) {
const daysRemaining = Math.ceil(
(lockPeriodMs - accountAge) / (24 * 60 * 60 * 1000)
);
throw new AppError(
`Account is locked for withdrawals. ${daysRemaining} days remaining`,
400
);
}
}
}
/**
* Verifica estado de KYC del usuario
*/
private async checkKYCStatus(userId: string): Promise<{
completed: boolean;
level: string;
}> {
// Implementar integración con servicio de KYC
// Por ahora, retornar mock
return {
completed: true,
level: 'basic',
};
}
/**
* Detecta actividad sospechosa
*/
async detectSuspiciousActivity(userId: string): Promise<boolean> {
// Múltiples cuentas creadas en corto tiempo
const recentAccounts = await this.repository.getRecentAccountsByUser(
userId,
86400
); // 24h
if (recentAccounts.length >= 3) {
logger.warn('Suspicious: Multiple accounts created', { user_id: userId });
return true;
}
// Patrón de depósito-retiro rápido
const recentTransactions = await this.repository.getRecentTransactions(
userId,
3600
); // 1h
const hasDepositAndWithdrawal =
recentTransactions.some((t) => t.type === 'deposit') &&
recentTransactions.some((t) => t.type === 'withdrawal');
if (hasDepositAndWithdrawal) {
logger.warn('Suspicious: Quick deposit-withdrawal pattern', {
user_id: userId,
});
return true;
}
return false;
}
}
5. Rate Limiting
5.1 Rate Limiter Middleware
// src/middlewares/rate-limit.middleware.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
// Cliente Redis para rate limiting distribuido
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
redisClient.connect();
/**
* Rate limiter general para API
*/
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // 100 requests por ventana
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
client: redisClient,
prefix: 'rl:api:',
}),
});
/**
* Rate limiter estricto para operaciones sensibles
*/
export const strictLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 5, // 5 requests por hora
message: 'Too many requests for this operation',
store: new RedisStore({
client: redisClient,
prefix: 'rl:strict:',
}),
});
/**
* Rate limiter personalizado
*/
export const customRateLimit = (maxRequests: number, windowSeconds: number) => {
return rateLimit({
windowMs: windowSeconds * 1000,
max: maxRequests,
store: new RedisStore({
client: redisClient,
prefix: 'rl:custom:',
}),
});
};
6. Encriptación de Datos
6.1 Encryption Service
// src/services/security/encryption.service.ts
import crypto from 'crypto';
export class EncryptionService {
private algorithm = 'aes-256-gcm';
private key: Buffer;
constructor() {
const secret = process.env.ENCRYPTION_KEY!;
this.key = crypto.scryptSync(secret, 'salt', 32);
}
/**
* Encripta datos sensibles
*/
encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Retornar iv:authTag:encrypted
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
/**
* Desencripta datos
*/
decrypt(encryptedData: string): string {
const parts = encryptedData.split(':');
const iv = Buffer.from(parts[0], 'hex');
const authTag = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Hash de datos (one-way)
*/
hash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}
}
6.2 Uso en Modelo de Datos
// Encriptar datos de banco antes de guardar
const encryptionService = new EncryptionService();
const destinationDetails = {
bank_account: '****1234',
routing_number: encryptionService.encrypt('026009593'),
account_holder_name: 'John Doe',
};
await repository.createWithdrawalRequest({
// ...
destination_details: destinationDetails,
});
7. Auditoría y Logging
7.1 Audit Logger
// src/services/security/audit-logger.service.ts
import { logger } from '../../utils/logger';
export enum AuditAction {
ACCOUNT_CREATED = 'ACCOUNT_CREATED',
DEPOSIT_INITIATED = 'DEPOSIT_INITIATED',
DEPOSIT_COMPLETED = 'DEPOSIT_COMPLETED',
WITHDRAWAL_REQUESTED = 'WITHDRAWAL_REQUESTED',
WITHDRAWAL_APPROVED = 'WITHDRAWAL_APPROVED',
WITHDRAWAL_REJECTED = 'WITHDRAWAL_REJECTED',
WITHDRAWAL_COMPLETED = 'WITHDRAWAL_COMPLETED',
ACCOUNT_PAUSED = 'ACCOUNT_PAUSED',
ACCOUNT_CLOSED = 'ACCOUNT_CLOSED',
SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
}
interface AuditLogEntry {
action: AuditAction;
user_id: string;
resource_type: string;
resource_id: string;
details?: Record<string, any>;
ip_address?: string;
user_agent?: string;
}
export class AuditLoggerService {
/**
* Registra evento de auditoría
*/
log(entry: AuditLogEntry): void {
logger.info('AUDIT', {
timestamp: new Date().toISOString(),
action: entry.action,
user_id: entry.user_id,
resource_type: entry.resource_type,
resource_id: entry.resource_id,
details: entry.details,
ip_address: entry.ip_address,
user_agent: entry.user_agent,
});
// Opcionalmente, guardar en tabla de auditoría
// await auditRepository.create(entry);
}
/**
* Log de evento de seguridad
*/
logSecurityEvent(
event: string,
userId: string,
severity: 'low' | 'medium' | 'high' | 'critical',
details?: Record<string, any>
): void {
logger.warn('SECURITY_EVENT', {
timestamp: new Date().toISOString(),
event,
user_id: userId,
severity,
details,
});
// Si es crítico, enviar alerta
if (severity === 'critical') {
// await alertService.sendSecurityAlert(event, userId, details);
}
}
}
7.2 Uso en Controllers
// En InvestmentController
const auditLogger = new AuditLoggerService();
async createAccount(req: Request, res: Response) {
// ... lógica de creación ...
auditLogger.log({
action: AuditAction.ACCOUNT_CREATED,
user_id: req.user!.id,
resource_type: 'account',
resource_id: account.id,
details: {
product_id: data.product_id,
initial_investment: data.initial_investment,
},
ip_address: req.ip,
user_agent: req.headers['user-agent'],
});
// ... respuesta ...
}
8. Configuración de Seguridad
8.1 Variables de Entorno
# Authentication
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRATION=1d
JWT_REFRESH_EXPIRATION=7d
# Encryption
ENCRYPTION_KEY=your-super-secret-encryption-key-32-chars
# Rate Limiting
REDIS_URL=redis://localhost:6379
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Business Limits
MAX_ACCOUNTS_PER_USER=10
MIN_DEPOSIT_AMOUNT=50.00
MAX_DAILY_DEPOSIT=50000.00
MIN_WITHDRAWAL_AMOUNT=50.00
MAX_DAILY_WITHDRAWAL=25000.00
ACCOUNT_LOCK_PERIOD_DAYS=30
# KYC
REQUIRE_KYC_FOR_INVESTMENT=true
KYC_SERVICE_URL=https://kyc-service.example.com
8.2 Helmet Configuration
// src/app.ts
import helmet from 'helmet';
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
})
);
9. Testing de Seguridad
9.1 Security Tests
// tests/security/auth.test.ts
describe('Authentication Security', () => {
it('should reject requests without token', async () => {
const response = await request(app).get('/api/v1/investment/accounts');
expect(response.status).toBe(401);
});
it('should reject expired tokens', async () => {
const expiredToken = generateExpiredToken();
const response = await request(app)
.get('/api/v1/investment/accounts')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
expect(response.body.error).toContain('expired');
});
it('should reject access to other users accounts', async () => {
const user1Token = await getAuthToken('user1');
const user2AccountId = 'account-belongs-to-user2';
const response = await request(app)
.get(`/api/v1/investment/accounts/${user2AccountId}`)
.set('Authorization', `Bearer ${user1Token}`);
expect(response.status).toBe(403);
});
});
10. Checklist de Seguridad
10.1 Pre-Deployment Security Checklist
- Todas las rutas requieren autenticación
- JWT secret es fuerte y único
- Encryption key es de 32 caracteres
- Rate limiting configurado en todos los endpoints
- CORS configurado correctamente
- Helmet habilitado con CSP
- Logs de auditoría funcionando
- Validaciones de input en todos los endpoints
- Ownership verificado en recursos sensibles
- KYC habilitado para nuevas cuentas
- Límites de transacciones configurados
- Datos de banco encriptados
- HTTPS forzado en producción
- Variables de entorno seguras
- Secrets no en código fuente
11. Referencias
- OWASP Top 10
- JWT Best Practices
- PCI DSS Compliance
- GDPR Data Protection
- Express.js Security Best Practices