Skip to content

Google Providers — Gmail, Sheets y Calendar

DaraMex expone tres capacidades Google sobre dos OAuth apps: una dedicada a Gmail (scopes restringidos) y otra a Workspace (Sheets + Calendar). La capacidad específica vive en el VO de configuración como capability.

Mapping capability → OAuth app

Capability (capability)OAuth app (provider)Provider runtime
gmailgoogle_gmailGoogleGmailProvider
sheetsgoogle_workspaceGoogleSheetsProvider
calendargoogle_workspaceGoogleCalendarProvider

La validación ocurre en dos puntos:

  1. ConnectionConfigFactory.create (dominio): rechaza una config cuyo capability no coincida con el provider (CONNECTION.INVALID_CONFIGURATION).
  2. StartGoogleOAuthHandler: antes de abrir el flujo OAuth, valida el mismo acoplamiento y retorna ConnectionOAuthErrors.providerCapabilityMismatch si no coincide.

ConnectionProviderFactory.create selecciona el IExternalProvider concreto (GoogleGmailProvider, GoogleSheetsProvider, GoogleCalendarProvider) leyendo la capability del VO. El provider alcanza sólo la app OAuth; la capability selecciona el cliente del API concreto.

Flujo de conexión

Paso 1 — Iniciar

http
POST /connections/oauth/{provider}/start
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "Gmail de soporte",
  "capability": "gmail",
  "visibility": "private"
}

provider es uno de google_gmail o google_workspace. capability debe corresponder al provider (ver tabla). visibility es opcional y por defecto "private".

Respuesta:

json
{
  "authUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
  "state": "<base64url-32-bytes>"
}

Paso 2 — Consentimiento en Google

El frontend redirige a authUrl. El usuario otorga permisos en Google.

Paso 3 — Callback (automático)

http
GET /connections/oauth/{provider}/callback?code=...&state=...

CompleteGoogleOAuthHandler consume el state en Redis, canjea el code por tokens, valida scopes, obtiene el email del userinfo, cifra los tokens y crea la conexión. Si CONNECTIONS_OAUTH_SUCCESS_URL está configurado, redirige con ?connectionId=<uuid>; si no, responde JSON con { connectionId, provider }.

Scopes por capability

Los scopes viven en el dominio (domain/constants/google-scopes.ts) porque son fijos por los APIs que se usan, no por el deployment. Un cambio en esta tabla obliga a reconsentir a todos los usuarios.

CapabilityScopes solicitados
gmailgmail.send, gmail.modify
sheetsspreadsheets, drive.file
calendarcalendar, calendar.events

Además, el flow service añade openid y email para poder resolver el googleAccountEmail vía userinfo.

Los scopes de Gmail son restringidos por Google y requieren verificación formal antes de onboarding de usuarios externos. Durante desarrollo, el proyecto puede permanecer en modo Testing con hasta 100 test users (ver runbook).

Visibility: private vs restricted

Al iniciar el flujo, el body incluye visibility:

json
{ "name": "Gmail de Juan",  "capability": "gmail",  "visibility": "private" }
{ "name": "Soporte general", "capability": "gmail", "visibility": "restricted" }
  • private (default): sólo el autor (addedById = user.sub) puede ver, actualizar o eliminar la conexión.
  • restricted: cualquier usuario dentro del mismo orgId + agencyId puede ver y usar la conexión.

El scope orgId + agencyId es siempre obligatorio; restricted no cruza organizaciones o agencias.

Multi-cuenta

Un mismo usuario puede tener varias conexiones activas del mismo provider y capability — por ejemplo dos google_gmail con capability gmail para distintas cuentas. Cada conexión mantiene su propio par (accessToken, refreshToken, googleAccountEmail).

Refresco automático de tokens

GoogleCredentialService.getFreshCredentials(connectionId) entrega siempre un accessToken fresco a los consumidores (AI tools, sync jobs). Si el token está dentro de la ventana de expiración (GOOGLE_TOKEN_REFRESH_SAFETY_WINDOW_MS, default 5 min), lo refresca contra Google antes de devolverlo.

No hay lock distribuido: dos refreshes concurrentes para la misma conexión son seguros — Google acepta ambos grants, el último save() en Postgres gana y ninguno invalida al otro. Ver la ADR de arquitectura OAuth para el análisis.

Qué hacer ante CONNECTION_REAUTH_REQUIRED

Ocurre cuando el refreshToken ya no es válido (Google devuelve invalid_grant en el refresh). Causas típicas:

  • El usuario revocó el acceso desde Google Account → Permissions.
  • Las credenciales de la OAuth app rotaron.
  • La conexión lleva demasiado tiempo inactiva bajo ciertas políticas de Workspace.

El handler marca la conexión como status = 'error' y el error emitido es:

json
{
  "error": "CONNECTION_REAUTH_REQUIRED",
  "connectionId": "01970000-0000-7000-...",
  "provider": "google_gmail",
  "capability": "gmail"
}

Solución: reconectar la cuenta desde la UI (nuevo flujo OAuth desde el inicio).

Troubleshooting

PROVIDER_NOT_CONFIGURED

El entorno no tiene GOOGLE_GMAIL_CLIENT_ID/SECRET o GOOGLE_WORKSPACE_CLIENT_ID/SECRET. Configurarlos en el entorno afectado (ver runbook).

OAUTH_STATE_INVALID

El state del callback expiró (TTL 600 s en Redis), ya fue consumido, o fue manipulado. Reiniciar el flujo desde el paso 1.

OAUTH_SCOPE_MISMATCH

El usuario desmarcó permisos en la pantalla de consentimiento. Reiniciar el flujo y aceptar todos los permisos requeridos.

CONNECTION.PROVIDER_CAPABILITY_MISMATCH

El body del start envió una capability que no coincide con el provider (ej. provider=google_gmail, capability=sheets). Revisar el mapping de la tabla al principio.

La conexión está en estado error

El token dejó de ser válido. Eliminar y reconectar.

Revocación al eliminar

DeleteConnectionHandler revoca el token de Google en modo fire-and-forget (void googleFlow.revokeToken(plainAccessToken).catch(...)) antes de eliminar la fila. La revocación es best-effort: si falla, el delete procede igualmente y se loguea google_token_revoke_failed.