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. エンドツーエンドのシーケンス
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": 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 へ遷移
フロントエンド:コールバックランディングページ
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) });
}
});
返される
wsaはプッシュと 同一の JWT 契約で、verifyWsaの使い方は一字一句同じです。
セキュリティ制約(必読)
redirect_uriは登録アプリと同一ドメインでなければならない:その scheme + host はclient_idと厳密に等しくなければなりません。/authorizeはブラウザ遷移エンドポイントで JSON エラーを返せないため、redirect_uriが欠落 / 非 http(s) / 異なるドメインの場合、未検証のアドレスへは決して遷移せず、組織選択ページに戻って?error=invalid_requestを付けます。これはオープンリダイレクト / トークン漏洩を防ぐ要です。- PKCE 強制:
code_challenge_method=S256(大文字小文字を区別)のみを受け付け、code_challenge = base64url(sha256(code_verifier))は padding なしです。現バージョンではclient_secretを使わず、PKCE によって「セッション開始」と「セッション交換」をバインドします。 - 使い捨ての
code:Redis に保存、TTL 10 分、交換時にアトミックに消費され、リプレイできません。リプレイ/期限切れは403209 invalid_grant、code_verifierの不一致は403210 invalid_verifierを返します。 state(CSRF):SDK はsessionStorageにstateとcode_verifierを保存し、コールバック時にstateの一致を検証してから続行します。- 組織の範囲:認可時にアカウントが選択したワークスペースのメンバーであること、かつそのアプリがその組織で利用可能であることを検証します(組織専用の拡張はその所属組織にのみ表示されます)。組織管理者があるアプリを 停止 した場合、たとえプラットフォームの辞書にまだ残っていても、その組織向けには発行されません。
/token 交換段階のエラーコード
/authorizeの構造的エラー(client_id/redirect_uriの欠落や異なるドメイン、code_challengeの不正、code_challenge_methodがS256でない)は JSON を返さず、302 で組織選択ページに戻って?error=invalid_requestを付けます。下表は/token段階の JSON エラーコードのみを列挙します。
| code | 意味 | 発生条件 |
|---|---|---|
403209 |
Invalid grant | code の欠落、期限切れ、または使用済み(リプレイ) |
403210 |
Invalid verifier | PKCE code_verifier が code_challenge と不一致 |
コールバック URL 上の ?error= の値:invalid_request(構造的エラー)/ access_denied(非メンバー、またはアプリがその組織で利用不可)/ server_error(予期しない失敗)。SDK の completeWorkspaceLogin はこれを WorkspaceLoginError('AuthorizeError') としてスローします。
次のステップ:プッシュ・プルにかかわらず、検証とセキュリティは 05-トークン検証とセキュリティ を読んでください。完全な API は 06-SDK APIリファレンス を参照してください。
