- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
25 KiB
RF-AUTH-003: Proveedores de Autenticación OAuth
📋 Metadata
| Campo | Valor |
|---|---|
| ID | RF-AUTH-003 |
| Módulo | Autenticación y Autorización |
| Prioridad | Alta |
| Estado | ✅ Implementado |
| Versión | 1.0 |
| Fecha creación | 2025-11-07 |
| Última actualización | 2025-11-07 |
🔗 Referencias
Especificación Técnica
📐 ET-AUTH-003: OAuth 2.0 Providers
Implementación DDL
🗄️ ENUM Canónico:
- Ubicación:
apps/database/ddl/00-prerequisites.sql:38-40 - Tipo:
public.auth_provider - Valores:
local,google,facebook,apple,microsoft,github
🗄️ Tablas que usan el ENUM:
-
auth_management.auth_providers→apps/database/ddl/schemas/auth_management/tables/05-auth_providers.sql- Columna:
provider_name public.auth_provider NOT NULL - Configuración de cada proveedor OAuth
- Columna:
-
auth_management.profiles→apps/database/ddl/schemas/auth_management/tables/03-profiles.sql- Columna:
auth_provider public.auth_provider DEFAULT 'local' - Tracking del origen de registro
- Columna:
🗄️ Funciones:
auth.get_available_providers()→ Retorna lista de proveedores habilitadosauth.validate_oauth_token()→ Valida tokens OAuth
Backend
💻 Implementación:
- Enum:
apps/backend/src/modules/auth/enums/auth-provider.enum.ts - Strategies (Passport.js):
apps/backend/src/modules/auth/strategies/google.strategy.tsapps/backend/src/modules/auth/strategies/facebook.strategy.tsapps/backend/src/modules/auth/strategies/apple.strategy.tsapps/backend/src/modules/auth/strategies/microsoft.strategy.tsapps/backend/src/modules/auth/strategies/github.strategy.ts
- Config:
apps/backend/src/config/oauth.config.ts - Controllers:
apps/backend/src/modules/auth/controllers/oauth.controller.ts - Guards:
apps/backend/src/modules/auth/guards/oauth.guard.ts
Frontend
🎨 Componentes:
- Types:
apps/frontend/src/types/auth.types.ts - Componentes:
apps/frontend/src/components/auth/LoginProviderButtons.tsxapps/frontend/src/components/auth/ProviderIcon.tsxapps/frontend/src/components/auth/OAuthCallback.tsxapps/frontend/src/components/auth/LoginForm.tsx
Mapeo Completo
📊 Ver mapeo completo: Requerimientos → Implementación
📝 Descripción del Requerimiento
Contexto
La autenticación moderna requiere soportar múltiples métodos de inicio de sesión para mejorar la experiencia del usuario y reducir fricción en el onboarding. Los usuarios esperan poder registrarse con sus cuentas existentes de servicios populares.
Necesidad del Negocio
Problema: Depender únicamente de autenticación local (email/password):
- Aumenta fricción en el registro (crear contraseña, verificar email)
- Mayor probabilidad de abandono durante onboarding
- Usuarios olvidan contraseñas frecuentemente
- No aprovecha la confianza en proveedores establecidos
Solución: Implementar OAuth 2.0 con 6 proveedores de autenticación que cubren >90% de usuarios potenciales:
- Local: Para usuarios que prefieren control completo
- Google: Mayor penetración en educación (Google Workspace)
- Facebook: Alta penetración en México
- Apple: Requerido para iOS, énfasis en privacidad
- Microsoft: Común en instituciones educativas
- GitHub: Para desarrolladores y usuarios técnicos
🎯 Requerimiento Funcional
RF-AUTH-003.1: Proveedores Soportados
El sistema DEBE soportar 6 proveedores de autenticación:
1. Local (local) 🔐
Descripción: Autenticación tradicional con email y contraseña
Características:
- Email + contraseña almacenados en Supabase Auth
- Contraseña hasheada con bcrypt
- Verificación de email obligatoria
- Recuperación de contraseña vía email
Flujo de Registro:
1. Usuario ingresa email + contraseña
2. Sistema valida formato y fortaleza de contraseña
3. Sistema crea cuenta en auth.users (Supabase)
4. Sistema envía email de verificación
5. Estado inicial: pending
6. Usuario verifica email → estado: active
Ventajas:
- ✅ Control completo de credenciales
- ✅ No depende de terceros
- ✅ Funciona offline (una vez autenticado)
Desventajas:
- ❌ Usuario debe recordar contraseña
- ❌ Mayor fricción en registro
- ❌ Requiere verificación de email
Casos de Uso:
- Usuarios que priorizan privacidad
- Instituciones con políticas estrictas de seguridad
- Usuarios sin cuentas en otros proveedores
2. Google (google) 🔴🟡🟢🔵
Descripción: OAuth 2.0 con Google
Características:
- Integración con Google Workspace (común en educación)
- Obtiene: email, nombre, foto de perfil
- No requiere contraseña en Gamilit
Configuración OAuth:
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'https://gamilit.com/auth/google/callback',
scope: ['profile', 'email']
}
Flujo de Autenticación:
1. Usuario hace click en "Continuar con Google"
2. Redirige a consent screen de Google
3. Usuario autoriza permisos (profile, email)
4. Google redirige a callback con authorization code
5. Backend intercambia code por access token
6. Backend obtiene perfil de usuario vía Google People API
7. Sistema busca/crea usuario con email de Google
8. Sistema crea sesión y retorna JWT
Datos Obtenidos:
- ✅ Email (verificado por Google)
- ✅ Nombre completo
- ✅ Foto de perfil (avatar URL)
- ✅ Google ID (para vincular cuenta)
Ventajas:
- ✅ Sin fricción (1 click)
- ✅ Email pre-verificado
- ✅ Alta confianza del usuario
- ✅ Común en educación (Google Classroom)
Restricciones:
- Solo emails públicos (no G Suite corporativos con restricciones)
3. Facebook (facebook) 💙
Descripción: OAuth 2.0 con Facebook/Meta
Características:
- Alta penetración en México (~70% de usuarios de internet)
- Obtiene: email, nombre, foto
Configuración OAuth:
{
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: 'https://gamilit.com/auth/facebook/callback',
scope: ['public_profile', 'email'],
profileFields: ['id', 'emails', 'name', 'picture']
}
Consideraciones Especiales:
- Email puede no estar verificado → Sistema requiere verificación adicional
- Algunos usuarios no tienen email en Facebook → Solicitar email alternativo
Ventajas:
- ✅ Alta adopción en México
- ✅ Familiar para usuarios no técnicos
Desventajas:
- ⚠️ Email no siempre verificado
- ⚠️ Perfil puede ser incompleto
4. Apple (apple)
Descripción: Sign in with Apple (OAuth 2.0)
Características:
- Requerido para apps iOS (política de Apple)
- Énfasis en privacidad
- Permite ocultar email real (relay email)
Configuración OAuth:
{
clientID: process.env.APPLE_SERVICE_ID,
teamID: process.env.APPLE_TEAM_ID,
keyID: process.env.APPLE_KEY_ID,
privateKey: process.env.APPLE_PRIVATE_KEY,
callbackURL: 'https://gamilit.com/auth/apple/callback',
scope: ['name', 'email']
}
Peculiaridades de Apple:
-
Email Relay (Hide My Email):
- Usuario puede ocultar email real
- Apple proporciona relay:
xyz@privaterelay.appleid.com - Emails enviados a relay se reenvían al email real
- Sistema debe soportar relay emails
-
Datos solo en primer login:
- Nombre y email solo se proporcionan en primer login
- Logins subsecuentes solo retornan Apple ID
- Sistema debe almacenar datos en primer login
-
Token especial:
- Usa JWT firmado (no access token tradicional)
- Requiere validación de firma
Flujo Especial:
// Primer login
{
id: 'apple_user_id',
email: 'user@example.com', // o relay
name: { firstName: 'Juan', lastName: 'Pérez' }
}
// Logins subsecuentes
{
id: 'apple_user_id'
// No email, no name
}
Ventajas:
- ✅ Requerido para iOS
- ✅ Alta confianza en privacidad
- ✅ Email verificado por Apple
Complejidad:
- ⚠️ Configuración más compleja (certificates, keys)
- ⚠️ Datos solo en primer login
5. Microsoft (microsoft) 🟦
Descripción: OAuth 2.0 con Microsoft Account / Azure AD
Características:
- Común en instituciones educativas (Office 365 Education)
- Soporta cuentas personales y organizacionales
- Obtiene: email, nombre, foto
Configuración OAuth:
{
clientID: process.env.MICROSOFT_CLIENT_ID,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: 'https://gamilit.com/auth/microsoft/callback',
scope: ['User.Read'],
tenant: 'common' // Acepta cuentas personales y organizacionales
}
Tipos de Cuenta:
- Personal:
@outlook.com,@hotmail.com,@live.com - Organizacional:
@edu.mx,@universidad.edu, etc.
Ventajas:
- ✅ Común en instituciones educativas
- ✅ Integración con Office 365
- ✅ Soporte para cuentas institucionales
Consideraciones:
- Algunas organizaciones restringen OAuth externo
- Verificar permisos de tenant organization
6. GitHub (github) 🐙
Descripción: OAuth 2.0 con GitHub
Características:
- Para desarrolladores y usuarios técnicos
- Obtiene: email, nombre, username, avatar
- Útil para contenido de programación/lógica
Configuración OAuth:
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'https://gamilit.com/auth/github/callback',
scope: ['user:email']
}
Datos Obtenidos:
- ✅ Username (único, útil para mostrar)
- ✅ Email (puede requerir scope adicional si es privado)
- ✅ Avatar
- ✅ Bio
Ventajas:
- ✅ Preferido por usuarios técnicos
- ✅ Username como identificador amigable
Limitación:
- Email puede ser privado → Requiere scope
user:email
RF-AUTH-003.2: Flujo OAuth Unificado
Todos los proveedores OAuth DEBEN seguir este flujo:
┌─────────────┐
│ Usuario │
│ hace click │
│ en provider │
└──────┬──────┘
│
▼
┌──────────────────────────────────────┐
│ Frontend: Redirige a OAuth provider │
│ URL: /auth/{provider} │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Provider: Consent screen │
│ Usuario autoriza permisos │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Provider: Redirige a callback │
│ Con authorization code │
│ URL: /auth/{provider}/callback?code=│
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend: Intercambia code por token │
│ Obtiene perfil de usuario │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend: Busca/crea usuario │
│ - Buscar por email │
│ - Si no existe → crear │
│ - Si existe → actualizar profile │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend: Crea sesión │
│ Genera JWT con user_id + role │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend: Redirige a frontend │
│ Con JWT en query param o cookie │
│ URL: /auth/callback?token={jwt} │
└──────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Frontend: Almacena JWT │
│ Redirige a dashboard │
└──────────────────────────────────────┘
RF-AUTH-003.3: Vinculación de Cuentas
Usuario PUEDE vincular múltiples proveedores a una cuenta:
Escenario:
- Usuario se registra con Google → email:
juan@gmail.com - Más tarde, intenta login con Facebook → mismo email:
juan@gmail.com - Sistema detecta email existente
- Sistema ofrece vincular cuentas
Flujo de Vinculación:
// Usuario ya tiene cuenta con email X
const existingUser = await findUserByEmail(oauthProfile.email);
if (existingUser && existingUser.auth_provider !== currentProvider) {
// Mostrar modal: "Ya tienes cuenta con {existingProvider}. ¿Vincular?"
if (userConfirms) {
// Añadir nuevo provider a auth_providers
await linkProvider(existingUser.id, currentProvider, oauthProfile);
// Usuario ahora puede usar cualquiera de los proveedores
}
}
Tabla de Vinculación:
-- auth_management.linked_providers
CREATE TABLE auth_management.linked_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth_management.profiles(user_id),
provider public.auth_provider NOT NULL,
provider_user_id TEXT NOT NULL, -- ID del usuario en el provider
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, provider),
UNIQUE(provider, provider_user_id)
);
RF-AUTH-003.4: Manejo de Errores OAuth
El sistema DEBE manejar casos de error:
1. Usuario Cancela Autorización
Provider → callback?error=access_denied
Sistema → Redirige a login con mensaje:
"Cancelaste la autorización. Intenta de nuevo o usa otro método."
2. Email No Proporcionado
Provider → perfil sin email
Sistema → Solicita email al usuario:
"Necesitamos tu email para continuar. Por favor ingresa uno."
3. Email Ya Registrado con Otro Provider
Sistema → Modal:
"Ya tienes cuenta con {provider1}. ¿Deseas vincular {provider2}?"
[Vincular] [Cancelar]
4. Token Inválido/Expirado
Sistema → Reintenta obtener token
Si falla 3 veces → Error genérico:
"Error al autenticar. Intenta de nuevo."
5. Provider Temporalmente No Disponible
Sistema → Mensaje amigable:
"El servicio de {provider} no está disponible. Intenta otro método."
📊 Casos de Uso
UC-AUTH-005: Usuario se registra con Google
Actor: Usuario nuevo Precondiciones: Usuario tiene cuenta de Google
Flujo:
- Usuario visita página de registro
- Usuario hace click en "Continuar con Google"
- Frontend redirige a:
https://gamilit.com/auth/google - Backend redirige a Google consent screen
- Usuario selecciona cuenta de Google
- Usuario autoriza permisos (profile, email)
- Google redirige a:
/auth/google/callback?code=abc123 - Backend intercambia code por access token
- Backend obtiene perfil de Google:
{ "id": "google_user_id", "email": "juan@gmail.com", "name": "Juan Pérez", "picture": "https://..." } - Backend busca usuario por email → No existe
- Backend crea nuevo usuario:
INSERT INTO auth_management.profiles ( user_id, email, display_name, avatar_url, auth_provider, status, role ) VALUES ( gen_random_uuid(), 'juan@gmail.com', 'Juan Pérez', 'https://...', 'google', 'active', -- Pre-verificado por Google 'student' ); - Backend genera JWT
- Backend redirige a:
/auth/callback?token={jwt} - Frontend almacena JWT en localStorage
- Frontend redirige a dashboard
Resultado: Usuario registrado y autenticado con Google, sin crear contraseña
UC-AUTH-006: Usuario inicia sesión con Apple ID
Actor: Usuario existente (registrado previamente con local) Precondiciones: Usuario tiene cuenta local, quiere vincular Apple
Flujo:
- Usuario en configuración de cuenta
- Usuario hace click en "Vincular Apple ID"
- Sistema inicia flujo OAuth con Apple
- Usuario autoriza en Apple
- Apple redirige con authorization code
- Backend intercambia code por token
- Backend obtiene perfil:
{ "id": "apple_user_id", "email": "juan@privaterelay.appleid.com", // Relay "name": { "firstName": "Juan", "lastName": "Pérez" } } - Backend detecta que usuario ya está autenticado
- Backend crea vinculación:
INSERT INTO auth_management.linked_providers ( user_id, provider, provider_user_id ) VALUES ( current_user_id, 'apple', 'apple_user_id' ); - Sistema muestra notificación: "Apple ID vinculado exitosamente"
Resultado: Usuario puede usar Apple ID o local para login futuro
UC-AUTH-007: Error - Email ya registrado con otro provider
Actor: Usuario Precondiciones: Usuario se registró previamente con Google
Flujo:
- Usuario hace click en "Continuar con Facebook"
- Usuario autoriza en Facebook
- Backend obtiene perfil de Facebook → email:
juan@gmail.com - Backend busca usuario por email → Existe (registrado con Google)
- Backend verifica que
auth_provideres diferente:google≠facebook - Frontend muestra modal:
Ya tienes una cuenta con Google ¿Deseas vincular tu cuenta de Facebook? [Vincular Cuentas] [Cancelar] - Usuario hace click en "Vincular Cuentas"
- Sistema vincula Facebook a cuenta existente
- Usuario puede usar Google o Facebook para futuros logins
Resultado: Cuentas vinculadas, usuario tiene múltiples métodos de login
🔐 Consideraciones de Seguridad
1. Validación de Tokens
SIEMPRE validar tokens con el provider:
async function validateOAuthToken(provider: string, token: string) {
switch (provider) {
case 'google':
// Validar con Google tokeninfo endpoint
const response = await fetch(
`https://oauth2.googleapis.com/tokeninfo?access_token=${token}`
);
if (!response.ok) throw new UnauthorizedException();
break;
case 'apple':
// Validar firma JWT de Apple
const decoded = await verifyAppleJWT(token);
if (!decoded) throw new UnauthorizedException();
break;
// ... otros providers
}
}
2. State Parameter (CSRF Protection)
Usar state parameter para prevenir CSRF:
// Antes de redirigir a provider
const state = generateRandomString(32);
await storeState(userId, state, 300); // 5 min expiration
// Redirigir a provider con state
redirect(`${providerAuthUrl}?state=${state}&...`);
// En callback, validar state
const receivedState = req.query.state;
const storedState = await getStoredState(userId);
if (receivedState !== storedState) {
throw new ForbiddenException('Invalid state');
}
3. Rate Limiting
Prevenir abuse de OAuth endpoints:
@UseGuards(ThrottlerGuard)
@Throttle(5, 60) // 5 intentos por minuto
@Get('auth/:provider')
async oauthLogin(@Param('provider') provider: string) {
// ...
}
4. Validación de Redirect URI
CRÍTICO: Validar redirect URIs para prevenir Open Redirect:
const allowedRedirects = [
'https://gamilit.com/auth/callback',
'http://localhost:3000/auth/callback', // Solo en dev
];
if (!allowedRedirects.includes(redirectUri)) {
throw new BadRequestException('Invalid redirect URI');
}
5. Almacenamiento Seguro de Secrets
NUNCA hardcodear secrets:
// ❌ MAL
const googleClientSecret = 'abc123...';
// ✅ BIEN
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
// Validar que existan al iniciar
if (!googleClientSecret) {
throw new Error('GOOGLE_CLIENT_SECRET not configured');
}
✅ Criterios de Aceptación
AC-001: Proveedores Configurados
- 6 proveedores configurados en backend
- Credentials almacenados en variables de entorno
- Callbacks registrados en cada provider
AC-002: OAuth Flows Funcionales
- Usuario puede registrarse con cualquier provider
- Usuario puede iniciar sesión con cualquier provider
- Redirect correctamente después de auth
- JWT generado con user_id y role
AC-003: Vinculación de Cuentas
- Sistema detecta email duplicado
- Modal de vinculación se muestra
- Vinculación funciona correctamente
- Usuario puede usar múltiples providers
AC-004: Manejo de Errores
- Error de cancelación manejado
- Email faltante solicitado
- Token inválido manejado
- Provider no disponible manejado
AC-005: Seguridad
- Tokens validados con provider
- State parameter implementado
- Rate limiting en endpoints OAuth
- Redirect URIs validados
- Secrets en variables de entorno
AC-006: UI/UX
- Botones de providers visibles en login/registro
- Iconos de cada provider correctos
- Loading states durante OAuth flow
- Mensajes de error amigables
🧪 Testing
Test Case 1: Registro exitoso con Google
test('User can register with Google', async () => {
// Mock Google OAuth response
mockGoogleOAuth({
id: 'google_123',
email: 'test@gmail.com',
name: 'Test User',
picture: 'https://...',
});
const response = await request(app.getHttpServer())
.get('/auth/google/callback')
.query({ code: 'mock_code' });
expect(response.status).toBe(302); // Redirect
expect(response.headers.location).toContain('/auth/callback?token=');
// Verificar usuario creado
const user = await getUserByEmail('test@gmail.com');
expect(user).toBeDefined();
expect(user.auth_provider).toBe('google');
expect(user.status).toBe('active'); // Pre-verified
});
Test Case 2: Vinculación de cuentas
test('Can link multiple OAuth providers to same account', async () => {
// Crear usuario con Google
const user = await createUserWithProvider({
email: 'test@example.com',
provider: 'google',
});
// Intentar login con Facebook (mismo email)
await loginAs(user);
mockFacebookOAuth({
id: 'fb_123',
email: 'test@example.com',
});
const response = await request(app.getHttpServer())
.get('/auth/facebook/callback')
.query({ code: 'mock_code' });
// Verificar vinculación
const linkedProviders = await getLinkedProviders(user.id);
expect(linkedProviders).toHaveLength(2);
expect(linkedProviders).toContainEqual(
expect.objectContaining({ provider: 'google' })
);
expect(linkedProviders).toContainEqual(
expect.objectContaining({ provider: 'facebook' })
);
});
Test Case 3: Error - Usuario cancela OAuth
test('Handles OAuth cancellation gracefully', async () => {
const response = await request(app.getHttpServer())
.get('/auth/google/callback')
.query({ error: 'access_denied' });
expect(response.status).toBe(302);
expect(response.headers.location).toContain('/login');
expect(response.headers.location).toContain('error=cancelled');
});
📚 Referencias Adicionales
Documentos Relacionados
Documentación OAuth
- OAuth 2.0 RFC
- Google OAuth Documentation
- Facebook Login Documentation
- Sign in with Apple Documentation
- Microsoft Identity Platform
- GitHub OAuth Documentation
Seguridad
📅 Historial de Cambios
| Versión | Fecha | Autor | Cambios |
|---|---|---|---|
| 1.0 | 2025-11-07 | Database Team | Creación inicial del requerimiento |
Documento: docs/01-requerimientos/01-autenticacion-autorizacion/RF-AUTH-003-oauth.md
Ruta relativa desde docs/: 01-requerimientos/01-autenticacion-autorizacion/RF-AUTH-003-oauth.md