logo
Entwicklung
Suchen
04 · Pull-Login (Login with GPTBots Workspace / M-Auth)

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

Wenn die Erweiterungs-App der Entwickler:innen eine eigenständige Website ist und einen „Login with GPTBots Workspace"-Button anbieten möchte. Nach dem Klick gehen Nutzer:innen zur GPTBots-Anmeldung, wählen einen Workspace aus und werden dann mit ihrem Anmeldezustand und den Identitätsinformationen der angemeldeten Nutzer:innen zurückgebracht.

Es kommt OAuth2-Autorisierungscode + PKCE zum Einsatz: Der Browser erhält nur einen einmaligen code; das eigentliche wsa wird von Ihrem Backend mit code + PKCE-code_verifier eingetauscht, und wsa gelangt nie in Browser-URL / Verlauf / Referer – sicherer als Push.

Die Prüfung nach dem Erhalt des wsa ist exakt dieselbe wie bei Push (siehe 05). Dieser Artikel behandelt nur „wie man das wsa erhält".

1. End-to-End-Ablauf

loading...
sequenceDiagram
    participant WS as Workspace-Seite „Erweiterungen"
    participant GB as GPTBots-Plattform
    participant FE as Erweiterungs-App-Landingpage
    participant BE as Erweiterungs-App-Backend

    Note over WS: Nutzer:in klickt App-Symbol
    WS->>GB: POST sign-token
    Note over GB: Prüft, ob Klickende:r Mitglied dieses workspace ist<br/>Signiert wsa mit dem Schlüssel (5-Minuten-JWT, aud=Ihr Host)
    GB-->>WS: Gibt wsa zurück
    WS->>FE: Öffnet app_home_url?wsa=JWT
    Note over FE: consumeHandoff(), liest ?wsa=
    FE->>BE: POST /session/exchange (mit wsa)
    Note over BE: verifyWsa() → identity<br/>Baut eigene Session auf
    BE-->>FE: identity
    Note over FE: history.replaceState entfernt ?wsa=

Plattform-Endpunkte

Zweck Methode Pfad
Autorisierungs-Einstieg (Browser-Navigation) GET /api/console/account/extension-app/authorize
Token-Austausch (Backend → Backend) POST /api/console/account/extension-app/token

/authorize-Parameter

Parameter Pflicht Beschreibung
client_id Ja Einstiegs-URL der Erweiterungs-App (app home URL)
redirect_uri Ja Callback-Adresse, deren Host mit client_id dieselbe Domain haben muss (gleiches scheme + host)
state Ja Zufälliger CSRF-String, wird beim Callback unverändert zur Prüfung zurückgegeben
code_challenge Ja base64url(sha256(code_verifier)), ohne Padding
code_challenge_method Ja Akzeptiert nur S256 (Groß-/Kleinschreibung beachten)
workspace_id Nein Vorausgewählter Workspace, überspringt die Organisationsauswahlseite

/authorize leitet je nach Anmeldezustand per 302 weiter zu: GPTBots-Anmeldeseite (nicht angemeldet) / Organisationsauswahlseite (angemeldet, aber keine Organisation gewählt) / redirect_uri?code&state (Organisation gewählt).

/token-Anfrage und -Antwort

Request-Body (Backend-Aufruf):

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

                    
Dieser Codeblock im schwebenden Fenster

Erfolgsantwort:

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

                    
Dieser Codeblock im schwebenden Fenster

SDK-Anbindung

Frontend: Anmeldung starten (Button-Klick)

import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk'; // Beim Klick auf „Login with GPTBots Workspace": await startWorkspaceLogin({ authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize', clientId: 'https://app.example.com/land', // = Ihre registrierte app home URL redirectUri: 'https://app.example.com/callback', // host muss dieselbe Domain wie clientId haben // workspaceId: 'p-xxx', // optional: vorausgewählter Workspace, überspringt die Organisationsauswahlseite // state: '...', // optional: standardmäßig automatisch generierter 16-Byte-Zufalls-CSRF-state }); // Das SDK erledigt automatisch: PKCE generieren (verifier→challenge), verifier+state in sessionStorage speichern, // prüfen, dass authorizeUrl eine absolute URL ist, einen sicheren Kontext verlangen (HTTPS/localhost) und dann zu /authorize weiterleiten
                      
                      import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk';

// Beim Klick auf „Login with GPTBots Workspace":
await startWorkspaceLogin({
  authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize',
  clientId: 'https://app.example.com/land',      // = Ihre registrierte app home URL
  redirectUri: 'https://app.example.com/callback', // host muss dieselbe Domain wie clientId haben
  // workspaceId: 'p-xxx',   // optional: vorausgewählter Workspace, überspringt die Organisationsauswahlseite
  // state: '...',           // optional: standardmäßig automatisch generierter 16-Byte-Zufalls-CSRF-state
});
// Das SDK erledigt automatisch: PKCE generieren (verifier→challenge), verifier+state in sessionStorage speichern,
// prüfen, dass authorizeUrl eine absolute URL ist, einen sicheren Kontext verlangen (HTTPS/localhost) und dann zu /authorize weiterleiten

                    
Dieser Codeblock im schwebenden Fenster

Frontend: Callback-Landingpage

loading...
sequenceDiagram
    participant FE as Erweiterungs-App-Frontend
    participant GB as GPTBots
    participant BE as Erweiterungs-App-Backend

    Note over FE: startWorkspaceLogin()<br/>generiert PKCE(verifier→challenge)<br/>speichert in sessionStorage, 302-Weiterleitung
    FE->>GB: GET /authorize
    alt Nicht angemeldet
        GB-->>FE: 302 zur GPTBots-Anmeldeseite (bestehende Anmeldung wiederverwenden)
    else Angemeldet, keine Organisation gewählt
        GB-->>FE: 302 Organisationsauswahlseite
    else Angemeldet, Organisation gewählt
        Note over GB: Stellt einmaligen code aus (Redis,<br/>gebunden an account/project/app/redirect/challenge)
        GB-->>FE: 302 redirect_uri?code&state
    end
    Note over FE: completeWorkspaceLogin()<br/>prüft state(CSRF), holt verifier
    FE->>BE: POST {code, codeVerifier}
    Note over BE: exchangeWorkspaceCode()
    BE->>GB: POST /token {code, verifier}
    Note over GB: Prüft code(einmalig GETDEL) + PKCE<br/>signiert wsa
    GB-->>BE: Gibt wsa zurück
    Note over BE: verifyWsa(wsa) → baut eigene Session auf
    BE-->>FE: identity

Backend: wsa eintauschen und prüfen

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, // Standard 10s, verhindert, dass eine langsame Plattformantwort Ihre Anfrage blockiert; 0 zum Deaktivieren übergeben }); 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,   // Standard 10s, verhindert, dass eine langsame Plattformantwort Ihre Anfrage blockiert; 0 zum Deaktivieren übergeben
    });
    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) });
  }
});

                    
Dieser Codeblock im schwebenden Fenster

Das zurückgegebene wsa folgt demselben JWT-Vertrag wie bei Push; die Verwendung von verifyWsa ist wortwörtlich identisch.

Sicherheitseinschränkungen (Pflichtlektüre)

  1. redirect_uri muss dieselbe Domain wie die registrierte App haben: scheme + host müssen exakt gleich client_id sein. /authorize ist ein Browser-Navigationsendpunkt und kann keinen JSON-Fehler zurückgeben; wenn redirect_uri fehlt / nicht http(s) ist / eine andere Domain hat, wird niemals zu einer ungeprüften Adresse gesprungen, sondern zur Organisationsauswahlseite mit ?error=invalid_request zurückgekehrt. Dies ist entscheidend, um Open Redirect / Token-Lecks zu verhindern.
  2. PKCE verpflichtend: Akzeptiert nur code_challenge_method=S256 (Groß-/Kleinschreibung beachten), code_challenge = base64url(sha256(code_verifier)) ohne Padding. Die aktuelle Version verwendet kein client_secret; PKCE bindet „Session-Start" und „Session-Austausch" aneinander.
  3. Einmaliger code: In Redis gespeichert, TTL 10 Minuten, beim Austausch atomar konsumiert, nicht wiederholbar. Wiederholung/Ablauf meldet 403209 invalid_grant, nicht übereinstimmender code_verifier meldet 403210 invalid_verifier.
  4. state (CSRF): Das SDK speichert state und code_verifier im sessionStorage, beim Callback wird geprüft, ob state übereinstimmt, bevor fortgefahren wird.
  5. Organisationsbereich: Bei der Autorisierung wird geprüft, ob das Konto Mitglied des gewählten Workspace ist und ob die App in dieser Organisation verfügbar ist (organisationseigene Erweiterungen sind nur für die zugehörige Organisation sichtbar); hat ein Organisationsadministrator eine App deaktiviert, wird für diese Organisation nichts signiert, selbst wenn sie noch im Plattformverzeichnis steht.

Fehlercodes der /token-Austauschphase

Strukturelle Fehler von /authorize (client_id/redirect_uri fehlt oder andere Domain, code_challenge ungültig, code_challenge_method nicht S256) geben kein JSON zurück, sondern leiten per 302 zur Organisationsauswahlseite mit ?error=invalid_request zurück. Die folgende Tabelle listet nur die JSON-Fehlercodes der /token-Phase auf.

code Bedeutung Auslösebedingung
403209 Invalid grant code fehlt, ist abgelaufen oder wurde bereits verwendet (Wiederholung)
403210 Invalid verifier PKCE-code_verifier stimmt nicht mit code_challenge überein

Werte von ?error= in der Callback-URL: invalid_request (struktureller Fehler) / access_denied (kein Mitglied oder App in dieser Organisation nicht verfügbar) / server_error (unerwarteter Fehler). Das completeWorkspaceLogin des SDK wirft es als WorkspaceLoginError('AuthorizeError').

Nächster Schritt: Ob Push oder Pull – lesen Sie zu Prüfung und Sicherheit 05-Token-Prüfung und Sicherheit; die vollständige API finden Sie unter 06-SDK-API-Referenz.