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

10 KiB

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

  1. Ir a Google Cloud Console
  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
  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


Ultima actualizacion: 2026-01-10 Autor: Backend Team