trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-001-oauth.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
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>
2026-01-07 09:31:29 -06:00

609 lines
20 KiB
Markdown

---
id: "ET-AUTH-001"
title: "OAuth Providers Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-001"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-001: Especificación Técnica - OAuth Providers
**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** ✅ Implementado
**Épica:** [OQI-001](../_MAP.md)
**Requerimiento:** [RF-AUTH-001](../requerimientos/RF-AUTH-001-oauth.md)
---
## Resumen
Esta especificación detalla la implementación técnica del sistema de autenticación OAuth 2.0 multi-proveedor para Trading Platform.
---
## Arquitectura
```
┌─────────────────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SocialLogin │ │ AuthCallback │ │ AuthStore │ │
│ │ Buttons.tsx │───▶│ Page.tsx │───▶│ (Zustand) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└────────────────────────────────┬────────────────────────────────────────┘
│ HTTPS
┌─────────────────────────────────────────────────────────────────────────┐
│ BACKEND │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ auth.routes.ts │───▶│ oauth.service │───▶│ token.service │ │
│ │ │ │ .ts │ │ .ts │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL (public schema) │ │
│ │ ┌──────────┐ ┌────────────────┐ ┌──────────────────────┐ │ │
│ │ │ users │ │ oauth_accounts │ │ sessions │ │ │
│ │ └──────────┘ └────────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ OAUTH PROVIDERS │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Google │ │ Facebook │ │ Twitter │ │ Apple │ │ GitHub │ │
│ └─────────┘ └──────────┘ └─────────┘ └─────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Componentes
### 1. OAuth Service (`oauth.service.ts`)
**Ubicación:** `apps/backend/src/modules/auth/services/oauth.service.ts`
```typescript
import { google } from 'googleapis';
import axios from 'axios';
export interface OAuthUserInfo {
provider: AuthProvider;
providerId: string;
email: string | null;
emailVerified: boolean;
firstName: string;
lastName: string;
avatarUrl: string | null;
accessToken: string;
refreshToken?: string;
}
export class OAuthService {
// Google OAuth
async getGoogleAuthUrl(state: string): Promise<string>;
async getGoogleUserInfo(code: string): Promise<OAuthUserInfo>;
// Facebook OAuth
async getFacebookAuthUrl(state: string): Promise<string>;
async getFacebookUserInfo(code: string): Promise<OAuthUserInfo>;
// Twitter OAuth 2.0
async getTwitterAuthUrl(state: string, codeChallenge: string): Promise<string>;
async getTwitterUserInfo(code: string, codeVerifier: string): Promise<OAuthUserInfo>;
// Apple Sign In
async getAppleAuthUrl(state: string): Promise<string>;
async getAppleUserInfo(code: string): Promise<OAuthUserInfo>;
// GitHub OAuth
async getGitHubAuthUrl(state: string): Promise<string>;
async getGitHubUserInfo(code: string): Promise<OAuthUserInfo>;
}
```
### 2. OAuth Routes
**Ubicación:** `apps/backend/src/modules/auth/auth.routes.ts`
```typescript
// Get authorization URL
router.get('/oauth/:provider/url', authController.getOAuthUrl);
// Exchange code for tokens
router.post('/oauth/:provider', authController.handleOAuthCallback);
// Unlink OAuth provider
router.delete('/oauth/:provider', authenticate, authController.unlinkOAuthProvider);
// List linked providers
router.get('/oauth/providers', authenticate, authController.getLinkedProviders);
```
---
## Configuración por Proveedor
### Google
```typescript
const googleConfig = {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: `${process.env.API_URL}/auth/oauth/google/callback`,
scopes: ['profile', 'email'],
accessType: 'offline',
prompt: 'consent',
};
// Authorization URL
const oauth2Client = new google.auth.OAuth2(
googleConfig.clientId,
googleConfig.clientSecret,
googleConfig.redirectUri
);
const authUrl = oauth2Client.generateAuthUrl({
access_type: googleConfig.accessType,
scope: googleConfig.scopes,
state: stateToken,
prompt: googleConfig.prompt,
});
```
### Facebook
```typescript
const facebookConfig = {
clientId: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
redirectUri: `${process.env.API_URL}/auth/oauth/facebook/callback`,
scopes: ['email', 'public_profile'],
};
// Authorization URL
const authUrl = `https://www.facebook.com/v18.0/dialog/oauth?` +
`client_id=${facebookConfig.clientId}` +
`&redirect_uri=${encodeURIComponent(facebookConfig.redirectUri)}` +
`&scope=${facebookConfig.scopes.join(',')}` +
`&state=${stateToken}`;
```
### X/Twitter (OAuth 2.0 with PKCE)
```typescript
const twitterConfig = {
clientId: process.env.TWITTER_CLIENT_ID,
clientSecret: process.env.TWITTER_CLIENT_SECRET,
redirectUri: `${process.env.API_URL}/auth/oauth/twitter/callback`,
scopes: ['tweet.read', 'users.read', 'offline.access'],
};
// PKCE challenge
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Authorization URL
const authUrl = `https://twitter.com/i/oauth2/authorize?` +
`response_type=code` +
`&client_id=${twitterConfig.clientId}` +
`&redirect_uri=${encodeURIComponent(twitterConfig.redirectUri)}` +
`&scope=${encodeURIComponent(twitterConfig.scopes.join(' '))}` +
`&state=${stateToken}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
```
### Apple Sign In
```typescript
const appleConfig = {
clientId: process.env.APPLE_CLIENT_ID, // Service ID
teamId: process.env.APPLE_TEAM_ID,
keyId: process.env.APPLE_KEY_ID,
privateKey: process.env.APPLE_PRIVATE_KEY,
redirectUri: `${process.env.API_URL}/auth/oauth/apple/callback`,
scopes: ['name', 'email'],
};
// Generate client secret (JWT)
function generateAppleClientSecret(): string {
return jwt.sign({}, appleConfig.privateKey, {
algorithm: 'ES256',
expiresIn: '180d',
audience: 'https://appleid.apple.com',
issuer: appleConfig.teamId,
subject: appleConfig.clientId,
keyid: appleConfig.keyId,
});
}
```
### GitHub
```typescript
const githubConfig = {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
redirectUri: `${process.env.API_URL}/auth/oauth/github/callback`,
scopes: ['read:user', 'user:email'],
};
// Authorization URL
const authUrl = `https://github.com/login/oauth/authorize?` +
`client_id=${githubConfig.clientId}` +
`&redirect_uri=${encodeURIComponent(githubConfig.redirectUri)}` +
`&scope=${encodeURIComponent(githubConfig.scopes.join(' '))}` +
`&state=${stateToken}`;
```
---
## Esquema de Base de Datos
### Tabla: oauth_accounts
```sql
CREATE TABLE oauth_accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider auth_provider_enum NOT NULL,
provider_user_id VARCHAR(255) NOT NULL,
email VARCHAR(255),
access_token TEXT, -- Encriptado
refresh_token TEXT, -- Encriptado
token_expires_at TIMESTAMPTZ,
raw_profile JSONB, -- Perfil completo del proveedor
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(provider, provider_user_id),
UNIQUE(user_id, provider)
);
-- ENUM para proveedores
CREATE TYPE auth_provider_enum AS ENUM (
'google',
'facebook',
'twitter',
'apple',
'github'
);
-- Índices
CREATE INDEX idx_oauth_accounts_user_id ON oauth_accounts(user_id);
CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider);
CREATE INDEX idx_oauth_accounts_email ON oauth_accounts(email);
```
---
## Flujo de Implementación
### 1. Obtener URL de Autorización
```typescript
// GET /auth/oauth/:provider/url
async getOAuthUrl(req: Request, res: Response) {
const { provider } = req.params;
const { redirectTo } = req.query;
// Generar state con información de redirect
const state = await this.generateState({
provider,
redirectTo: redirectTo || '/dashboard',
timestamp: Date.now(),
});
// Almacenar state en Redis (10 min TTL)
await redis.setEx(`oauth:state:${state}`, 600, JSON.stringify({
provider,
redirectTo,
createdAt: Date.now(),
}));
let authUrl: string;
let codeVerifier: string | null = null;
switch (provider) {
case 'google':
authUrl = await this.oauthService.getGoogleAuthUrl(state);
break;
case 'twitter':
codeVerifier = generateCodeVerifier();
await redis.setEx(`oauth:pkce:${state}`, 600, codeVerifier);
authUrl = await this.oauthService.getTwitterAuthUrl(state, codeVerifier);
break;
// ... otros proveedores
}
res.json({ authUrl, state });
}
```
### 2. Manejar Callback
```typescript
// POST /auth/oauth/:provider
async handleOAuthCallback(req: Request, res: Response) {
const { provider } = req.params;
const { code, state } = req.body;
// Validar state
const stateData = await redis.get(`oauth:state:${state}`);
if (!stateData) {
throw new UnauthorizedError('Invalid or expired state');
}
await redis.del(`oauth:state:${state}`);
// Obtener user info del proveedor
let userInfo: OAuthUserInfo;
switch (provider) {
case 'google':
userInfo = await this.oauthService.getGoogleUserInfo(code);
break;
case 'twitter':
const codeVerifier = await redis.get(`oauth:pkce:${state}`);
userInfo = await this.oauthService.getTwitterUserInfo(code, codeVerifier);
await redis.del(`oauth:pkce:${state}`);
break;
// ... otros proveedores
}
// Buscar o crear usuario
const user = await this.findOrCreateUser(userInfo);
// Generar tokens JWT
const tokens = await this.tokenService.generateTokens(user);
res.json({
user: sanitizeUser(user),
tokens,
redirectTo: JSON.parse(stateData).redirectTo,
});
}
```
### 3. Buscar o Crear Usuario
```typescript
async findOrCreateUser(oauthInfo: OAuthUserInfo): Promise<User> {
// Buscar cuenta OAuth existente
let oauthAccount = await db.query(`
SELECT * FROM oauth_accounts
WHERE provider = $1 AND provider_user_id = $2
`, [oauthInfo.provider, oauthInfo.providerId]);
if (oauthAccount.rows[0]) {
// Usuario existente - actualizar tokens
await this.updateOAuthTokens(oauthAccount.rows[0].id, oauthInfo);
return this.userService.findById(oauthAccount.rows[0].user_id);
}
// Buscar usuario por email (si disponible)
if (oauthInfo.email) {
const existingUser = await this.userService.findByEmail(oauthInfo.email);
if (existingUser) {
// Vincular OAuth a usuario existente
await this.linkOAuthAccount(existingUser.id, oauthInfo);
return existingUser;
}
}
// Crear nuevo usuario
const newUser = await this.userService.create({
email: oauthInfo.email,
firstName: oauthInfo.firstName,
lastName: oauthInfo.lastName,
avatarUrl: oauthInfo.avatarUrl,
emailVerified: oauthInfo.emailVerified,
status: oauthInfo.emailVerified ? 'active' : 'pending_verification',
});
// Vincular cuenta OAuth
await this.linkOAuthAccount(newUser.id, oauthInfo);
return newUser;
}
```
---
## Componentes Frontend
### SocialLoginButtons
```typescript
// apps/frontend/src/modules/auth/components/SocialLoginButtons.tsx
export const SocialLoginButtons: React.FC<Props> = ({ mode, onStart, onSuccess, onError }) => {
const handleOAuthLogin = async (provider: OAuthProvider) => {
onStart?.();
try {
// Obtener URL de autorización
const { data } = await api.get(`/auth/oauth/${provider}/url`, {
params: { redirectTo: window.location.pathname },
});
// Guardar state para verificación
sessionStorage.setItem('oauth_state', data.state);
// Redirigir a proveedor
window.location.href = data.authUrl;
} catch (error) {
onError?.(error);
}
};
return (
<div className="space-y-3">
<OAuthButton provider="google" onClick={() => handleOAuthLogin('google')} />
<OAuthButton provider="facebook" onClick={() => handleOAuthLogin('facebook')} />
<OAuthButton provider="twitter" onClick={() => handleOAuthLogin('twitter')} />
<OAuthButton provider="apple" onClick={() => handleOAuthLogin('apple')} />
<OAuthButton provider="github" onClick={() => handleOAuthLogin('github')} />
</div>
);
};
```
### AuthCallback Page
```typescript
// apps/frontend/src/modules/auth/pages/AuthCallback.tsx
export const AuthCallback: React.FC = () => {
const navigate = useNavigate();
const { setUser, setTokens } = useAuthStore();
useEffect(() => {
const handleCallback = async () => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const provider = params.get('provider') || extractProvider(pathname);
// Validar state
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
navigate('/login?error=invalid_state');
return;
}
sessionStorage.removeItem('oauth_state');
try {
const { data } = await api.post(`/auth/oauth/${provider}`, {
code,
state,
});
setUser(data.user);
setTokens(data.tokens);
navigate(data.redirectTo || '/dashboard');
} catch (error) {
navigate(`/login?error=${error.code}`);
}
};
handleCallback();
}, []);
return <LoadingSpinner message="Completando inicio de sesión..." />;
};
```
---
## Seguridad
### Encriptación de Tokens
```typescript
import crypto from 'crypto';
const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; // 32 bytes
const IV_LENGTH = 16;
export function encryptToken(token: string): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decryptToken(encrypted: string): string {
const [ivHex, authTagHex, encryptedData] = encrypted.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
```
### PKCE para OAuth 2.0
```typescript
import crypto from 'crypto';
export function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
export function generateCodeChallenge(verifier: string): string {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
```
---
## Testing
### Unit Tests
```typescript
describe('OAuthService', () => {
describe('getGoogleUserInfo', () => {
it('should return user info from Google', async () => {
const mockCode = 'test_code';
// Mock Google API response
nock('https://oauth2.googleapis.com')
.post('/token')
.reply(200, { access_token: 'test_token' });
nock('https://www.googleapis.com')
.get('/oauth2/v2/userinfo')
.reply(200, {
id: '12345',
email: 'test@gmail.com',
verified_email: true,
given_name: 'Test',
family_name: 'User',
picture: 'https://...',
});
const result = await oauthService.getGoogleUserInfo(mockCode);
expect(result.provider).toBe('google');
expect(result.email).toBe('test@gmail.com');
expect(result.emailVerified).toBe(true);
});
});
});
```
---
## Referencias
- [Google OAuth 2.0 Documentation](https://developers.google.com/identity/protocols/oauth2)
- [Facebook Login Documentation](https://developers.facebook.com/docs/facebook-login)
- [Twitter OAuth 2.0 Documentation](https://developer.twitter.com/en/docs/authentication/oauth-2-0)
- [Sign in with Apple Documentation](https://developer.apple.com/documentation/sign_in_with_apple)
- [GitHub OAuth Apps Documentation](https://docs.github.com/en/apps/oauth-apps)