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. 端到端時序
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-參考。
