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
wsaes exactamente igual que en el modo push (consulta 05). Este documento solo trata «cómo obtener elwsa».
1. Secuencia de extremo a extremo
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 |
Sí | URL de entrada de la app de extensión (app home URL) |
redirect_uri |
Sí | Dirección de callback; su host debe ser del mismo dominio que client_id (mismo scheme + host) |
state |
Sí | Cadena aleatoria CSRF, se devuelve tal cual en el callback para su verificación |
code_challenge |
Sí | base64url(sha256(code_verifier)), sin padding |
code_challenge_method |
Sí | 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": "…" }
Respuesta exitosa:
{
"code": 0,
"msg": "OK",
"data": { "wsa": "eyJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 300 }
}
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
Frontend: página de aterrizaje del callback
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) });
}
});
El
wsadevuelto tiene el mismo contrato JWT que en el modo push, y el uso deverifyWsaes idéntico palabra por palabra.
Restricciones de seguridad (lectura obligatoria)
- El
redirect_uridebe ser del mismo dominio que la app registrada: su scheme + host debe ser exactamente igual al declient_id./authorizees un endpoint de navegación del navegador y no puede devolver errores en JSON; cuando elredirect_urifalta / 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. - PKCE obligatorio: solo se acepta
code_challenge_method=S256(distingue mayúsculas y minúsculas), concode_challenge = base64url(sha256(code_verifier))sin padding. La versión actual no usaclient_secret; PKCE vincula la «sesión que inicia» con la «sesión que canjea». codede 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 devuelve403209 invalid_grant, y uncode_verifierque no coincide devuelve403210 invalid_verifier.state(CSRF): el SDK guarda elstatey elcode_verifierensessionStorage, y en el callback solo continúa si elstatecoincide.- 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 declient_id/redirect_urio dominio distinto,code_challengeinválido,code_challenge_methoddistinto deS256) 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.
