Nuevas Épicas (MCH-029 a MCH-033): - Infraestructura SaaS multi-tenant - Auth Social (OAuth2) - Auditoría Empresarial - Feature Flags - Onboarding Wizard Nuevas Integraciones (INT-010 a INT-014): - Email Providers (SendGrid, Mailgun, SES) - Storage Cloud (S3, GCS, Azure) - OAuth Social - Redis Cache - Webhooks Outbound Nuevos ADRs (0004 a 0011): - Notifications Realtime - Feature Flags Strategy - Storage Abstraction - Webhook Retry Strategy - Audit Log Retention - Rate Limiting - OAuth Social Implementation - Email Multi-provider Actualizados: - MASTER_INVENTORY.yml - CONTEXT-MAP.yml - HERENCIA-SIMCO.md - Mapas de documentación Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
259 lines
5.6 KiB
Markdown
259 lines
5.6 KiB
Markdown
---
|
|
id: ADR-0010
|
|
type: ADR
|
|
title: "OAuth Social Strategy"
|
|
status: Accepted
|
|
decision_date: 2026-01-10
|
|
updated_at: 2026-01-10
|
|
simco_version: "4.0.1"
|
|
stakeholders:
|
|
- "Equipo MiChangarrito"
|
|
tags:
|
|
- oauth
|
|
- authentication
|
|
- google
|
|
- apple
|
|
- passport
|
|
---
|
|
|
|
# ADR-0010: OAuth Social Strategy
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | ADR-0010 |
|
|
| **Estado** | Accepted |
|
|
| **Fecha** | 2026-01-10 |
|
|
| **Autor** | Architecture Team |
|
|
| **Supersede** | - |
|
|
|
|
---
|
|
|
|
## Contexto
|
|
|
|
MiChangarrito quiere permitir registro e inicio de sesion con cuentas sociales (Google, Apple) para reducir friccion de onboarding. Necesitamos decidir:
|
|
|
|
1. Que proveedores soportar
|
|
2. Como integrar con nuestro sistema de auth existente
|
|
3. Como manejar vinculacion de cuentas
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
**Implementamos OAuth 2.0 con Passport.js para Google y Apple, con tabla separada oauth_accounts vinculada a users.**
|
|
|
|
- Google: Principal proveedor social
|
|
- Apple: Requerido para iOS App Store
|
|
- Passport.js: Strategies bien mantenidas
|
|
|
|
---
|
|
|
|
## Alternativas Consideradas
|
|
|
|
### Opcion 1: Auth0/Firebase Auth
|
|
- **Pros:**
|
|
- Todo manejado
|
|
- Multiples providers
|
|
- **Cons:**
|
|
- Costo adicional
|
|
- Dependencia externa
|
|
- Menos control
|
|
|
|
### Opcion 2: Passport.js (Elegida)
|
|
- **Pros:**
|
|
- Open source
|
|
- Bien documentado
|
|
- Control total
|
|
- Sin costos adicionales
|
|
- **Cons:**
|
|
- Mas trabajo de implementacion
|
|
- Mantener actualizaciones
|
|
|
|
### Opcion 3: Implementacion manual
|
|
- **Pros:**
|
|
- Control absoluto
|
|
- **Cons:**
|
|
- Mucho trabajo
|
|
- Propenso a errores de seguridad
|
|
- Mantener cambios de API
|
|
|
|
---
|
|
|
|
## Consecuencias
|
|
|
|
### Positivas
|
|
|
|
1. **Control:** Logica de negocio en nuestro codigo
|
|
2. **Flexibilidad:** Agregar providers facilmente
|
|
3. **Integracion:** Se integra con nuestro JWT flow
|
|
4. **Costo:** Sin gastos adicionales
|
|
|
|
### Negativas
|
|
|
|
1. **Mantenimiento:** Actualizar strategies
|
|
2. **Configuracion:** Setup en consolas de providers
|
|
|
|
---
|
|
|
|
## Implementacion
|
|
|
|
### Modelo de Datos
|
|
|
|
```sql
|
|
CREATE TABLE auth.oauth_accounts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
|
provider VARCHAR(20) NOT NULL, -- google, apple
|
|
provider_user_id VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255),
|
|
name VARCHAR(255),
|
|
avatar_url TEXT,
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
expires_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
UNIQUE(provider, provider_user_id)
|
|
);
|
|
```
|
|
|
|
### Flujo de Autenticacion
|
|
|
|
```
|
|
1. Usuario hace clic en "Continuar con Google"
|
|
2. Redirect a Google OAuth
|
|
3. Usuario autoriza
|
|
4. Callback a nuestro servidor con code
|
|
5. Intercambiar code por tokens
|
|
6. Obtener perfil de usuario
|
|
7. Buscar/crear usuario en nuestra BD
|
|
8. Generar JWT nuestro
|
|
9. Redirect a frontend con JWT
|
|
```
|
|
|
|
### Casos de Vinculacion
|
|
|
|
| Escenario | Accion |
|
|
|-----------|--------|
|
|
| Nuevo usuario, nuevo email | Crear user + oauth_account |
|
|
| Email existe, mismo provider | Error (ya vinculado) |
|
|
| Email existe, otro provider | Ofrecer vincular cuentas |
|
|
| Usuario logueado vincula nueva | Agregar oauth_account |
|
|
|
|
### Codigo de Vinculacion
|
|
|
|
```typescript
|
|
async findOrCreateFromOAuth(profile: OAuthProfile): Promise<User> {
|
|
// 1. Buscar oauth_account existente
|
|
const existingOAuth = await this.oauthRepo.findOne({
|
|
where: {
|
|
provider: profile.provider,
|
|
providerUserId: profile.id,
|
|
},
|
|
});
|
|
|
|
if (existingOAuth) {
|
|
return existingOAuth.user;
|
|
}
|
|
|
|
// 2. Buscar user por email
|
|
const existingUser = await this.userRepo.findOne({
|
|
where: { email: profile.email },
|
|
});
|
|
|
|
if (existingUser) {
|
|
// Vincular automaticamente si email verificado en provider
|
|
if (profile.emailVerified) {
|
|
await this.createOAuthAccount(existingUser, profile);
|
|
return existingUser;
|
|
}
|
|
|
|
// Sino, pedir confirmacion
|
|
throw new EmailExistsError(profile.email, profile.provider);
|
|
}
|
|
|
|
// 3. Crear nuevo usuario
|
|
const user = await this.createUser({
|
|
email: profile.email,
|
|
name: profile.name,
|
|
avatarUrl: profile.avatar,
|
|
emailVerified: profile.emailVerified,
|
|
});
|
|
|
|
await this.createOAuthAccount(user, profile);
|
|
|
|
return user;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Providers Soportados
|
|
|
|
### Google
|
|
|
|
```typescript
|
|
passport.use(new GoogleStrategy({
|
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
callbackURL: '/auth/google/callback',
|
|
scope: ['profile', 'email'],
|
|
}, verify));
|
|
```
|
|
|
|
### Apple
|
|
|
|
```typescript
|
|
passport.use(new AppleStrategy({
|
|
clientID: process.env.APPLE_CLIENT_ID,
|
|
teamID: process.env.APPLE_TEAM_ID,
|
|
keyID: process.env.APPLE_KEY_ID,
|
|
privateKeyString: process.env.APPLE_PRIVATE_KEY,
|
|
callbackURL: '/auth/apple/callback',
|
|
scope: ['name', 'email'],
|
|
}, verify));
|
|
```
|
|
|
|
---
|
|
|
|
## Mobile
|
|
|
|
### Google (Expo)
|
|
|
|
```typescript
|
|
const [request, response, promptAsync] = Google.useAuthRequest({
|
|
clientId: GOOGLE_CLIENT_ID,
|
|
iosClientId: GOOGLE_IOS_CLIENT_ID,
|
|
androidClientId: GOOGLE_ANDROID_CLIENT_ID,
|
|
});
|
|
```
|
|
|
|
### Apple (iOS)
|
|
|
|
```typescript
|
|
import * as AppleAuthentication from 'expo-apple-authentication';
|
|
|
|
const credential = await AppleAuthentication.signInAsync({
|
|
requestedScopes: [
|
|
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
],
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [Passport.js](http://www.passportjs.org/)
|
|
- [Google Identity](https://developers.google.com/identity)
|
|
- [Sign in with Apple](https://developer.apple.com/sign-in-with-apple/)
|
|
- [INT-012: OAuth Social](../02-integraciones/INT-012-oauth-social.md)
|
|
- [MCH-030: Auth Social](../01-epicas/MCH-030-auth-social.md)
|
|
|
|
---
|
|
|
|
**Fecha decision:** 2026-01-10
|
|
**Autores:** Architecture Team
|