michangarrito/docs/02-integraciones/INT-012-oauth-social.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
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>
2026-01-13 01:43:15 -06:00

364 lines
10 KiB
Markdown

---
id: INT-012
type: Integration
title: "OAuth Social"
provider: "Google/Apple"
status: Planificado
integration_type: "auth"
created_at: 2026-01-10
updated_at: 2026-01-10
simco_version: "4.0.1"
tags:
- oauth
- authentication
- google
- apple
- social-login
---
# INT-012: OAuth Social
## Metadata
| Campo | Valor |
|-------|-------|
| **Codigo** | INT-012 |
| **Proveedor** | Google, Apple |
| **Tipo** | Autenticacion |
| **Estado** | Planificado |
| **Multi-tenant** | Si |
| **Epic Relacionada** | MCH-030 |
| **Owner** | Backend Team |
---
## 1. Descripcion
Integracion OAuth 2.0 para login social con Google y Apple. Permite a los usuarios registrarse e iniciar sesion con un clic usando sus cuentas existentes.
**Casos de uso principales:**
- Registro simplificado (un clic)
- Login sin password
- Vinculacion de cuenta social a cuenta existente
- Sync de perfil (nombre, foto)
---
## 2. Credenciales Requeridas
### Google OAuth
| Variable | Descripcion | Tipo | Obligatorio |
|----------|-------------|------|-------------|
| `GOOGLE_CLIENT_ID` | Client ID de Google Cloud | string | SI |
| `GOOGLE_CLIENT_SECRET` | Client Secret | string | SI |
| `GOOGLE_CALLBACK_URL` | URL de callback | string | SI |
### Apple Sign-In
| Variable | Descripcion | Tipo | Obligatorio |
|----------|-------------|------|-------------|
| `APPLE_CLIENT_ID` | Service ID (web) o App ID (iOS) | string | SI |
| `APPLE_TEAM_ID` | Team ID de Apple Developer | string | SI |
| `APPLE_KEY_ID` | Key ID del private key | string | SI |
| `APPLE_PRIVATE_KEY` | Private key (.p8 content) | string | SI |
| `APPLE_CALLBACK_URL` | URL de callback | string | SI |
### Ejemplo de .env
```env
# Google OAuth
GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxx
GOOGLE_CALLBACK_URL=https://api.michangarrito.com/auth/google/callback
# Apple Sign-In
APPLE_CLIENT_ID=com.michangarrito.web
APPLE_TEAM_ID=XXXXXXXXXX
APPLE_KEY_ID=XXXXXXXXXX
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGT....\n-----END PRIVATE KEY-----"
APPLE_CALLBACK_URL=https://api.michangarrito.com/auth/apple/callback
```
---
## 3. Flujo OAuth 2.0
### Google
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────>│ /auth/ │────>│ Google │────>│ Callback │
│ (Web/ │ │ google │ │ OAuth │ │ /auth/ │
│ Mobile) │ │ │ │ Screen │ │ google/ │
└──────────┘ └──────────┘ └──────────┘ │ callback │
└────┬─────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ JWT │<────│ Create/ │<────│ Verify │<────│ Get │
│ Token │ │ Link │ │ Token │ │ Profile │
│ │ │ User │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
### Endpoints
| Ruta | Metodo | Descripcion |
|------|--------|-------------|
| `/auth/google` | GET | Inicia flujo OAuth Google |
| `/auth/google/callback` | GET | Callback de Google |
| `/auth/apple` | GET | Inicia flujo Apple Sign-In |
| `/auth/apple/callback` | POST | Callback de Apple |
| `/auth/link/:provider` | POST | Vincular cuenta social |
| `/auth/unlink/:provider` | DELETE | Desvincular cuenta |
---
## 4. Implementacion
### Passport.js Strategies
```typescript
// Google Strategy
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['profile', 'email'],
},
async (accessToken, refreshToken, profile, done) => {
const user = await findOrCreateUser({
provider: 'google',
providerId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value,
});
return done(null, user);
}
));
```
```typescript
// Apple Strategy
import { Strategy as AppleStrategy } from 'passport-apple';
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: process.env.APPLE_CALLBACK_URL,
scope: ['name', 'email'],
},
async (accessToken, refreshToken, idToken, profile, done) => {
// Apple solo envia nombre en primer login
const user = await findOrCreateUser({
provider: 'apple',
providerId: profile.id,
email: profile.email,
name: profile.name?.firstName,
});
return done(null, user);
}
));
```
---
## 5. Manejo de Errores
| Error | Descripcion | Accion |
|-------|-------------|--------|
| access_denied | Usuario cancelo | Redirect a login |
| invalid_request | Parametros incorrectos | Log + error page |
| server_error | Error del provider | Retry o fallback |
| email_exists | Email ya registrado | Ofrecer vincular |
### Error Handling
```typescript
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleCallback(
@Req() req: Request,
@Res() res: Response,
) {
try {
const jwt = await this.authService.generateJwt(req.user);
res.redirect(`${FRONTEND_URL}/auth/callback?token=${jwt}`);
} catch (error) {
if (error instanceof EmailExistsError) {
res.redirect(`${FRONTEND_URL}/auth/link?provider=google&email=${error.email}`);
} else {
res.redirect(`${FRONTEND_URL}/auth/error?code=${error.code}`);
}
}
}
```
---
## 6. Tabla oauth_accounts
```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,
raw_profile JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(provider, provider_user_id),
UNIQUE(user_id, provider)
);
CREATE INDEX idx_oauth_accounts_user ON auth.oauth_accounts(user_id);
```
---
## 7. Multi-tenant
### Comportamiento
- OAuth es a nivel de **usuario**, no de tenant
- Un usuario puede pertenecer a multiples tenants
- Al login, se selecciona tenant activo
### Flujo Multi-tenant
```
1. Usuario hace login con Google
2. Sistema busca/crea usuario por email
3. Si usuario tiene multiples tenants:
- Redirect a selector de tenant
4. Si usuario tiene un solo tenant:
- Login directo a ese tenant
5. Si usuario no tiene tenant:
- Crear tenant o unirse a invitacion pendiente
```
---
## 8. Mobile Implementation
### Expo/React Native (Google)
```typescript
import * as Google from 'expo-auth-session/providers/google';
const [request, response, promptAsync] = Google.useAuthRequest({
clientId: GOOGLE_CLIENT_ID,
iosClientId: GOOGLE_IOS_CLIENT_ID,
androidClientId: GOOGLE_ANDROID_CLIENT_ID,
});
const handleGoogleLogin = async () => {
const result = await promptAsync();
if (result.type === 'success') {
const { id_token } = result.params;
await api.post('/auth/google/mobile', { id_token });
}
};
```
### iOS Native (Apple)
```typescript
import * as AppleAuthentication from 'expo-apple-authentication';
const handleAppleLogin = async () => {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
await api.post('/auth/apple/mobile', {
identityToken: credential.identityToken,
fullName: credential.fullName,
});
};
```
---
## 9. Testing
### Mock Providers
```typescript
// test/mocks/google-oauth.mock.ts
export const mockGoogleProfile = {
id: 'google-123',
displayName: 'Test User',
emails: [{ value: 'test@gmail.com', verified: true }],
photos: [{ value: 'https://photo.url' }],
};
```
### Test de Integracion
```typescript
describe('Google OAuth', () => {
it('should create new user on first login', async () => {
const response = await request(app)
.get('/auth/google/callback')
.query({ code: 'mock-code' });
expect(response.status).toBe(302);
expect(response.headers.location).toContain('token=');
});
});
```
---
## 10. Configuracion de Consolas
### Google Cloud Console
1. Ir a [Google Cloud Console](https://console.cloud.google.com)
2. Crear proyecto o seleccionar existente
3. APIs & Services > Credentials
4. Create Credentials > OAuth Client ID
5. Application type: Web application
6. Authorized redirect URIs:
- `https://api.michangarrito.com/auth/google/callback`
- `http://localhost:3000/auth/google/callback`
### Apple Developer
1. Ir a [Apple Developer](https://developer.apple.com)
2. Certificates, Identifiers & Profiles
3. Identifiers > App IDs > Agregar Sign in with Apple capability
4. Identifiers > Services IDs > Crear para web
5. Keys > Crear key con Sign in with Apple
6. Descargar .p8 (solo se puede una vez)
---
## 11. Referencias
- [Google Identity Platform](https://developers.google.com/identity)
- [Sign in with Apple](https://developer.apple.com/sign-in-with-apple/)
- [Passport.js Google OAuth](http://www.passportjs.org/packages/passport-google-oauth20/)
- [ADR-0010: OAuth Social Strategy](../97-adr/ADR-0010-oauth-social.md)
---
**Ultima actualizacion:** 2026-01-10
**Autor:** Backend Team