logo
Desarrollo
Buscar
Integración push (handshake de identidad / Push Handoff)

Integración push (handshake de identidad / Push Handoff)

Escenario: el usuario hace clic en la app de extensión del desarrollador en la página Espacio de trabajo → Extensiones, y la plataforma envía la identidad del usuario mediante ?wsa=<JWT> a la página de aterrizaje de la app de extensión de destino. El desarrollador la consume en la página de aterrizaje y, tras verificarla en el backend, la canjea por su propia sesión.

En este modo, el desarrollador no necesita invocar el endpoint de firma de la plataforma: ese lo invoca el frontend del espacio de trabajo de forma proactiva cuando el usuario hace clic. El desarrollador solo se encarga de «recibir y verificar».

Secuencia de extremo a extremo

loading...
sequenceDiagram
    autonumber
    actor User as El usuario accede a la app de extensión
    participant GB as Plataforma GPTBots
    participant FE as App de extensión - página de aterrizaje
    participant BE as App de extensión - backend

    User->>GB: Hace clic en el icono de la app (POST sign-token)
    GB->>GB: Verifica que quien hace clic es miembro de ese workspace
    GB->>GB: Firma el wsa con la clave<br/>(JWT de 5 minutos, aud=host de la app de extensión)
    GB->>FE: Abre app_home_url?wsa=<JWT>

    Note over FE: consumeHandoff()<br/>lee ?wsa=, hace POST al backend de la app de extensión
    FE->>BE: POST /session/exchange { wsa }
    BE->>BE: verifyWsa() → identity
    BE->>BE: Establece la sesión propia
    BE-->>FE: Devuelve la sesión (Set-Cookie)

    Note over FE: history.replaceState elimina el ?wsa=

Reglas de cómo la plataforma genera la URL de redirección (el desarrollador no necesita implementarlo):

  • A partir de la información de registro, hace una coincidencia exacta de la app de extensión de destino según app_home_url (las URL no registradas se rechazan siempre en la firma, para prevenir la filtración de identidad).
  • Verifica que quien hace clic es realmente miembro de ese espacio de trabajo (workspace_id).
  • Si el app_home_url ya lleva una query, concatena el wsa con &; el valor del wsa ya está codificado en URL, y tras leerlo no necesitas decodificarlo manualmente.

Frontend: consumir el wsa de la página de aterrizaje

Usar el SDK (recomendado)

import { consumeHandoff } from '@gptbots/workspace-extension-sdk'; try { const identity = await consumeHandoff({ exchangeUrl: '/session/exchange', // tu propio endpoint de verificación en el backend // search: location.search, // por defecto lee location.search // paramName: 'wsa', // nombre del parámetro por defecto: wsa // strip: true, // por defecto elimina el wsa de la URL tras el éxito }); bootYourApp(identity); } catch (e) { // no hay wsa (el usuario accedió directamente), o la verificación del backend falló showLoginOrError(e); }
                      
                      import { consumeHandoff } from '@gptbots/workspace-extension-sdk';

try {
  const identity = await consumeHandoff({
    exchangeUrl: '/session/exchange', // tu propio endpoint de verificación en el backend
    // search:   location.search,     // por defecto lee location.search
    // paramName: 'wsa',              // nombre del parámetro por defecto: wsa
    // strip:     true,              // por defecto elimina el wsa de la URL tras el éxito
  });
  bootYourApp(identity);
} catch (e) {
  // no hay wsa (el usuario accedió directamente), o la verificación del backend falló
  showLoginOrError(e);
}

                    
Este bloque de código en una ventana flotante

consumeHandoff hace cuatro cosas: ① lee el ?wsa= ② hace POST { wsa } a exchangeUrltras el éxito elimina el ?wsa= con history.replaceState ④ devuelve el identity que retornó el backend.

Momento de la eliminación: el token solo se elimina de la URL tras un intercambio exitoso, de modo que un fallo transitorio puntual pueda reintentarse actualizando la página. Es un token de un solo uso válido durante 5 minutos; si el desarrollador desea eliminarlo de inmediato también en caso de fallo, puede invocar manualmente stripHandoffToken() después del catch.

No crear sesión, solo leer la identidad (receive-only)

import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk'; const token = readHandoffToken(); // función pura, devuelve la cadena JWT original o null if (token) { // aun así se recomienda enviar el token a tu backend y ejecutar verifyWsa antes de confiar en su contenido (no analices el JWT en el frontend como identidad de confianza) await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) }); } stripHandoffToken(); // pase lo que pase, elimina el wsa de la URL
                      
                      import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk';

const token = readHandoffToken();   // función pura, devuelve la cadena JWT original o null
if (token) {
  // aun así se recomienda enviar el token a tu backend y ejecutar verifyWsa antes de confiar en su contenido (no analices el JWT en el frontend como identidad de confianza)
  await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) });
}
stripHandoffToken();                // pase lo que pase, elimina el wsa de la URL

                    
Este bloque de código en una ventana flotante

⚠️ No decodifiques el JWT en el frontend y lo tomes como identidad de confianza. La firma del JWT solo puede verificarse con la clave, y la clave solo está en el backend. El análisis en el frontend solo sirve como marcador de posición «no seguro» para mostrar datos; cualquier decisión de autorización debe basarse en el resultado de verifyWsa del backend.


Backend: verificar el wsa

3.1 Usar el SDK (recomendado)

import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify'; app.post('/session/exchange', (req, res) => { try { const identity = verifyWsa(req.body.wsa, { secret: process.env.EXTENSION_APP_SECRET, // clave per-app de Tier2 o clave compartida de Tier1 audience: 'app.example.com', // host de la app de extensión, debe ser igual al aud // issuer: 'gptbots-workspace', // por defecto // leewaySeconds: 30, // tolerancia de deriva del reloj, por defecto 30s // algorithms: ['HS256'], // por defecto }); // Aislamiento multitenant: asigna la petición al nombre de identity.workspaceId const sid = createSession(identity); res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' }); res.json(identity); } catch (e) { const code = e instanceof WsaVerificationError ? e.code : 'Error'; res.status(401).json({ code }); } });
                      
                      import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';

app.post('/session/exchange', (req, res) => {
  try {
    const identity = verifyWsa(req.body.wsa, {
      secret: process.env.EXTENSION_APP_SECRET, // clave per-app de Tier2 o clave compartida de Tier1
      audience: 'app.example.com',              // host de la app de extensión, debe ser igual al aud
      // issuer: 'gptbots-workspace',          // por defecto
      // leewaySeconds: 30,                    // tolerancia de deriva del reloj, por defecto 30s
      // algorithms: ['HS256'],                // por defecto
    });

    // Aislamiento multitenant: asigna la petición al nombre de identity.workspaceId
    const sid = createSession(identity);
    res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
    res.json(identity);
  } catch (e) {
    const code = e instanceof WsaVerificationError ? e.code : 'Error';
    res.status(401).json({ code });
  }
});

                    
Este bloque de código en una ventana flotante

Sin usar el SDK

El wsa es un JWT HS256 estándar, y cualquier librería JWT de cualquier lenguaje puede verificarlo. Los ejemplos de Java / Node / Python están en 05-Verificación de token y seguridad §3. Uses o no el SDK, los cuatro elementos firma / iss / aud / exp deben verificarse siempre.

Registrar la app de extensión (obtener la clave)

El OWNER/ADMIN del espacio de trabajo: Espacio de trabajo → Gestión del espacio → Apps de extensión → Añadir, y completa:

Campo Descripción
Nombre de la app Nombre visible, se recomienda ≤ 12 caracteres para evitar truncamientos
Icono de la app Icono cuadrado, se recomienda ≥ 128×128
URL de entrada de la app Tu app_home_url, que actúa como clave única; al firmar se compara de forma estricta por la cadena completa
Modo de autenticación Selecciona workspace_account (requiere transmitir la identidad)

Tras enviar, el sistema muestra el App Secret en texto plano una única vez; cópialo y guárdalo de inmediato (una vez cerrado, solo puedes rotarlo).

Lista de comprobación de la página de aterrizaje

  • Tras recibir el ?wsa=, primero haz POST al servicio de backend de la app de extensión para verificarlo, y luego confía en su contenido
  • Si la verificación es correcta, elimina el wsa de inmediato con history.replaceState (consumeHandoff ya lo hace por defecto)
  • Canjéalo por la sesión propia, y las peticiones XHR/fetch/img posteriores ya no transmitirán el wsa
  • Gestiona la rama de «el usuario accede directamente, sin wsa» (guíalo al inicio de sesión o al modo anónimo)
  • Ante un fallo de verificación, ofrece un mensaje legible según WsaVerificationError.code

Detalles de seguridad y verificación en varios lenguajes: Verificación de token y seguridad. ¿Quieres cambiarlo por un «botón de inicio de sesión dentro de la app»?: Inicio de sesión pull.