logo
Development
検索
04 · プルログイン(Login with GPTBots Workspace / M-Auth)

04 · プルログイン(Login with GPTBots Workspace / M-Auth)

開発者の拡張アプリが独立したサイトで、「Login with GPTBots Workspace」 ボタンを提供したい場合。ユーザーがクリックすると GPTBots にログインし、ワークスペースを選択し、ログイン状態とログインユーザーの ID 情報を持ち帰ります。

OAuth2 認可コード + PKCE を採用します:ブラウザは使い捨ての code を受け取るだけで、本物の wsaあなたのバックエンドcode + PKCE code_verifier で交換します。wsaブラウザの URL / 履歴 / Referer に決して入りません——プッシュより安全です。

wsa を取得した後の検証は、プッシュと まったく同じ です(05 を参照)。本文では「どうやって wsa を取得するか」だけを説明します。

1. エンドツーエンドのシーケンス

loading...
sequenceDiagram
    participant WS as ワークスペース「拡張」ページ
    participant GB as GPTBots プラットフォーム
    participant FE as 拡張アプリランディングページ
    participant BE as 拡張アプリバックエンド

    Note over WS: ユーザーがアプリアイコンをクリック
    WS->>GB: POST sign-token
    Note over GB: クリックした人がその workspace メンバーか検証<br/>シークレットで wsa に署名(5 分間の JWT、aud=あなたの host)
    GB-->>WS: wsa を返す
    WS->>FE: app_home_url?wsa=JWT を開く
    Note over FE: consumeHandoff()、?wsa= を読み取る
    FE->>BE: POST /session/exchange(wsa を携える)
    Note over BE: verifyWsa() → identity<br/>自前の session を確立
    BE-->>FE: identity
    Note over FE: history.replaceState で ?wsa= を消去

プラットフォームエンドポイント

用途 メソッド パス
認可入口(ブラウザ遷移) GET /api/console/account/extension-app/authorize
トークン交換(バックエンド → バックエンド) POST /api/console/account/extension-app/token

/authorize パラメータ

パラメータ 必須 説明
client_id はい 拡張アプリの入口 URL(app home URL)
redirect_uri はい コールバックアドレス。その host は client_id と同一ドメイン(同一 scheme + host)でなければならない
state はい CSRF 用のランダム文字列。コールバック時にそのまま持ち帰って検証
code_challenge はい base64url(sha256(code_verifier))、padding なし
code_challenge_method はい S256 のみ受け付ける(大文字小文字を区別)
workspace_id いいえ ワークスペースを事前選択し、組織選択ページをスキップ

/authorize はログイン状態に応じて次へ 302 します:GPTBots ログインページ(未ログイン)/ 組織選択ページ(ログイン済み・組織未選択)/ redirect_uri?code&state(組織選択済み)。

/token のリクエストとレスポンス

リクエストボディ(バックエンドが呼び出し):

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

                    
このコードブロックをポップアップで表示

成功レスポンス:

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

                    
このコードブロックをポップアップで表示

SDK 接続

フロントエンド:ログインの開始(ボタンクリック)

import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk'; // 「Login with GPTBots Workspace」をクリックしたとき: await startWorkspaceLogin({ authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize', clientId: 'https://app.example.com/land', // = あなたが登録した app home URL redirectUri: 'https://app.example.com/callback', // host は clientId と同一ドメインでなければならない // workspaceId: 'p-xxx', // 任意:ワークスペースを事前選択し、組織選択ページをスキップ // state: '...', // 任意:デフォルトで 16 バイトのランダム CSRF state を自動生成 }); // SDK が自動で行うこと:PKCE(verifier→challenge) の生成、verifier+state を sessionStorage に保存、 // authorizeUrl が絶対 URL であることの検証、セキュアコンテキスト(HTTPS/localhost)の要求、そして /authorize へ遷移
                      
                      import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk';

// 「Login with GPTBots Workspace」をクリックしたとき:
await startWorkspaceLogin({
  authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize',
  clientId: 'https://app.example.com/land',      // = あなたが登録した app home URL
  redirectUri: 'https://app.example.com/callback', // host は clientId と同一ドメインでなければならない
  // workspaceId: 'p-xxx',   // 任意:ワークスペースを事前選択し、組織選択ページをスキップ
  // state: '...',           // 任意:デフォルトで 16 バイトのランダム CSRF state を自動生成
});
// SDK が自動で行うこと:PKCE(verifier→challenge) の生成、verifier+state を sessionStorage に保存、
// authorizeUrl が絶対 URL であることの検証、セキュアコンテキスト(HTTPS/localhost)の要求、そして /authorize へ遷移

                    
このコードブロックをポップアップで表示

フロントエンド:コールバックランディングページ

loading...
sequenceDiagram
    participant FE as 拡張アプリフロントエンド
    participant GB as GPTBots
    participant BE as 拡張アプリバックエンド

    Note over FE: startWorkspaceLogin()<br/>PKCE(verifier→challenge) を生成<br/>sessionStorage に保存、302 遷移
    FE->>GB: GET /authorize
    alt 未ログイン
        GB-->>FE: 302 で GPTBots ログインページへ(既存ログインを再利用)
    else ログイン済み・組織未選択
        GB-->>FE: 302 組織選択ページ
    else ログイン済み・組織選択済み
        Note over GB: 使い捨て code を発行(Redis、<br/>account/project/app/redirect/challenge にバインド)
        GB-->>FE: 302 redirect_uri?code&state
    end
    Note over FE: completeWorkspaceLogin()<br/>state(CSRF) を検証、verifier を取り出す
    FE->>BE: POST {code, codeVerifier}
    Note over BE: exchangeWorkspaceCode()
    BE->>GB: POST /token {code, verifier}
    Note over GB: code(使い捨て GETDEL) + PKCE を検証<br/>wsa に署名
    GB-->>BE: wsa を返す
    Note over BE: verifyWsa(wsa) → 自前の session を確立
    BE-->>FE: identity

バックエンド:wsa を交換して検証する

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。プラットフォームの遅い応答であなたのリクエストがハングするのを防ぐ;0 を渡すと無効 }); 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。プラットフォームの遅い応答であなたのリクエストがハングするのを防ぐ;0 を渡すと無効
    });
    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) });
  }
});

                    
このコードブロックをポップアップで表示

返される wsa はプッシュと 同一の JWT 契約で、verifyWsa の使い方は一字一句同じです。

セキュリティ制約(必読)

  1. redirect_uri は登録アプリと同一ドメインでなければならない:その scheme + host は client_id と厳密に等しくなければなりません。/authorize はブラウザ遷移エンドポイントで JSON エラーを返せないため、redirect_uri が欠落 / 非 http(s) / 異なるドメインの場合、未検証のアドレスへは決して遷移せず、組織選択ページに戻って ?error=invalid_request を付けます。これはオープンリダイレクト / トークン漏洩を防ぐ要です。
  2. PKCE 強制code_challenge_method=S256(大文字小文字を区別)のみを受け付け、code_challenge = base64url(sha256(code_verifier)) は padding なしです。現バージョンでは client_secret を使わず、PKCE によって「セッション開始」と「セッション交換」をバインドします。
  3. 使い捨ての code:Redis に保存、TTL 10 分、交換時にアトミックに消費され、リプレイできません。リプレイ/期限切れは 403209 invalid_grantcode_verifier の不一致は 403210 invalid_verifier を返します。
  4. state(CSRF):SDK は sessionStoragestatecode_verifier を保存し、コールバック時に state の一致を検証してから続行します。
  5. 組織の範囲:認可時にアカウントが選択したワークスペースのメンバーであること、かつそのアプリがその組織で利用可能であることを検証します(組織専用の拡張はその所属組織にのみ表示されます)。組織管理者があるアプリを 停止 した場合、たとえプラットフォームの辞書にまだ残っていても、その組織向けには発行されません。

/token 交換段階のエラーコード

/authorize の構造的エラー(client_id/redirect_uri の欠落や異なるドメイン、code_challenge の不正、code_challenge_methodS256 でない)は JSON を返さず、302 で組織選択ページに戻って ?error=invalid_request を付けます。下表は /token 段階の JSON エラーコードのみを列挙します。

code 意味 発生条件
403209 Invalid grant code の欠落、期限切れ、または使用済み(リプレイ)
403210 Invalid verifier PKCE code_verifiercode_challenge と不一致

コールバック URL 上の ?error= の値:invalid_request(構造的エラー)/ access_denied(非メンバー、またはアプリがその組織で利用不可)/ server_error(予期しない失敗)。SDK の completeWorkspaceLogin はこれを WorkspaceLoginError('AuthorizeError') としてスローします。

次のステップ:プッシュ・プルにかかわらず、検証とセキュリティは 05-トークン検証とセキュリティ を読んでください。完全な API は 06-SDK APIリファレンス を参照してください。