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