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-参考。
