logo
Développement
Rechercher
04 · Connexion pull (Login with GPTBots Workspace / M-Auth)

04 · Connexion pull (Login with GPTBots Workspace / M-Auth)

Lorsque l'application d'extension du développeur est un site indépendant qui souhaite proposer un bouton « Login with GPTBots Workspace ». Après avoir cliqué, l'utilisateur se connecte à GPTBots, choisit un espace de travail, puis revient avec l'état de connexion et les informations d'identité de l'utilisateur connecté.

Utilise le code d'autorisation OAuth2 + PKCE : le navigateur n'obtient qu'un code à usage unique ; le véritable wsa est échangé par votre backend à l'aide du code + code_verifier PKCE, et le wsa n'entre jamais dans l'URL / l'historique / le Referer du navigateur — plus sûr que le mode push.

La vérification une fois le wsa obtenu est exactement identique au mode push (voir 05). Cet article ne traite que de « comment obtenir le wsa ».

1. Séquence de bout en bout

loading...
sequenceDiagram
    participant WS as Page « Extensions » de l'espace de travail
    participant GB as Plateforme GPTBots
    participant FE as Page d'atterrissage de l'application d'extension
    participant BE as Backend de l'application d'extension

    Note over WS: L'utilisateur clique sur l'icône de l'application
    WS->>GB: POST sign-token
    Note over GB: Vérifie que le cliqueur est membre de ce workspace<br/>Signe le wsa avec la clé (JWT 5 minutes, aud=votre host)
    GB-->>WS: Retourne le wsa
    WS->>FE: Ouvre app_home_url?wsa=JWT
    Note over FE: consumeHandoff(), lit le ?wsa=
    FE->>BE: POST /session/exchange (avec wsa)
    Note over BE: verifyWsa() → identity<br/>Établit sa propre session
    BE-->>FE: identity
    Note over FE: history.replaceState efface le ?wsa=

Endpoints de la plateforme

Usage Méthode Chemin
Entrée d'autorisation (navigation du navigateur) GET /api/console/account/extension-app/authorize
Échange de jeton (backend → backend) POST /api/console/account/extension-app/token

Paramètres de /authorize

Paramètre Obligatoire Description
client_id Oui URL d'accès de l'application d'extension (app home URL)
redirect_uri Oui Adresse de callback, dont le host doit être du même domaine que client_id (même scheme + host)
state Oui Chaîne aléatoire anti-CSRF, renvoyée telle quelle lors du callback pour vérification
code_challenge Oui base64url(sha256(code_verifier)), sans padding
code_challenge_method Oui Accepte uniquement S256 (sensible à la casse)
workspace_id Non Espace de travail présélectionné, saute la page de sélection d'organisation

/authorize redirige en 302, selon l'état de connexion, vers : la page de connexion GPTBots (non connecté) / la page de sélection d'organisation (connecté mais organisation non choisie) / redirect_uri?code&state (organisation choisie).

Requête et réponse de /token

Corps de requête (appelé par le backend) :

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

                    
Ce bloc de code dans la fenêtre flottante

Réponse en cas de succès :

{ "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 }
}

                    
Ce bloc de code dans la fenêtre flottante

Intégration du SDK

Frontend : lancer la connexion (au clic sur le bouton)

import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk'; // Au clic sur « Login with GPTBots Workspace » : await startWorkspaceLogin({ authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize', clientId: 'https://app.example.com/land', // = l'app home URL que vous avez enregistrée redirectUri: 'https://app.example.com/callback', // le host doit être du même domaine que clientId // workspaceId: 'p-xxx', // optionnel : espace de travail présélectionné, saute la page de sélection d'organisation // state: '...', // optionnel : par défaut, génère automatiquement un state CSRF aléatoire de 16 octets }); // Le SDK automatiquement : génère le PKCE (verifier→challenge), stocke verifier+state dans sessionStorage, // vérifie que authorizeUrl est une URL absolue, exige un contexte sécurisé (HTTPS/localhost), puis redirige vers /authorize
                      
                      import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk';

// Au clic sur « Login with GPTBots Workspace » :
await startWorkspaceLogin({
  authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize',
  clientId: 'https://app.example.com/land',      // = l'app home URL que vous avez enregistrée
  redirectUri: 'https://app.example.com/callback', // le host doit être du même domaine que clientId
  // workspaceId: 'p-xxx',   // optionnel : espace de travail présélectionné, saute la page de sélection d'organisation
  // state: '...',           // optionnel : par défaut, génère automatiquement un state CSRF aléatoire de 16 octets
});
// Le SDK automatiquement : génère le PKCE (verifier→challenge), stocke verifier+state dans sessionStorage,
// vérifie que authorizeUrl est une URL absolue, exige un contexte sécurisé (HTTPS/localhost), puis redirige vers /authorize

                    
Ce bloc de code dans la fenêtre flottante

Frontend : page d'atterrissage du callback

loading...
sequenceDiagram
    participant FE as Frontend de l'application d'extension
    participant GB as GPTBots
    participant BE as Backend de l'application d'extension

    Note over FE: startWorkspaceLogin()<br/>génère le PKCE (verifier→challenge)<br/>stocke dans sessionStorage, redirection 302
    FE->>GB: GET /authorize
    alt Non connecté
        GB-->>FE: 302 vers la page de connexion GPTBots (réutilise la connexion existante)
    else Connecté, organisation non choisie
        GB-->>FE: 302 page de sélection d'organisation
    else Connecté, organisation choisie
        Note over GB: Émet un code à usage unique (Redis,<br/>lié à account/project/app/redirect/challenge)
        GB-->>FE: 302 redirect_uri?code&state
    end
    Note over FE: completeWorkspaceLogin()<br/>vérifie le state (CSRF), récupère le verifier
    FE->>BE: POST {code, codeVerifier}
    Note over BE: exchangeWorkspaceCode()
    BE->>GB: POST /token {code, verifier}
    Note over GB: Vérifie le code (GETDEL à usage unique) + PKCE<br/>signe le wsa
    GB-->>BE: Retourne le wsa
    Note over BE: verifyWsa(wsa) → établit sa propre session
    BE-->>FE: identity

Backend : échanger le wsa et le vérifier

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, // 10s par défaut, évite qu'une réponse lente de la plateforme bloque votre requête ; passez 0 pour désactiver }); 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,   // 10s par défaut, évite qu'une réponse lente de la plateforme bloque votre requête ; passez 0 pour désactiver
    });
    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) });
  }
});

                    
Ce bloc de code dans la fenêtre flottante

Le wsa retourné suit exactement le même contrat JWT que le mode push, et l'usage de verifyWsa est identique au mot près.

Contraintes de sécurité (lecture obligatoire)

  1. Le redirect_uri doit être du même domaine que l'application enregistrée : son scheme + host doivent être exactement égaux à ceux de client_id. /authorize est un endpoint de navigation du navigateur, il ne peut pas retourner une erreur JSON ; lorsque le redirect_uri est manquant / non http(s) / d'un domaine différent, la plateforme ne redirige jamais vers une adresse non vérifiée, mais revient à la page de sélection d'organisation avec ?error=invalid_request. C'est la clé pour éviter les redirections ouvertes / fuites de jeton.
  2. PKCE obligatoire : accepte uniquement code_challenge_method=S256 (sensible à la casse), code_challenge = base64url(sha256(code_verifier)) sans padding. La version actuelle n'utilise pas de client_secret ; PKCE relie la « session initiatrice » et la « session d'échange ».
  3. code à usage unique : stocké dans Redis, TTL de 10 minutes, consommé de façon atomique lors de l'échange, non rejouable. Un rejeu/une expiration renvoie 403209 invalid_grant, et un code_verifier non concordant renvoie 403210 invalid_verifier.
  4. state (CSRF) : le SDK stocke le state et le code_verifier dans sessionStorage, et ne continue que si le state correspond lors du callback.
  5. Portée organisationnelle : lors de l'autorisation, la plateforme vérifie que le compte est membre de l'espace de travail sélectionné et que cette application est disponible dans cette organisation (les extensions à usage propre d'une organisation ne sont visibles que pour l'organisation à laquelle elles appartiennent) ; une fois qu'un administrateur d'organisation a désactivé une application, aucune émission n'est faite pour cette organisation, même si elle figure encore dans le dictionnaire de la plateforme.

Codes d'erreur de la phase d'échange /token

Les erreurs structurelles de /authorize (client_id/redirect_uri manquant ou d'un domaine différent, code_challenge invalide, code_challenge_method différent de S256) ne retournent pas de JSON, mais redirigent en 302 vers la page de sélection d'organisation avec ?error=invalid_request. Le tableau ci-dessous ne liste que les codes d'erreur JSON de la phase /token.

code Signification Condition de déclenchement
403209 Invalid grant code manquant, déjà expiré ou déjà utilisé (rejeu)
403210 Invalid verifier Le code_verifier PKCE ne correspond pas au code_challenge

Valeurs possibles de ?error= sur l'URL de callback : invalid_request (erreur structurelle) / access_denied (non-membre ou application non disponible dans cette organisation) / server_error (échec inattendu). Le completeWorkspaceLogin du SDK lève cela sous forme de WorkspaceLoginError('AuthorizeError').

Étape suivante : que ce soit en push ou en pull, lisez 05-Vérification du jeton et sécurité pour la vérification et la sécurité ; pour l'API complète, voir 06-Référence de l'API SDK.