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
wsaobtenu est exactement identique au mode push (voir 05). Cet article ne traite que de « comment obtenir lewsa».
1. Séquence de bout en bout
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": "…" }
Réponse en cas de succès :
{
"code": 0,
"msg": "OK",
"data": { "wsa": "eyJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 300 }
}
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
Frontend : page d'atterrissage du callback
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) });
}
});
Le
wsaretourné suit exactement le même contrat JWT que le mode push, et l'usage deverifyWsaest identique au mot près.
Contraintes de sécurité (lecture obligatoire)
- Le
redirect_uridoit être du même domaine que l'application enregistrée : son scheme + host doivent être exactement égaux à ceux declient_id./authorizeest un endpoint de navigation du navigateur, il ne peut pas retourner une erreur JSON ; lorsque leredirect_uriest 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. - 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 declient_secret; PKCE relie la « session initiatrice » et la « session d'échange ». 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 renvoie403209 invalid_grant, et uncode_verifiernon concordant renvoie403210 invalid_verifier.state(CSRF) : le SDK stocke lestateet lecode_verifierdanssessionStorage, et ne continue que si lestatecorrespond lors du callback.- 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_urimanquant ou d'un domaine différent,code_challengeinvalide,code_challenge_methoddifférent deS256) 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.
