Token 與撤銷

流程跑完之後,串接方需要知道的 Signet token 事項:生命週期、刷新、撤銷、即時驗證。

On-Behalf-Of(OBO)

啟用後,機密用戶端 API A 可以將 Signet 簽發、aud 指向 A 的使用者 access token,交換成只能存取 B 的 token。
以表單 POST /oauth/token,提供 grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer、requested_token_use=on_behalf_of、assertion、單一 resource 與 scope。
A 使用 Basic 或表單憑證驗證,不能混用;重複/未知參數與 extra_claims 均拒絕。

管理員設定 OBO_ENABLED=true 與 OBO_POLICIES_FILE,明確指定 A 的 audience 所有權與 scope 對應,並將 B 加入 A 的允許資源。
使用者須先透過既有授權流程,同意 F 存取 A,以及 A 存取 B。缺少同意回傳 invalid_grant。

輸出保留使用者 sub,設定 client_id=A、act.sub=client:<A ID>、aud=B。
有效期取 OBO_TOKEN_EXPIRATION(預設及上限 5m)、client profile 與來源到期時間的最早者;不發 refresh/ID token,不允許再次 OBO。
所有 grant 的 act、may_act 均為服務端保留欄位。不包含 Agent OBO 或 Entra 串接。

線上驗證即時檢查來源、兩筆同意、使用者、clients 與政策。需要即時撤銷時,須結合本地 JWT 驗證與不快取的 introspection,並配置足夠限流額度。
啟用 ownership 檢查時,B 可能只取得 active,仍須自行檢查 audience 與 claims。離線驗證只會在 token 到期後失效。
關閉 OBO 會拒絕交換及委派 token 線上驗證;降版前須撤銷剩餘委派紀錄並等待最長 token 有效期。

使用場景、政策設定、兩次同意流程與完整交換範例:OBO 串接指南。

Token 生命週期

流程成功後您會拿到其中一或多種:

Token 格式 生命週期(依客戶端 profile 而定) 用途
Access token JWT short 15m · standard 10h · long 24h(近似值) Authorization: Bearer 打 API
Refresh token JWT(請當不透明處理) short 1d · standard 7d · long 30d(近似值) 到 /oauth/token 換新 access token
ID token JWT 與 access token 相同 客戶端身分資訊 — 見 OIDC

Refresh token 內部是 JWT,但您應該 把它當成不透明 — 在客戶端不要去解析其 claim,收穫為零還會耦合到內部實作。

實際數值取決於管理員為此客戶端選擇的 token profile。請一律相信 token response 的 expires_in,永遠不要寫死。

Audience Binding (aud claim)

當流程帶有 resource=<URL> 參數(RFC 8707),簽發的 access token 的 aud 會綁到該 resource。不帶 resource 時 aud 會回退到部署層級的 JWT_AUDIENCE。Refresh token 一律用靜態 JWT_AUDIENCE,不會帶每次請求的 resource。Resource server 應檢查 aud 等於自己的識別字,同時要求 type=access — 見 JWT 驗證 §Audience Binding。

每客戶端允許清單(預設全部拒絕):客戶端只能把 aud 綁到管理員已加入該客戶端 允許資源清單 的 resource 值。若清單為空,任何 您傳入的 resource= 都會以 invalid_target 被拒 — 完全不傳 resource(走 JWT_AUDIENCE 回退)仍可運作。請管理員把您客戶端需要的每個資源識別字加入允許清單。見 錯誤處理 §Resource Indicator 錯誤。

刷新 token

在 refresh token 本身過期之前,任何時刻都可以:

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
# 機密客戶端:改用 -u "$CLIENT_ID:$CLIENT_SECRET",body 不要帶 client_id

回應 與初次 token 交換相同格式。

刷新時縮小 resource(RFC 8707 §2.2):可選擇帶 resource=... 來簽發 aud 為原授權 子集 的新 access token。要求未在原授權的 resource(擴張)會回 400 invalid_target — refresh token 不會被消耗。省略 resource 則拿到綁定完整授權集的 token。

何時刷新:提前刷,例如過期前 30–60 秒,不要等到收到 401 才做。這樣可以避免請求失敗的中途錯誤與重試的噪音。

若您的部署啟用 rotation 模式(下節),還必須 將同一 session 的並發刷新序列化 — 兩個分頁同時刷新會直接毀掉 session。

輪轉模式:重用偵測的陷阱

某些 Signet 部署會啟用 rotation 模式(ENABLE_TOKEN_ROTATION=true)。在這個模式:

  • 每次刷新會簽發 新的 refresh token 並 作廢 舊的。
  • 若舊 refresh token 被再次使用(兩個分頁搶刷、網路抖動後重試、token 被偷去用),Signet 會偵測到重用,然後 把整個 token family 撤銷。
  • 後續請求會回 {"error": "invalid_grant"}。

對串接方的實務意義:

  • 序列化每個使用者 / session 的刷新(mutex、single-flight)。兩個分頁同時刷新,兩邊都拿著同一份舊 refresh token,一個會贏,另一個會用 剛被作廢 的舊 token 觸發重用偵測,整個 session 就死了。
  • 立刻持久化新 refresh token。儲存更新前,不要先用舊的再發一輪請求。
  • 刷新時收到 invalid_grant 是終態 — 請顯示登入頁面,不要重試。

從 token response 本身無法判斷 rotation 是否開啟。若您的串接必須同時支援兩種模式,一律把回傳的 refresh_token 存下來(即使看起來一樣 — rotation 模式下它會不同)。

登出 — /oauth/revoke(RFC 7009)

登出時撤銷 refresh token(access token 可選),讓被偷走的也變成啞彈:

curl -X POST https://your-signet/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=REFRESH_TOKEN" \
  -d "token_type_hint=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID"
# 機密客戶端:帶 client_secret 或使用 HTTP Basic
參數 必填 值
token 是 要撤銷的 token
token_type_hint 否 access_token 或 refresh_token
client_id 是 機密客戶端還需要 client_secret

依 RFC 7009,不論 token 原本存不存在,端點一律回 200 OK。不要依賴回應判斷狀態 — 直接當作 token 已經消失。

撤銷一張 refresh token 也會(在 rotation 模式下)作廢整個 token family。撤銷 access token 不會 順便作廢對應的 refresh token — 要嘛兩者都撤,要嘛登出時撤 refresh token,短效的 access token 自然過期即可。

本端點同時也能撤銷個人 API 金鑰。 傳 token=sgk_...,不需要客戶端憑證 — 持有金鑰本身即是授權。適合讓 script 在工作結束時自行拆掉它的短命金鑰。見 API 金鑰。

Caller-Supplied Extra Claims

/oauth/token 接受一個選填的 extra_claims form 參數 — 一個 JSON 物件,內含要嵌入所簽發 token 的額外 claim。它在四種 grant 全都可用(authorization_code、device_code、client_credentials、refresh_token)。

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode 'extra_claims={"tenant":"acme","region":"eu"}'

規則與防護:

  • 值必須是 JSON 物件。畸形 JSON、超過大小上限、或使用保留 key 都會回 400 invalid_request。
  • 預設大小上限(營運者可各自調整或停用):≤ 4096 bytes raw、≤ 16 keys、每個值 ≤ 512 bytes。
  • 保留 key 會被拒:標準 RFC/OIDC/Signet 管理的 claim(iss、sub、aud、exp、iat、jti、type、scope、client_id、user_id、nonce、at_hash…)不能被覆寫。
  • 整個功能可由營運者停用(EXTRA_CLAIMS_ENABLED=false),此時任何非空的 extra_claims 都會被拒絕。
  • 也會嵌入 refresh token。 對於會簽發 refresh token 的 grant(authorization_code、device_code,以及 rotation 模式的 refresh_token 兌換),同樣的 claim 也會併入 refresh token JWT。您應把 refresh token 當成不透明,但它其實可以解碼 — 請把這些 claim 納入資料外洩評估,別把您不希望存在於一個數天有效 token 裡的東西放進 extra_claims。
  • 無狀態 — 不會被持久化。 Claim 不會隨授權一起儲存,所以您必須 每次刷新都重新提供 extra_claims 才能讓它們留在新的 token 中。

信任模型:這些 claim 是 呼叫端自我宣稱的,並非由 Signet 背書。Resource server 必須把它們當成不可信輸入 — 絕不可把某個 extra_claims 值當成 Signet 已為其擔保那樣拿來做授權判斷。

即時驗證

resource server 的本地 JWT 驗證見 JWT 驗證。那條路速度快、可水平擴展,但 無法 察覺被撤銷 / 停用的 token — 一張被撤銷的 JWT 在密碼學上仍然有效,直到 exp。

需要即時察覺撤銷時,用以下端點之一:

/oauth/introspect(RFC 7662)— 首選

需要客戶端認證(呼叫端本身必須是已註冊的 Signet 客戶端):

curl -X POST https://your-signet/oauth/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=TOKEN_TO_CHECK" \
  -d "token_type_hint=access_token"

回應:

{
  "active": true,
  "scope": "openid profile email",
  "client_id": "client-uuid",
  "username": "alice",
  "token_type": "Bearer",
  "exp": 1700000000,
  "iat": 1699996400,
  "sub": "user-uuid",
  "iss": "https://your-signet",
  "jti": "unique-token-id"
}

若 token 無效、過期、被撤銷或停用,回應是:

{ "active": false }

擁有權關卡:預設情況下(INTROSPECTION_REQUIRE_OWNERSHIP=true)上述完整 metadata 只 對您自己的客戶端所簽發的 token 回傳。若您內省一個屬於 其他 客戶端的 有效 token,回應會被精簡成 { "active": true } — 沒有 sub、scope、username、client_id、aud、exp 等。除非您的營運者已把此旗標設為 false,否則不要把 resource server 建立在跨客戶端內省之上。

政策強制 要有即時性時用這個 — 管理儀表板、高價值操作,任何無法容忍「陳舊有效」窗口長達整個 access token 壽命(standard profile 約 10 小時、long 24 小時)的場景。

/oauth/tokeninfo — 輕量替代

以 Bearer header 帶 token,回傳較少的欄位。不用客戶端憑證(token 本身即認證):

curl -H "Authorization: Bearer TOKEN_TO_CHECK" https://your-signet/oauth/tokeninfo
{
  "active": true,
  "user_id": "user-uuid",
  "client_id": "client-uuid",
  "scope": "openid profile email",
  "exp": 1700000000,
  "iss": "https://your-signet",
  "subject_type": "user"
}

Client Credentials 發出的 token,subject_type 會是 "client"。無效 token 回 401 並帶 OAuth invalid_token 錯誤。

個人 API 金鑰:本端點與 /oauth/introspect 也接受不透明的 sgk_ 金鑰,回應會多一個 token_type: "personal_api_key",方便您套用不同政策。金鑰 不是 JWT,所以這兩個端點是驗證它的 唯一 途徑 — 見 API 金鑰。

已登入使用者可從 開發 → Token Info(/account/token-info)貼上任一憑證,查看經驗證的專案、過期時間、scopes 與主體資訊。表單使用 POST,不會把憑證放進網址或結果頁,且查詢會寫入使用者的稽核日誌。無效、過期、撤銷與未知憑證刻意共用相同結果。

該選哪個?

需求 方式
resource server 大量驗證,可容忍短暫陳舊 本地 JWKS 驗證(不打 Signet)
需要即時撤銷狀態,呼叫端能做客戶端認證 /oauth/introspect
使用者 session 內的互動式檢查 開發 → Token Info(/account/token-info)
呼叫端本身就是此 token 的持有者 /oauth/tokeninfo
驗證不透明的 sgk_ 個人 API 金鑰 /oauth/tokeninfo,除非您就是以金鑰所綁定的那個應用認證 — 本地驗證不可能(API 金鑰)

速率限制

Signet 對 token 路徑端點做每 IP 速率限制 — 唯一例外是 /oauth/introspect:每個已認證的客戶端應用各有自己的額度(同一個 egress IP 後面的整個 fleet 不會擠成一個桶),後面再加一道每 IP 上限。預設值(營運者可調整):

端點 預設限制
POST /oauth/token 每 IP 20 req/min
POST /oauth/device/code 每 IP 10 req/min
POST /device/verify 每 IP 10 req/min
POST /oauth/introspect 每客戶端應用 600 req/min,每 IP 1200
GET /oauth/tokeninfo 每 IP 600 req/min
POST /account/token-info 每 IP 600 req/min(獨立瀏覽器額度)
GET/POST /oauth/userinfo 每 IP 600 req/min
POST /login 每 IP 5 req/min

超過限制回 429 Too Many Requests。若有 Retry-After header 請遵守;沒有的話指數退避。能批次就批次 — 可以本地 JWKS 驗證時,絕對不要每個請求都打 /oauth/tokeninfo 或 /oauth/introspect 來驗 JWT;唯一真的需要往返的憑證是 sgk_ 個人 API 金鑰,而且您應該短暫快取其驗證結果(API 金鑰)。

相關文件