--- 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