logo
開發者文件
搜尋
04 · 拉式登入(Login with GPTBots Workspace / M-Auth)

04 · 拉式登入(Login with GPTBots Workspace / M-Auth)

當開發者的擴充應用程式是個獨立站點,想提供一個 「Login with GPTBots Workspace」 按鈕。使用者點擊後去 GPTBots 登入、選擇工作空間,再帶回登入態和登入使用者身份資訊。

採用 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-參考