| id |
type |
title |
provider |
status |
integration_type |
created_at |
updated_at |
simco_version |
tags |
| INT-012 |
Integration |
OAuth Social |
Google/Apple |
Planificado |
auth |
2026-01-10 |
2026-01-10 |
4.0.1 |
| 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
# 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
// 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);
}
));
// 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
@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
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)
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)
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
// 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
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
- Ir a Google Cloud Console
- Crear proyecto o seleccionar existente
- APIs & Services > Credentials
- Create Credentials > OAuth Client ID
- Application type: Web application
- Authorized redirect URIs:
https://api.michangarrito.com/auth/google/callback
http://localhost:3000/auth/google/callback
Apple Developer
- Ir a Apple Developer
- Certificates, Identifiers & Profiles
- Identifiers > App IDs > Agregar Sign in with Apple capability
- Identifiers > Services IDs > Crear para web
- Keys > Crear key con Sign in with Apple
- Descargar .p8 (solo se puede una vez)
11. Referencias
Ultima actualizacion: 2026-01-10
Autor: Backend Team