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>
364 lines
10 KiB
Markdown
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
|