05 · 令牌校驗與安全
05 · 令牌校驗與安全
無論推式還是拉式,最終開發者都會拿到一枚 wsa(HS256 JWT)。這一篇是所有接入方的必讀項——校驗不嚴 = 身份可偽造。
強制校驗清單
拿到 wsa 後,在擴充應用程式的後端服務中依序校驗(verifyWsa 已內建全部):
- 簽章:用平台分發/註冊取得的金鑰以 HS256 校驗。失敗 → 拒絕。
exp:目前時間 ≤exp(含少量 leeway)。exp必須存在——沒有exp的令牌應直接拒絕(否則等於永不過期)。iss:必須等於gptbots-workspace。aud:必須等於目標擴充應用程式的 host。這是防止「令牌被偷去打另一個應用程式」的關鍵防線,不可省略。iat/nbf(如存在):不得在未來(超出 leeway),防止「簽發時間設在遙遠未來」的令牌長期可用。workspace_id分租:若擴充做了多工作空間隔離,把請求歸到該workspace_id下,禁止跨租存取。- 落地後立刻抹除 URL 裡的
wsa/code:history.replaceState,避免使用者複製 URL 時把令牌一起分享出去。 - 建立自有工作階段:換成擴充應用程式自己的 session/cookie,後續請求不要再依賴
wsa——它 5 分鐘就過期。
官方 SDK 的
verifyWsa強制要求exp存在、校驗iat/nbf未來漂移,並做常量時間簽章比較與演算法白名單(預設僅HS256,杜絕alg混淆攻擊)。用它就自動滿足 1–5。
推薦用 SDK 校驗
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
try {
const id = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
// id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
if (e instanceof WsaVerificationError) {
// e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
// | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
}
}
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
try {
const id = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
// id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
if (e instanceof WsaVerificationError) {
// e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
// | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
}
}
此代碼塊在浮窗中顯示
完整選項與錯誤碼見 SDK-API-參考。
不用 SDK 的校驗(多語言)
wsa 是標準 HS256 JWT,任意 JWT 函式庫都能校驗。務必顯式開啟 iss / aud 校驗並鎖定 algorithms=['HS256']。
Java(auth0 java-jwt)
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
.withIssuer("gptbots-workspace")
.withAudience("app.example.com")
.acceptLeeway(30) // 容忍 30s 時鐘漂移
.build();
DecodedJWT jwt = verifier.verify(wsaParam);
String userId = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role = jwt.getClaim("role").asString();
String username = jwt.getClaim("username").asString(); // 可能為 null
String email = jwt.getClaim("email").asString(); // 可能為 null
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
.withIssuer("gptbots-workspace")
.withAudience("app.example.com")
.acceptLeeway(30) // 容忍 30s 時鐘漂移
.build();
DecodedJWT jwt = verifier.verify(wsaParam);
String userId = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role = jwt.getClaim("role").asString();
String username = jwt.getClaim("username").asString(); // 可能為 null
String email = jwt.getClaim("email").asString(); // 可能為 null
此代碼塊在浮窗中顯示
Node.js(jsonwebtoken)
const jwt = require('jsonwebtoken');
const payload = jwt.verify(wsaParam, SHARED_SECRET, {
algorithms: ['HS256'],
issuer: 'gptbots-workspace',
audience: 'app.example.com',
clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;
const jwt = require('jsonwebtoken');
const payload = jwt.verify(wsaParam, SHARED_SECRET, {
algorithms: ['HS256'],
issuer: 'gptbots-workspace',
audience: 'app.example.com',
clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;
此代碼塊在浮窗中顯示
Python(PyJWT)
import jwt
payload = jwt.decode(
wsa_param,
SHARED_SECRET,
algorithms=["HS256"],
issuer="gptbots-workspace",
audience="app.example.com",
leeway=30,
)
user_id = payload["sub"]
workspace_id = payload["workspace_id"]
role = payload["role"]
import jwt
payload = jwt.decode(
wsa_param,
SHARED_SECRET,
algorithms=["HS256"],
issuer="gptbots-workspace",
audience="app.example.com",
leeway=30,
)
user_id = payload["sub"]
workspace_id = payload["workspace_id"]
role = payload["role"]
此代碼塊在浮窗中顯示
金鑰編碼提醒:HS256 直接把金鑰字串按 UTF-8 位元組作為 HMAC key。Tier 2 的 per-app 金鑰形如
wext_+64 hex,請原樣字串作為金鑰傳入(不要再做 hex 解碼),與平台簽章側保持一致。
安全要求(逐條落實)
- 金鑰即整個安全模型。目前 HS256 是對稱金鑰:一旦洩露,任何人都能偽造任意工作空間使用者身份打進接入擴充應用程式。只放後端(環境變數/KMS),絕不寫進前端程式碼、git 儲存庫、日誌、用戶端設定。
- JWT 在 URL 中的暴露面(僅推式):query string 會被瀏覽器歷史、Web 伺服器 access log、CDN 快取日誌、Referer 記錄。即使被記錄,5 分鐘內被拿到仍可利用。落地後立即
history.replaceState抹除(consumeHandoff預設已做)。拉式(M-Auth)天生不把wsa放進 URL,更安全。 - 不要把
wsa透傳到子資源。換成自有工作階段後,後續 XHR/fetch/img 一律不再帶原始wsa,否則它會出現在每個子資源的 Referer 裡。 - 時鐘同步。HS256 對
exp嚴格判定,伺服器時鐘需 NTP 同步;範例裡 30sleeway容忍小幅漂移,別擴大到分鐘級。 role僅作預設權限對映。它只反映使用者在該工作空間的角色,不代表在你應用程式內的權限;你的應用程式維護自己的權限模型。- 多租戶隔離。始終用
workspace_id作為資料隔離鍵,防止 A 工作空間的使用者讀到 B 的資料。 - 金鑰輪換/撤銷應急:在空間管理裡對應用程式「輪換金鑰」,會產生新金鑰並一次性顯示,隨後用新金鑰重新設定你的後端。
5. 前端可信邊界(重要)
- 前端只做顯示佔位,不做授權判斷。前端能 base64 解開 JWT 的 payload,但沒有金鑰無法驗簽——任何人都能偽造一個「看起來對」的 payload。
- 所有「這個人是誰、能不能做某事」的判斷,必須以擴充應用程式後端
verifyWsa的結果為準。 - 因此標準策略是:前端把
wsa/code交給擴充應用程式後端 → 後端校驗 → 後端建工作階段 → 前端只認這個工作階段。
下一步:SDK-API-參考 查完整 API;遇到任何問題時請查閱 常見問題與排錯。
