實(shí)戰(zhàn):基于 RFC 9068 短時(shí) JWT 的 API 認(rèn)證接入指南)
Forem 委派 API 訪問(wèn)實(shí)戰(zhàn)基于 RFC 9068 短時(shí) JWT 的 API 認(rèn)證接入指南【免費(fèi)下載鏈接】foremFor empowering community 項(xiàng)目地址: https://gitcode.com/gh_mirrors/fo/forem本文圍繞 Forem 倉(cāng)庫(kù)的官方文檔 docs/delegated_access.md 展開(kāi)系統(tǒng)講解如何讓 Forem 的 V1 API 接受來(lái)自單一可信委派服務(wù)的短時(shí) RFC 9068 訪問(wèn)令牌access-token JWT作為 API Key 之外的另一條認(rèn)證通道。讀者將掌握完整的部署配置項(xiàng)、令牌與 JWKS 契約、緩存與密鑰輪換機(jī)制以及結(jié)合 app/services/delegated_access/ 源碼的底層驗(yàn)證原理可直接據(jù)此在自建 Forem 實(shí)例上安全接入委派認(rèn)證。什么是委派 API 訪問(wèn)Forem 的 API 默認(rèn)通過(guò)api-key請(qǐng)求頭進(jìn)行認(rèn)證見(jiàn) app/controllers/api/v1/api_controller.rb 中的authenticate_with_api_key。委派訪問(wèn)Delegated Access提供了一條互補(bǔ)路徑Forem 可以接受一個(gè)受信任委派服務(wù)簽發(fā)的、短生命周期的 RFC 9068 access-token JWT。需要特別明確兩個(gè)邊界它只是認(rèn)證方式的補(bǔ)充不改變賬戶關(guān)聯(lián)account linking也不影響瀏覽器登錄browser sign-in授權(quán)決策仍由委派服務(wù)自身負(fù)責(zé)Forem不會(huì)把控制器動(dòng)作映射到scope也不會(huì)重新解釋哪些 OAuth 客戶端可以使用某個(gè)授權(quán)。委派服務(wù)必須在授權(quán)范圍內(nèi)才簽發(fā)令牌。從源碼結(jié)構(gòu)看委派訪問(wèn)由三部分組成組件文件職責(zé)配置解析app/services/delegated_access/configuration.rb讀取并校驗(yàn)環(huán)境變量構(gòu)造 Verifier令牌/JWKS 校驗(yàn)app/services/delegated_access/verifier.rb解析 token、加載并緩存 JWKS、校驗(yàn)簽名與聲明JWKS 拉取app/services/delegated_access/jwks_client.rb從配置的 HTTPS 端點(diǎn)獲取 JWKS 文檔接入點(diǎn)位于 app/controllers/api/v1/api_controller.rb 的authenticate_with_delegated_access錯(cuò)誤類型定義在 app/services/delegated_access/errors.rb。部署配置必填項(xiàng)開(kāi)啟委派訪問(wèn)在啟用前必須一次性設(shè)置以下全部環(huán)境變量否則 configuration.rb 的required校驗(yàn)會(huì)直接拋出ArgumentErrorDELEGATED_ACCESS_ENABLEDtrue DELEGATED_ACCESS_ISSUERhttps://api.example.com DELEGATED_ACCESS_AUDIENCEhttps://community.example.com DELEGATED_ACCESS_IDENTITY_PROVIDERexternal-provider DELEGATED_ACCESS_OWNER_CLAIMhttps://api.example.com/claims/dev_user_id DELEGATED_ACCESS_JWKS_URIhttps://api.example.com/.well-known/jwks.json各配置項(xiàng)的語(yǔ)義與源碼級(jí)約束DELEGATED_ACCESS_ENABLED取值必須是字面量true或false否則啟動(dòng)即報(bào)錯(cuò)。只有在true時(shí)API 才會(huì)解釋Authorization: Bearer ...頭DELEGATED_ACCESS_ISSUER / AUDIENCE令牌iss/aud聲明的精確匹配值屬于部署級(jí)的信任設(shè)置DELEGATED_ACCESS_IDENTITY_PROVIDER用于在Identity表中按provider字段定位用戶見(jiàn)下文認(rèn)證流程DELEGATED_ACCESS_OWNER_CLAIM指定哪個(gè) claim 攜帶 Forem 用戶 IDDELEGATED_ACCESS_JWKS_URIJWKS 端點(diǎn)地址path_required: true要求帶非根路徑。文檔強(qiáng)調(diào)issuer、audience、identity provider、owner claim、JWKS URI 都是精確的部署信任設(shè)置JWKS URI 與 issuer必須是 HTTPS且絕不會(huì)從令牌聲明或請(qǐng)求頭中選取。Forem 不要求復(fù)制公鑰 PEM、不校驗(yàn)活躍的kid是否預(yù)注冊(cè)也不需要 OAuth client ID。源碼中還隱含了更細(xì)的校驗(yàn)規(guī)則configuration.rbHTTPS 校驗(yàn)要求uri.is_a?(URI::HTTPS)、host 非空、無(wú) userinfo、無(wú) query、無(wú) fragmentDELEGATED_ACCESS_OWNER_CLAIM必須是帶路徑的 HTTPS URI且不能是 RFC 9068 注冊(cè)聲明iss sub aud exp nbf iat jti client_id scope nonce sid以防止與標(biāo)準(zhǔn)聲明沖突must be collision-resistant??蛇x項(xiàng)安全上界保守默認(rèn)值DELEGATED_ACCESS_JWKS_MAX_AGE_SECONDS300 DELEGATED_ACCESS_MAX_TOKEN_LIFETIME_SECONDS60DELEGATED_ACCESS_JWKS_MAX_AGE_SECONDS每個(gè) Puma worker進(jìn)程內(nèi)JWKS 密鑰緩存的生存期默認(rèn) 300 秒。緩存過(guò)期后每個(gè)進(jìn)程只有第一個(gè)請(qǐng)求負(fù)責(zé)刷新并發(fā)請(qǐng)求會(huì)在寬限期內(nèi)繼續(xù)使用剛過(guò)期的條目超出寬限期后如果拉取失敗則絕不再使用過(guò)期條目DELEGATED_ACCESS_MAX_TOKEN_LIFETIME_SECONDS令牌允許的最大生命周期exp - iat默認(rèn) 60 秒強(qiáng)制令牌短時(shí)有效。這兩個(gè)變量在 configuration.rb 中通過(guò)positive_integer解析必須是正整數(shù)否則拋DELEGATED_ACCESS_JWKS_MAX_AGE_SECONDS must be a positive integer類錯(cuò)誤隨后被傳入Verifier的jwks_cache_lifetime與maximum_token_lifetime。初始化機(jī)制委派訪問(wèn)配置通過(guò) config/initializers/delegated_access.rb 掛載在to_prepare回調(diào)中執(zhí)行DelegatedAccess::Configuration.from_env.freeze存放到Rails.application.config.x.delegated_access。該配置對(duì)象是凍結(jié)的開(kāi)發(fā)環(huán)境下代碼重載時(shí)會(huì)重新構(gòu)建。之所以放在to_prepare里是為了讓 Zeitwerk 正常解析自動(dòng)加載的DelegatedAccess常量而不是手寫 require。未啟用時(shí)的行為只要DELEGATED_ACCESS_ENABLED不是trueAPI 就會(huì)完全忽略Authorization頭行為與引入此功能之前完全一致——即使客戶端同時(shí)攜帶api-key與Authorization也不會(huì)受影響。令牌與密鑰契約該契約遵循 RFC 9068尤其是其中面向資源服務(wù)器的檢查顯式的 access-token 類型、issuer、audience、簽名與過(guò)期時(shí)間。Forem 在此基礎(chǔ)上進(jìn)一步收窄了允許的形式簽名算法僅限RS256typ必須是atjwt頭部必須攜帶非空kid必須包含受限的sub、exp、iat、nbf、jti以及配置的 owner claim會(huì)話用途聲明sid與nonce會(huì)被拒絕完整的 RFC 9068 聲明集含client_id、scope由委派服務(wù)負(fù)責(zé)簽發(fā)但Forem 不使用這兩個(gè)聲明做授權(quán)。在實(shí)現(xiàn)上verifier.rb 的verify流程分兩段不校驗(yàn)簽名的解碼先做令牌與頭部的形式校驗(yàn)validate_token!、validate_header!確認(rèn)header.keys.sort HEADER_MEMBERS即恰好是alg、kid、typ三個(gè)成員且alg RS256、typ atjwt、kid是長(zhǎng)度不超過(guò) 128 字節(jié)的非空字符串嚴(yán)格校驗(yàn)簽名解碼通過(guò)JWT.decode(..., algorithms: [RS256], jwks: method(:load_jwks), iss:, aud:, required_claims: ..., verify_expiration/not_before/iat: true, leeway: 5)完成簽名驗(yàn)證、issuer/audience 精確匹配、時(shí)間聲明校驗(yàn)leeway為 5 秒時(shí)鐘偏差CLOCK_SKEW_SECONDS。聲明的二次校驗(yàn)validate_claims見(jiàn) verifier.rb包括aud必須與配置精確相等存在nonce或sid即拒絕sub、jti必須是長(zhǎng)度受限的非空字符串默認(rèn)上限 256 字節(jié)iat、nbf、exp必須是整數(shù)且exp - iat為正且不超過(guò)maximum_token_lifetimenbf不得晚于expowner claim 必須是\A[1-9]\d{0,18}\z格式的正整數(shù)字符串且不超過(guò)有符號(hào) 64 位整數(shù)上限9_223_372_036_854_775_807解析后作為Claims.owner_id。JWKS 端點(diǎn)要求配置的 JWKS 端點(diǎn)必須返回頂層含keys數(shù)組的 JSON且滿足至少一個(gè)唯一的 RSA 公鑰簽名密鑰標(biāo)記use: sig、alg: RS256模長(zhǎng)≥ 2048 位其他公鑰會(huì)被忽略任何私鑰 RSA 參數(shù)d p q dp dq qi oth都會(huì)使整個(gè)響應(yīng)失效。對(duì)應(yīng)源碼在 verifier.rbvalidated_signing_keys依次校驗(yàn)keys存在、數(shù)量不超過(guò)MAX_KEYS 32、每個(gè)成員是對(duì)象、不含私鑰參數(shù)、kid不重復(fù)eligible_key?進(jìn)一步要求 RSA 公鑰、模長(zhǎng) ≥ 2048 且指數(shù)e為不小于 3 的奇數(shù)。拉取端 jwks_client.rb 也有硬性約束連接與讀取超時(shí)各 2 秒、響應(yīng)體上限 64 KB、要求 HTTP 200 且content-type為application/json或application/jwk-setjson并以application/jwk-setjson, application/json作為 Accept 頭。認(rèn)證流程從 Bearer 頭到當(dāng)前用戶當(dāng)委派訪問(wèn)啟用且請(qǐng)求攜帶Authorization: Bearer token時(shí)api_controller.rb 的authenticate_with_delegated_access執(zhí)行以下鏈路讀取Rails.application.config.x.delegated_access若未啟用則直接返回delegated_bearer_token解析Authorization頭必須匹配Bearerscheme令牌非空且不含逗號(hào)否則拋InvalidTokenconfig.verifier.verify(token)完成全部令牌與簽名校驗(yàn)返回Claims(subject:, owner_id:)用Identity.includes(:user).where(provider: config.identity_provider, uid: claims.subject, user_id: claims.owner_id).sole在provideruid委派服務(wù)的 subjectuser_idowner claim 解析出的用戶 ID三個(gè)維度上精確定位用戶若用戶被標(biāo)記為 spam / suspended 或未注冊(cè)同樣拋InvalidToken校驗(yàn)通過(guò)后設(shè)置user authenticated_user user后續(xù) Pundit 授權(quán)與普通登錄用戶一致。在authenticate_with_api_keyapi_controller.rb中委派認(rèn)證優(yōu)先級(jí)高于 API Key先嘗試委派令牌失敗/缺失再回退到api-key請(qǐng)求頭。注意這個(gè)回退只在沒(méi)有有效委派令牌時(shí)發(fā)生一旦請(qǐng)求出示了 Bearer 令牌錯(cuò)誤處理就是嚴(yán)格的見(jiàn)下節(jié)。失敗、緩存與密鑰輪換錯(cuò)誤語(yǔ)義401 Unauthorized畸形令牌、聲明或簽名無(wú)效、未知kid。對(duì)應(yīng)rescue_from DelegatedAccess::Errors::InvalidTokenapi_controller.rb返回{error:unauthorized,status:401}503 Service Unavailable僅當(dāng)沒(méi)有可用緩存條目且信任端點(diǎn)不可用或返回?zé)o效內(nèi)容時(shí)觸發(fā)rescue_from DelegatedAccess::Errors::Unavailableapi_controller.rb返回{error:delegated access unavailable,status:503}。文檔明確在啟用委派訪問(wèn)期間這兩種情況都不會(huì)從已出示的 Bearer 令牌回退到 API Key 認(rèn)證。緩存與防放大緩存邏輯在 verifier.rb 的load_jwks使用cache.fetchexpires_in: jwks_cache_lifetime并設(shè)置了race_condition_ttl: JWKS_REFRESH_GRACE_SECONDS10 秒實(shí)現(xiàn)每進(jìn)程單請(qǐng)求刷新、其余請(qǐng)求繼續(xù)用剛過(guò)期條目的節(jié)流策略。遙測(cè)通過(guò)ForemStatsClient.increment(delegated_access.verification, tags: [outcome:...]記錄refresh、cache_hit、accepted、rejected、unavailable_jwks等結(jié)果。防放大機(jī)制未知kid不會(huì)使緩存失效也不會(huì)觸發(fā)對(duì) issuer 的請(qǐng)求從而防止攻擊者用隨機(jī) ID 刷爆 JWKS 端點(diǎn)流量。密鑰輪換與應(yīng)急失效文檔給出的輪換步驟先同時(shí)發(fā)布舊密鑰與繼任密鑰等待至少一個(gè)配置的緩存生命周期DELEGATED_ACCESS_JWKS_MAX_AGE_SECONDS之后才開(kāi)始用繼任密鑰簽名。若懷疑密鑰泄露操作員可在Rails console中立即使進(jìn)程內(nèi)密鑰緩存失效Rails.application.config.x.delegated_access.invalidate_cache!該命令映射到 configuration.rb 的invalidate_cache!最終調(diào)用 verifier.rb 刪除緩存鍵:delegated_access_jwks。注意必須在每個(gè) Forem 應(yīng)用進(jìn)程中分別執(zhí)行或直接重啟全部應(yīng)用進(jìn)程執(zhí)行前應(yīng)先把被泄露的公鑰從 issuer 的 JWKS 中移除緩存只包含經(jīng)過(guò)校驗(yàn)的公鑰材料且不是持久化的重啟即清空。安全邊界小結(jié)綜合文檔與源碼可以歸納出以下硬性邊界對(duì)應(yīng)常量定義在 verifier.rb約束值來(lái)源令牌最大字節(jié)數(shù)16,38416 KBMAX_TOKEN_BYTESJWKS 最大密鑰數(shù)32MAX_KEYSkid最大字節(jié)數(shù)128MAX_KID_BYTES字符串聲明最大字節(jié)數(shù)256MAX_STRING_CLAIM_BYTES時(shí)鐘偏差5 秒CLOCK_SKEW_SECONDS刷新寬限期10 秒JWKS_REFRESH_GRACE_SECONDSRSA 模長(zhǎng)下限2048 位eligible_key?緩存生命周期 / 令牌最大壽命300s / 60s默認(rèn)Configuration默認(rèn)值這些數(shù)值共同保證令牌短命、輸入有界、JWKS 文檔小且只含公鑰、簽名強(qiáng)度有下限從輸入面到信任面都做了收斂。委派服務(wù)需要為每個(gè)授權(quán)請(qǐng)求簽發(fā)完整且正確的 RFC 9068 聲明集并自行承擔(dān)授權(quán)判斷——Forem 只做認(rèn)證與用戶映射。測(cè)試與驗(yàn)證倉(cāng)庫(kù)在 spec/services/delegated_access/ 下提供了三個(gè)核心測(cè)試文件可作為接入時(shí)的參考基線verifier_spec.rb覆蓋令牌頭部校驗(yàn)、聲明校驗(yàn)、生命周期校驗(yàn)、JWKS 解析與緩存刷新、未知kid行為等configuration_spec.rb覆蓋環(huán)境變量必填、HTTPS 校驗(yàn)、owner claim 沖突檢查、正整數(shù)邊界jwks_client_spec.rb覆蓋 HTTP 狀態(tài)、Content-Type、響應(yīng)體大小上限與超時(shí)處理。端到端層面authenticate_with_delegated_access的 401/503 映射可見(jiàn)于 app/controllers/api/v1/api_controller.rb相關(guān)請(qǐng)求級(jí)行為可在 spec/requests/api/v1/users_spec.rb 等 API 測(cè)試中進(jìn)一步驗(yàn)證。簡(jiǎn)言之委派訪問(wèn)為 Forem 提供了一條短時(shí)、精確、可輪換、可應(yīng)急失效的機(jī)器對(duì)機(jī)器認(rèn)證通道只要委派服務(wù)嚴(yán)格按本文契約簽發(fā)令牌、正確配置 JWKS 并遵循輪換節(jié)奏即可安全地讓受信任服務(wù)以指定用戶身份調(diào)用 Forem API而不必分發(fā)長(zhǎng)期有效的 API Key。【免費(fèi)下載鏈接】foremFor empowering community 項(xiàng)目地址: https://gitcode.com/gh_mirrors/fo/forem創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考