logo
開發者文件
搜尋
推式接入(身份握手 / Push Handoff)

推式接入(身份握手 / Push Handoff)

場景:使用者在 工作空間 → 擴充 頁點擊開發者的擴充應用程式,平台把使用者身份以 ?wsa=<JWT> 推給目標擴充應用程式落地頁。開發者在落地頁消費並在後端校驗後、換成自己的工作階段。

本模式下開發者不需要呼叫平台的簽章端點——那是工作空間前端在使用者點擊時主動呼叫的。開發者只負責「收下並校驗」。

端到端時序

loading...
sequenceDiagram
    autonumber
    actor User as 使用者存取擴充應用程式
    participant GB as GPTBots 平台
    participant FE as 擴充應用程式-落地頁
    participant BE as 擴充應用程式-後端

    User->>GB: 點擊應用程式圖示(POST sign-token)
    GB->>GB: 校驗點擊人是該 workspace 成員
    GB->>GB: 用金鑰簽 wsa<br/>(5 分鐘 JWT,aud=擴充應用程式 host)
    GB->>FE: 打開 app_home_url?wsa=<JWT>

    Note over FE: consumeHandoff()<br/>讀 ?wsa=,POST 至擴充應用程式後端
    FE->>BE: POST /session/exchange { wsa }
    BE->>BE: verifyWsa() → identity
    BE->>BE: 建立自有 session
    BE-->>FE: 回傳 session(Set-Cookie)

    Note over FE: history.replaceState 抹掉 ?wsa=

平台產生跳轉 URL 的規則(開發者無需實作):

  • 從註冊資訊裡按 app_home_url 精確比對目標擴充應用程式(不在冊的 URL 一律拒簽,防身份外洩)。
  • 校驗點擊人確實是該工作空間(workspace_id)成員。
  • app_home_url 已帶 query,則用 & 拼接 wsawsa 值已 URL 編碼,你讀取後無需手動解碼。

前端:消費落地頁的 wsa

使用 SDK(推薦)

import { consumeHandoff } from '@gptbots/workspace-extension-sdk'; try { const identity = await consumeHandoff({ exchangeUrl: '/session/exchange', // 你自己的後端校驗介面 // search: location.search, // 預設讀 location.search // paramName: 'wsa', // 預設參數名 wsa // strip: true, // 預設成功後抹除 URL 中的 wsa }); bootYourApp(identity); } catch (e) { // 沒有 wsa(使用者直接存取)、或後端校驗失敗 showLoginOrError(e); }
                      
                      import { consumeHandoff } from '@gptbots/workspace-extension-sdk';

try {
  const identity = await consumeHandoff({
    exchangeUrl: '/session/exchange', // 你自己的後端校驗介面
    // search:   location.search,     // 預設讀 location.search
    // paramName: 'wsa',              // 預設參數名 wsa
    // strip:     true,              // 預設成功後抹除 URL 中的 wsa
  });
  bootYourApp(identity);
} catch (e) {
  // 沒有 wsa(使用者直接存取)、或後端校驗失敗
  showLoginOrError(e);
}

                    
此代碼塊在浮窗中顯示

consumeHandoff 做四件事:① 讀 ?wsa= ② POST { wsa }exchangeUrl成功後 history.replaceState 抹掉 ?wsa= ④ 回傳後端回傳的 identity

抹除時機:令牌只在成功交換後才從 URL 抹除,以便一次瞬時失敗可以重新整理重試。這是一枚 5 分鐘一次性令牌;若開發者希望失敗也立即抹除,可 catch 後手動呼叫 stripHandoffToken()

不建工作階段,只讀身份(receive-only)

import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk'; const token = readHandoffToken(); // 純函數,回傳原始 JWT 字串或 null if (token) { // 仍建議把 token 發給你後端 verifyWsa 後再信任其內容(不要在前端解析 JWT 當可信身份) await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) }); } stripHandoffToken(); // 無論如何抹掉 URL 裡的 wsa
                      
                      import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk';

const token = readHandoffToken();   // 純函數,回傳原始 JWT 字串或 null
if (token) {
  // 仍建議把 token 發給你後端 verifyWsa 後再信任其內容(不要在前端解析 JWT 當可信身份)
  await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) });
}
stripHandoffToken();                // 無論如何抹掉 URL 裡的 wsa

                    
此代碼塊在浮窗中顯示

⚠️ 不要在前端解開 JWT 就當可信身份。JWT 的簽章只有用金鑰才能校驗,而金鑰只在後端。前端解析僅能用於「非安全」的顯示佔位,任何授權判斷都必須以後端 verifyWsa 的結果為準。


後端:校驗 wsa

3.1 用 SDK(推薦)

import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify'; app.post('/session/exchange', (req, res) => { try { const identity = verifyWsa(req.body.wsa, { secret: process.env.EXTENSION_APP_SECRET, // Tier2 per-app 金鑰 或 Tier1 共享金鑰 audience: 'app.example.com', // 擴充應用程式 host,必須等於 aud // issuer: 'gptbots-workspace', // 預設 // leewaySeconds: 30, // 時鐘漂移容忍,預設 30s // algorithms: ['HS256'], // 預設 }); // 多租戶隔離:把請求歸到 identity.workspaceId 名下 const sid = createSession(identity); res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' }); res.json(identity); } catch (e) { const code = e instanceof WsaVerificationError ? e.code : 'Error'; res.status(401).json({ code }); } });
                      
                      import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';

app.post('/session/exchange', (req, res) => {
  try {
    const identity = verifyWsa(req.body.wsa, {
      secret: process.env.EXTENSION_APP_SECRET, // Tier2 per-app 金鑰 或 Tier1 共享金鑰
      audience: 'app.example.com',              // 擴充應用程式 host,必須等於 aud
      // issuer: 'gptbots-workspace',          // 預設
      // leewaySeconds: 30,                    // 時鐘漂移容忍,預設 30s
      // algorithms: ['HS256'],                // 預設
    });

    // 多租戶隔離:把請求歸到 identity.workspaceId 名下
    const sid = createSession(identity);
    res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
    res.json(identity);
  } catch (e) {
    const code = e instanceof WsaVerificationError ? e.code : 'Error';
    res.status(401).json({ code });
  }
});

                    
此代碼塊在浮窗中顯示

不使用 SDK

wsa 是標準 HS256 JWT,任意語言的 JWT 函式庫都能校驗。Java / Node / Python 範例見 05-令牌校驗與安全 §3無論用不用 SDK,簽章 / iss / aud / exp 四項都必須校驗。

註冊擴充應用程式(取得金鑰)

工作空間的 OWNER/ADMIN:工作空間 → 空間管理 → 擴充應用程式 → 新增,填寫:

欄位 說明
應用程式名稱 顯示名,建議 ≤ 12 個漢字避免截斷
應用程式圖示 方形圖示,建議 ≥ 128×128
應用程式入口 URL 你的 app_home_url,作為唯一鍵,簽章時按完整字串嚴格比對
驗證模式 workspace_account(需要傳遞身份)

提交後一次性明文顯示 App Secret,立即複製儲存(關閉後僅能輪換)。

落地頁 checklist

  • 收到 ?wsa=先 POST 至擴充應用程式後端服務進行校驗,再信任內容
  • 校驗通過立刻 history.replaceState 抹除 wsaconsumeHandoff 預設已做)
  • 換成自有工作階段,後續 XHR/fetch/img 不再透傳 wsa
  • 處理「使用者直接存取、無 wsa」的分支(引導登入或匿名)
  • 校驗失敗按 WsaVerificationError.code 給出可讀提示

安全細節與多語言校驗:令牌校驗與安全。想改成「應用程式內登入按鈕」:拉式登入