logo
Desarrollo
Buscar
04 · Inicio de sesión pull (Login with GPTBots Workspace / M-Auth)

04 · Inicio de sesión pull (Login with GPTBots Workspace / M-Auth)

Cuando la app de extensión del desarrollador es un sitio independiente y quiere ofrecer un botón «Login with GPTBots Workspace». Al hacer clic, el usuario va a iniciar sesión en GPTBots, selecciona el espacio de trabajo, y regresa con el estado de sesión y la información de identidad del usuario que ha iniciado sesión.

Utiliza código de autorización OAuth2 + PKCE: el navegador solo obtiene un code de un solo uso, y el verdadero wsa lo canjea tu backend con code + el code_verifier de PKCE; el wsa nunca entra en la URL / el historial / el Referer del navegador, lo que lo hace más seguro que el modo push.

La verificación una vez obtenido el wsa es exactamente igual que en el modo push (consulta 05). Este documento solo trata «cómo obtener el wsa».

1. Secuencia de extremo a extremo

loading...
sequenceDiagram
    participant WS as Página «Extensiones» del espacio de trabajo
    participant GB as Plataforma GPTBots
    participant FE as Página de aterrizaje de la app de extensión
    participant BE as Backend de la app de extensión

    Note over WS: El usuario hace clic en el icono de la app
    WS->>GB: POST sign-token
    Note over GB: Verifica que quien hace clic es miembro de ese workspace<br/>Firma el wsa con la clave (JWT de 5 minutos, aud=tu host)
    GB-->>WS: Devuelve el wsa
    WS->>FE: Abre app_home_url?wsa=JWT
    Note over FE: consumeHandoff(), lee el ?wsa=
    FE->>BE: POST /session/exchange (lleva el wsa)
    Note over BE: verifyWsa() → identity<br/>Establece la sesión propia
    BE-->>FE: identity
    Note over FE: history.replaceState elimina el ?wsa=

Endpoints de la plataforma

Uso Método Ruta
Entrada de autorización (navegación del navegador) GET /api/console/account/extension-app/authorize
Canje del token (backend → backend) POST /api/console/account/extension-app/token

Parámetros de /authorize

Parámetro Obligatorio Descripción
client_id URL de entrada de la app de extensión (app home URL)
redirect_uri Dirección de callback; su host debe ser del mismo dominio que client_id (mismo scheme + host)
state Cadena aleatoria CSRF, se devuelve tal cual en el callback para su verificación
code_challenge base64url(sha256(code_verifier)), sin padding
code_challenge_method Solo se acepta S256 (distingue mayúsculas y minúsculas)
workspace_id No Espacio de trabajo preseleccionado, omite la página de selección de organización

/authorize, según el estado de sesión, hace un 302 a: la página de inicio de sesión de GPTBots (no autenticado) / la página de selección de organización (autenticado pero sin organización seleccionada) / redirect_uri?code&state (organización ya seleccionada).

Petición y respuesta de /token

Cuerpo de la petición (invocado por el backend):

{ "code": "…", "codeVerifier": "…" }
                      
                      { "code": "…", "codeVerifier": "…" }

                    
Este bloque de código en una ventana flotante

Respuesta exitosa:

{ "code": 0, "msg": "OK", "data": { "wsa": "eyJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 300 } }
                      
                      {
  "code": 0,
  "msg": "OK",
  "data": { "wsa": "eyJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 300 }
}

                    
Este bloque de código en una ventana flotante

Integración con el SDK

Frontend: iniciar el login (al hacer clic en el botón)

import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk'; // Al hacer clic en «Login with GPTBots Workspace»: await startWorkspaceLogin({ authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize', clientId: 'https://app.example.com/land', // = la app home URL que registraste redirectUri: 'https://app.example.com/callback', // el host debe ser del mismo dominio que clientId // workspaceId: 'p-xxx', // opcional: espacio de trabajo preseleccionado, omite la página de selección de organización // state: '...', // opcional: por defecto genera automáticamente un state CSRF aleatorio de 16 bytes }); // El SDK hace automáticamente: genera PKCE(verifier→challenge), guarda verifier+state en sessionStorage, // verifica que authorizeUrl es una URL absoluta, exige un contexto seguro (HTTPS/localhost) y luego redirige a /authorize
                      
                      import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk';

// Al hacer clic en «Login with GPTBots Workspace»:
await startWorkspaceLogin({
  authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize',
  clientId: 'https://app.example.com/land',      // = la app home URL que registraste
  redirectUri: 'https://app.example.com/callback', // el host debe ser del mismo dominio que clientId
  // workspaceId: 'p-xxx',   // opcional: espacio de trabajo preseleccionado, omite la página de selección de organización
  // state: '...',           // opcional: por defecto genera automáticamente un state CSRF aleatorio de 16 bytes
});
// El SDK hace automáticamente: genera PKCE(verifier→challenge), guarda verifier+state en sessionStorage,
// verifica que authorizeUrl es una URL absoluta, exige un contexto seguro (HTTPS/localhost) y luego redirige a /authorize

                    
Este bloque de código en una ventana flotante

Frontend: página de aterrizaje del callback

loading...
sequenceDiagram
    participant FE as Frontend de la app de extensión
    participant GB as GPTBots
    participant BE as Backend de la app de extensión

    Note over FE: startWorkspaceLogin()<br/>genera PKCE(verifier→challenge)<br/>guarda en sessionStorage, redirige con 302
    FE->>GB: GET /authorize
    alt No autenticado
        GB-->>FE: 302 a la página de inicio de sesión de GPTBots (reutiliza el login existente)
    else Autenticado, sin organización seleccionada
        GB-->>FE: 302 a la página de selección de organización
    else Autenticado, organización seleccionada
        Note over GB: Emite un code de un solo uso (Redis,<br/>vinculado a account/project/app/redirect/challenge)
        GB-->>FE: 302 redirect_uri?code&state
    end
    Note over FE: completeWorkspaceLogin()<br/>verifica el state (CSRF), recupera el verifier
    FE->>BE: POST {code, codeVerifier}
    Note over BE: exchangeWorkspaceCode()
    BE->>GB: POST /token {code, verifier}
    Note over GB: Verifica el code (GETDEL de un solo uso) + PKCE<br/>firma el wsa
    GB-->>BE: Devuelve el wsa
    Note over BE: verifyWsa(wsa) → establece la sesión propia
    BE-->>FE: identity

Backend: canjear el wsa y verificarlo

import { exchangeWorkspaceCode, verifyWsa } from '@gptbots/workspace-extension-verify'; // POST /session/workspace-login { code, codeVerifier } app.post('/session/workspace-login', async (req, res) => { try { const { wsa } = await exchangeWorkspaceCode({ tokenUrl: 'https://www.gptbots.ai/api/console/account/extension-app/token', code: req.body.code, codeVerifier: req.body.codeVerifier, // timeoutMs: 10000, // por defecto 10s, evita que una respuesta lenta de la plataforma bloquee tu petición; pasa 0 para desactivarlo }); const identity = verifyWsa(wsa, { secret: process.env.EXTENSION_APP_SECRET, audience: 'app.example.com', }); const sid = createSession(identity); res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' }); res.json(identity); } catch (e) { res.status(401).json({ error: String(e) }); } });
                      
                      import { exchangeWorkspaceCode, verifyWsa } from '@gptbots/workspace-extension-verify';

// POST /session/workspace-login  { code, codeVerifier }
app.post('/session/workspace-login', async (req, res) => {
  try {
    const { wsa } = await exchangeWorkspaceCode({
      tokenUrl: 'https://www.gptbots.ai/api/console/account/extension-app/token',
      code: req.body.code,
      codeVerifier: req.body.codeVerifier,
      // timeoutMs: 10000,   // por defecto 10s, evita que una respuesta lenta de la plataforma bloquee tu petición; pasa 0 para desactivarlo
    });
    const identity = verifyWsa(wsa, {
      secret: process.env.EXTENSION_APP_SECRET,
      audience: 'app.example.com',
    });
    const sid = createSession(identity);
    res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
    res.json(identity);
  } catch (e) {
    res.status(401).json({ error: String(e) });
  }
});

                    
Este bloque de código en una ventana flotante

El wsa devuelto tiene el mismo contrato JWT que en el modo push, y el uso de verifyWsa es idéntico palabra por palabra.

Restricciones de seguridad (lectura obligatoria)

  1. El redirect_uri debe ser del mismo dominio que la app registrada: su scheme + host debe ser exactamente igual al de client_id. /authorize es un endpoint de navegación del navegador y no puede devolver errores en JSON; cuando el redirect_uri falta / no es http(s) / es de un dominio distinto, jamás redirige a una dirección no verificada, sino que vuelve a la página de selección de organización con ?error=invalid_request. Esta es la clave para prevenir redirecciones abiertas / filtración de tokens.
  2. PKCE obligatorio: solo se acepta code_challenge_method=S256 (distingue mayúsculas y minúsculas), con code_challenge = base64url(sha256(code_verifier)) sin padding. La versión actual no usa client_secret; PKCE vincula la «sesión que inicia» con la «sesión que canjea».
  3. code de un solo uso: se guarda en Redis, con TTL de 10 minutos, y se consume de forma atómica al canjearlo, por lo que no puede reproducirse. La reproducción/expiración devuelve 403209 invalid_grant, y un code_verifier que no coincide devuelve 403210 invalid_verifier.
  4. state (CSRF): el SDK guarda el state y el code_verifier en sessionStorage, y en el callback solo continúa si el state coincide.
  5. Alcance de la organización: al autorizar, se verifica que la cuenta es miembro del espacio de trabajo seleccionado y que la app está disponible en esa organización (las extensiones de uso propio de una organización solo son visibles para la organización a la que pertenecen); una vez que el administrador de la organización desactiva una app, no se emitirá una firma para esa organización aunque siga en el diccionario de la plataforma.

Códigos de error de la fase de canje de /token

Los errores estructurales de /authorize (falta de client_id/redirect_uri o dominio distinto, code_challenge inválido, code_challenge_method distinto de S256) no devuelven JSON, sino que hacen un 302 de vuelta a la página de selección de organización con ?error=invalid_request. La siguiente tabla solo lista los códigos de error en JSON de la fase de /token.

code Significado Condición de activación
403209 Invalid grant El code falta, ha expirado o ya ha sido usado (reproducción)
403210 Invalid verifier El code_verifier de PKCE no coincide con el code_challenge

Valores de ?error= en la URL de callback: invalid_request (error estructural) / access_denied (no es miembro o la app no está disponible en esa organización) / server_error (fallo inesperado). El completeWorkspaceLogin del SDK lo lanza como WorkspaceLoginError('AuthorizeError').

Siguiente paso: sea push o pull, para la verificación y la seguridad lee 05-Verificación de token y seguridad; para la API completa consulta 06-Referencia de la API del SDK.