跳至主要内容

ADR-030: 互動式 Session 生命週期、Refresh Rotation 與錯誤契約

ADR 編號: 030 狀態: 已接受,分階段實作中 (Accepted) 決策日期: 2026-08-11 決策者: Development Team 取代: 無 被取代: 無 2026-10-24 修訂: 接受跨環境 bounded Session profile;目標 Web 19.0.0-alpha.136/Server 4.0.0-alpha.39,框架實作人審已通過,配對 artifact 已於 2026-10-25 發布,產品採用另行完成。僅 opt-in profile 局部取代原 callback 逾時釋鎖及 Session 政策,見決策八。


2026-08-21 錯誤揭露修訂:refresh-session-invalid stable code 與 client reauthentication 分支保持不變,但「Server 對外一律不透明、精確原因只進 audit」不再是框架預設。引擎保留 missing、expired、revoked、eligibility、rotation/replay 與 policy rejection 的實際原因;參考實作 預設最大揭露 detail、例外型別、訊息與 stack trace。應用可注入最小或自訂揭露政策恢復通用 detail。Opaque Refresh Credential 的儲存、rotation 與 Session family 決策不受影響。


摘要​

互動式登入採「短效 Access JWT + 有狀態、可輪替的 Refresh Session」:Access Token 只負責短期 API 存取,Refresh Session 才是登入連續性的權威來源。瀏覽器以 HttpOnly Cookie 持有不透明 Refresh Credential,Server 只保存雜湊並在每次成功 refresh 後原子輪替;rememberMe 同時決定 Cookie 是否跨瀏覽器工作階段保留,以及 Refresh Session 的滑動閒置期限。

前端不得把任意 API 401、網路錯誤或 5xx 解讀為 Session 過期。未採用決策八的 opt-in profile 時,正常契約只有 Refresh API 以穩定的 RFC 9457 problem type/errorCode=refresh-session-invalid 明確拒絕 Refresh Session時,才進入 重新認證流程;無 code 的 legacy Refresh 401 在相容期內會記 warning 後走同一出口。人類可讀的 detail 不作程式分支;參考 Server 預設回傳精確原因,應用可用揭露政策收斂為不透明訊息。 Refresh 拒絕無論採何種揭露政策都維持 refresh-session-invalid stable code,精確原因亦寫入 audit log。

開發環境重建資料、清除 Refresh Session store 或使用臨時金鑰,屬於環境重設,既有登入需要重新建立; 不得為了掩蓋此現象而把 seed Account.id 固定,或把 assigned-ID generator 擴散成正式環境的通用持久化機制。


背景 (Context)​

問題陳述​

fix/session-expiration-false-positives 暴露的不是單一例外映射錯誤,而是四個未被共同建模的概念:

  1. Access Token 失效不等於 Refresh Session 失效。
  2. Refresh 基礎設施失敗不等於使用者需要重新登入。
  3. rememberMe 究竟是前端 storage 選擇,還是 Server Session 政策,規格與實作互相矛盾。
  4. Account 身分、Server 簽章金鑰與 Refresh Session 各有獨立生命週期,不能靠固定 seed UUID 混成一件事。

現況走查​

層級現況風險
Server refresh controller已知 AuthenticationException 與其他所有 Exception 都映成 401DB、cache、程式錯誤或暫時性故障會被誤報成 Session 失效
Server error body有 RFC 9457 形狀與 errorCode 能力,但 auth 路徑大多只送 detail,部分直接沿用 exception message前端只能猜狀態碼或解析文字;內部訊息可能外洩
Web HTTP client已有 proactive refresh、single-flight、一次重放,且保留 transport/5xx核心方向正確,但 app adapter 目前只以 401 判定 refresh 明確失效
Refresh Token長效 JWT、不輪替、沒有 Refresh Session store無法實作滑動閒置期限、rotation 與 replay detection
Remember Me前端實作只選 localStorage/sessionStorage,不送 Server;US-001 卻要求 30 天滑動 Session同一功能存在兩套互斥契約;密碼、OTP 與 OIDC 行為會漂移
MockRefresh header 的 bare token 可能被當作 Bearer header 解析;查無舊 subject 時回 404Mock 與 Server 線格式不同,且 404 會讓前端無法統一判斷 Refresh Session
開發重啟DB seed、JWT keypair、blacklist/session store 可各自重設固定其中一個 UUID 不能保證既有 token 可延續,卻會改變正式環境的 identity policy

規格衝突​

目前產品規格至少有以下直接矛盾:

  • US-001(2026-08-04)要求登入傳 rememberMe、30 天滑動 Refresh Session、每次 refresh 輪替。
  • US-015 Scenario 2(2026-08-01)要求 remember 不進 API,且不得改變 Refresh Token 有效期。
  • 現行實作符合 US-015 的「純前端 storage」方向,但不符合較新的 US-001。

本 ADR 提議以較新的 US-001 為產品意圖,並將所有互動式登入入口對齊同一個 Session issuance 契約; US-015 應在 ADR 接受後修訂。

範圍​

本 ADR 涵蓋:

  • 帳密、Email OTP、OIDC 與 signed-link exchange 所建立的互動式 Session
  • Access Token、Refresh Session、logout/revoke、rememberMe
  • Server error taxonomy 與前端狀態轉移
  • 開發環境重啟與 seed identity 的邊界

本 ADR 不改變:

  • M2M client_credentials 與 API key;它們沒有互動式 Session、remember 或 refresh token
  • AuthPrincipal.subject() 的既有定義:在 reference implementation 中為 Account UUID
  • 受保護資源的 RFC 6750 Bearer challenge 基本線格式

考量的方案 (Options Considered)​

方案 A:維持長效 Refresh JWT,只修正例外映射​

說明:保留 X-Refresh-Token 與固定期限 JWT,只讓未知例外回 5xx,前端以 401 判斷失效。

優點:

  • ✅ 變更最小,可快速消除一部分 false positive
  • ✅ 不新增持久化資料模型

缺點:

  • ❌ 無法滿足 rotation、replay detection 與滑動閒置期限
  • ❌ Refresh credential 存於 Web Storage,XSS 可直接讀取
  • ❌ Refresh Session 仍與 JWT signing key 綁定,key rotation/臨時金鑰重啟必然中斷

評分:2/5

方案 B:有狀態的 Refresh Session + 不透明 HttpOnly Credential(採用)​

說明:Access Token 維持短效 JWT;Refresh credential 改為高熵不透明值,Server 保存雜湊與 Session metadata,透過 Secure; HttpOnly; SameSite Cookie 呈遞並在 refresh 時原子輪替。

優點:

  • ✅ 可完整表達 revoke、idle expiry、rotation 與 replay detection
  • ✅ Refresh credential 不暴露給 JavaScript
  • ✅ Signing key rotation 只使舊 Access Token 需要 refresh,不必終止仍有效的 Refresh Session
  • ✅ remember 可成為一致的 Server/browser Session 政策

缺點:

  • ❌ 需要持久化 store、交易式 rotation 與過期清理
  • ❌ Cookie 模式需明確處理 CSRF、same-site 與部署拓樸
  • ❌ 嚴格 rotation 前必須解決多分頁同時 refresh 的競態

評分:5/5

方案 C:完整 BFF/Server-side Session,不向瀏覽器發 Access JWT​

說明:Browser 只持有 session cookie,BFF 代為呼叫下游 API。

優點:

  • ✅ Browser 不接觸任何 bearer token
  • ✅ 撤銷與 Session 管理最直接

缺點:

  • ❌ 會改變目前 SPA 直接呼叫 API 的部署與擴展模型
  • ❌ 超出本次修正範圍,也會與現有 mobile/API consumer 模式產生新邊界

評分:3/5


決策 (Decision)​

選擇方案:B——有狀態的 Refresh Session + 不透明 HttpOnly Credential。

本決策分階段落地;第一階段先修正錯誤契約與 false positive,不以「最終模型尚未完成」為由延後 現有安全修正。

決策一:Session 與 Token 詞彙分離​

詞彙定義權威來源
Authentication證明目前請求代表哪個 principalSpring Security context
Access Token正式 Server 短效 Bearer JWT(既有基線 15 分鐘);決策八的 Mock 為 opaque Access正式 JWT 簽章/claims,新 profile 另查 family;Mock 透過 inspect
Refresh Session可撤銷、可輪替、有閒置期限的互動式登入狀態RefreshSessionStore
Refresh CredentialBrowser 持有、用來證明 Refresh Session 的一次性高熵 secretHttpOnly Cookie + Server hash
Remember MeSession issuance policy,不是身分屬性登入/exchange request + Refresh Session
Reauthentication required需再次證明使用者身分;既有基線由明確 Refresh 拒絕觸發,新 profile 亦可由受控 reconcile 觸發;不等同已登出Refresh API problem type,或決策八的 family fence/恢復狀態

「Access Token expired」只代表應嘗試 refresh;「Session expired」只能描述 Refresh Session 的特定 拒絕原因。UI 的通用狀態與元件應命名為 Reauthentication Required,避免把 revoke、帳號刪除、 credential replay 或開發環境重設都誤稱為 expiration。

決策二:前端狀態機​

以下圖表描述既有基線。新 bounded profile 另有 family mismatch、owner 中斷恢復與更新頁面出口,依決策八分流;保留基線不表示兩種 profile 可混用同一 Cookie。

必須遵守以下轉移:

事件Session stateToken storageUI 行為
一般 API 401尚未判定失效保留single-flight refresh
Refresh 200有效原子換成新 access/refresh原請求最多重放一次
Refresh 401 + refresh-session-invalid需要重新認證可先保留 identity hint;credential 由 Server 清 cookie顯示重新認證;無 current user 則完整登入
Refresh 401、但缺少可辨識 code相容期內視為需要重新認證保留 identity hint記錄 client warning 後顯示重新認證;不得靜默卡住
Refresh transport/timeout/5xx未知、不得判失效保留走 Server availability/reconnect,不顯示 Session Expired
一般 API 403有效保留顯示權限不足,不 refresh、不登出
一般 API 429有效保留依 Retry-After 等待
explicit logout已撤銷/登出清除回登入頁

同一瀏覽器分頁內沿用既有 single-flight;嚴格 Refresh Token rotation 上線前,還必須以 Web Locks 等同源協調機制序列化跨分頁 refresh。否則兩個合法分頁同時呈遞舊 credential,第二個請求會被誤判 為 replay。Server 的 atomic compare-and-rotate 仍是最終一致性防線,不能只靠前端鎖。

strict 模式的部署不變量:一個 Refresh Session Cookie family 只能由一個 Browser origin 消費。 Web Locks 是 per-origin,而 cookie 可因 Domain/共同 API origin 被多個前端 origin 共用;這種拓樸 沒有共同 mutex,合法競態會觸發 strict replay revoke。正式環境需要多個前端 origin 時,必須切分 cookie/Session family,或另立 ADR 將 Server 改成具明確安全邊界的 grace protocol;不得只放寬 Cookie Domain。

localhost 開發是明確、受限的例外:reference Server 可在 app.env=DEV 時設定 app.security.refresh-session.rotation-mode=reusable。此模式仍以原子 credential-use 驗證撤銷與期限、 滑動 idle window 並重鑄 Access Token,但不輪替 Refresh Credential,也不執行 replay family revoke, 讓不同 port 的 Vite origins 可共用同一個開發 Session。reusable 不是 production protocol;設定於 非 DEV 環境必須 fail fast。

決策三:穩定、可機器判讀的錯誤契約​

互動式 auth HTTP 殼使用 RFC 9457 application/problem+json:

  • type 是主要、穩定的問題識別 URI:urn:appfuse:error:<code>。
  • errorCode 與 <code> 相同,供現有 TypeScript ErrorResponse 直接消費;新 client 以 type 為主要識別,errorCode 作為既有相容欄位。
  • title/detail 是人類可讀文字,可翻譯、可調整,前端不得解析或比對。
  • instance 是請求路徑或不透明 occurrence identifier,不放敏感資料。
  • Server log/audit event 另記 internal reason、correlation ID 與必要維運資訊;不得把 exception message 直接當 wire message。

舊版 Server、gateway 或反向代理可能回傳沒有 stable code 的 Refresh 401。為避免 client 落入「沒有 availability event、也沒有 reauthentication event」的無出口狀態,相容期內 framework classifier 會記錄 warning 並要求重新認證。這是 legacy fallback,不是正常契約;transport、timeout 與 5xx 仍一律保留 Session。待支援矩陣中的所有 Server/gateway 都符合 stable problem contract 後再移除 fallback。

最小 auth taxonomy:

情境HTTPerrorCodeClient 決策
登入欄位無效400validation-error留在表單並標示欄位
帳密錯、tenant 不符、帳號不存在或不可互動登入401最大揭露為 bad-credentials、username-not-found 或實際帳號狀態;最小揭露為 invalid-credentials顯示 policy 選定的原因;client 不以 detail 分支
產品明定的暫時鎖定423login-temporarily-locked顯示可重試時間;不建立 Session
登入限流429authentication-rate-limited依 Retry-After 等待
受保護資源未帶 credential401authentication-required有本地 Session 才嘗試 refresh
Access Token 無效、過期或被撤銷401access-token-invalid嘗試 refresh;尚不宣告 Session 失效
已認證但權限不足403insufficient-permission不 refresh、不清 Session
Refresh credential 缺少、無效、過期、被撤銷、replay、subject 不存在或不可用401refresh-session-invalid既有基線的重新認證出口;新 profile 另依決策八處理 family mismatch 與 owner 中斷
Refresh 依賴暫時不可用503authentication-service-unavailable保留 Session,進入 reconnect
未預期 Server 錯誤500internal-error保留 Session,記錄 correlation ID

受保護資源的 401/403 同時維持 RFC 6750 WWW-Authenticate:invalid_token 與 insufficient_scope 是標準 challenge code;ProblemDetail 的 app code 用來驅動本產品行為,兩者不得混用。 M2M token endpoint 繼續使用 RFC 6749 的 error/error_description,不強迫改成 ProblemDetail。

Server 實作必須以型別或結果物件表達「預期的 Refresh Session 拒絕」;controller 只能把該類拒絕映成 401 refresh-session-invalid。DB、cache、serialization、程式缺陷與其他未知例外不得被 catch-all 轉成 401。

決策四:Refresh Session 與 rotation​

框架提供不綁定 JPA 的 RefreshSessionStore capability;reference implementation 擁有具體 Entity/ Repository,對齊 ADR-017 的 ownership 原則。

每個 Refresh Session 至少保存:

欄位用途
sessionIdSession family 穩定識別
subjectAuthPrincipal.subject();Account row 生命期內穩定
tenantId多租戶查核與 audit
credentialHash/credentialSelectorHash/generation秘密 selector 定位 family、verifier hash 原子驗證與輪替;不保存明文 credential
rememberedSession policy
idleExpiresAtRemember Me 滑動閒置期限
createdAt/lastRefreshedAtaudit 與清理
revokedAt/revokeReasonlogout、安全事件與 replay 撤銷
versioncompare-and-rotate 併發控制

production-safe 的 strict 模式下,Refresh 成功必須在單一原子操作中完成:

  1. 以舊 credential hash 找到 Session 並鎖定/compare version。
  2. 檢查未撤銷、未逾期,並以 AuthPrincipalLookup 重載目前帳號狀態與權限。
  3. 產生新高熵 credential,只保存新 hash;舊 credential 失效。
  4. Remembered Session 將 idleExpiresAt 自本次成功 refresh 往後推 30 天。
  5. 回傳新 Access Token,並以 Set-Cookie 原子替換 Refresh Credential。

偵測到已輪替 credential 被再次使用時,預設撤銷整個 Session family 並留下 audit event。為避免把多分頁 競態當成攻擊,rotation 上線門檻包含跨分頁 refresh 序列化與對競態/replay 的整合測試;不得只完成 Server 嚴格拒絕的一半。

開發用 reusable 模式沿用同一份 Session store 與 principal eligibility 契約,但原子操作改為 credential-use:credential hash 必須吻合、Session 必須未撤銷且未過期,成功後只更新 lastRefreshedAt/idleExpiresAt,回應仍寫回同一 credential。此模式刻意不提供 replay detection, 代價是開發瀏覽器中的 credential 若外洩,可在 Session 到期或撤銷前重複使用。

決策五:Remember Me 是 Session issuance policy​

rememberMe 不是 Account 欄位,也不只是 Redux action metadata。所有會建立互動式 Session 的入口都 必須解析成共同的 SessionOptions:

Session 入口Remember policy 的傳遞方式
帳密登入login body 的 rememberMe
Email OTPverify request 的 rememberMe,或建立 challenge 時由 Server 保存;兩者擇一後全程一致
OIDCinitiation request 寫入受保護的 state/context,callback 建 Session 時取回
signed-link exchange預設 non-remembered;若產品允許記住,exchange request 明確傳入且受 purpose policy 約束
reauthentication modal沿用原 Session 的 policy,不默默改成 remembered

Browser 行為:

  • rememberMe=false:Refresh Credential 使用 session cookie,不設 Expires/Max-Age;關閉瀏覽器 工作階段後即無法恢復。Server 仍設定有限的清理期限,避免孤兒資料永久存在。
  • rememberMe=true:Refresh Credential 使用 persistent cookie;Max-Age 與 Server 的 30 天滑動 idle window 對齊,每次成功 refresh 一併更新。
  • Access Token 只存在記憶體;頁面啟動時以 Refresh Cookie bootstrap,不把 access/refresh token 放入 localStorage 或 sessionStorage。
  • Cookie 至少為 Secure; HttpOnly; SameSite=Lax,Path 收窄到 auth session 端點;跨站部署若需 SameSite=None,必須另加 Origin/CSRF 驗證,不能只改 cookie flag。

決策六:身分持久化與重啟語意​

  1. Account.id 是該 persisted row 的身分,建立後穩定;刪除再建立是新身分,舊 Session 不得依 username 或 seed 名稱自動轉綁。
  2. Seed 必須以業務 natural key 做冪等查找,讓既有資料庫中的 row 保留原 ID。只有產品明定的 bootstrap identity 才能使用指定 ID;「希望開發重啟後不用登入」不是足夠理由。
  3. 正式/測試環境必須使用持久化、可共享的 JWT signing keys 與 Refresh Session store;缺少必要設定 應 fail fast,不得默默產生每個 instance 不同的臨時狀態。
  4. 零設定開發模式可以使用臨時 key/store,但啟動時必須明確警告「重啟或重建資料後需重新登入」。 這是 environment reset,不應以「Session expired」呈現,也不以固定 seed UUID 假裝 Session 仍有效。
  5. 採用不透明 Refresh Credential 後,單純 JWT signing key rotation 只會讓舊 Access Token refresh, 不必終止 Refresh Session;若 DB/Refresh Session store 被清除,則重新登入是正確結果。

Reference servers 以 production-safe default 落實此不變量:沒有 signing key 時一律 fail fast(原 app.security.jwt.allow-ephemeral-key 的 development opt-in 已由 ADR-033 移除,金鑰對只由 /env-config 產生); Refresh Cookie 預設 Secure=true,development HTTP 必須明確 opt out。這些預設不依賴部署人員記得 補上 production override。Refresh rotation 預設 strict,只有 app.env=DEV 可明示 reusable;Cookie 名稱未覆寫時由穩定的 spring.application.name 推導(例如 app-server → APP_SERVER_REFRESH), 避免同一開發 host 上輪流啟動不同 scaffold 專案時互相覆寫 credential。

決策七:框架與應用責任​

層級責任
appfuse-serverSession orchestration 契約、typed rejection、credential 產生/雜湊、store SPI、rotation 原語
app-*-server reference implementationEntity/Repository adapter、HTTP endpoint、Cookie policy、ProblemDetail mapping、產品 TTL
appfuse-web狀態機、single-flight、跨分頁協調、stable type/errorCode classifier 與 legacy 401 warning fallback
app-office/mockupRedux identity projection、登入 UI、reauthentication UI、與 Server/MSW adapter 接線
MSW handlers與真 Server 相同的 header/cookie、status、errorCode 與 rotation 語意

決策八:跨環境 bounded Session profile(2026-10-24 修訂)​

框架維護者於 2026-10-24 接受 Web RFC 與 Server RFC,採方案 A,承接設計人審 20261024-session-cross-runtime-design 第 3 輪通過及已接受的殘留 Session 取捨。本節記錄既有設計的接受落點,沒有新增未經審閱的 runtime 方案。

版本與適用範圍。 兩稿已由 Web 19.0.0-alpha.136 與 Server 4.0.0-alpha.39 配對發布並更新為 implemented;不可變產物指紋、程式/參考 type commit 與驗證範圍見配對發布紀錄。框架交付不代表消費端已升級或啟用。正式 Server 透過受信任組裝明確 opt-in;Vite Mock、Host Mock 與產品 adapter 共同接入協調契約。未 opt-in 者保留本 ADR 原有 remember/idle、Refresh-only factory 與 callback timeout 語意。

主題接受的共同契約
執行模式與共同交付Vite 與 Host Mock 發 opaque Access,正式 Server 保留簽章 JWT;同一 MSW adapter 不依 Vite/Host 自造 Token。Web/Server 可分別發版,產品須等配對 artifact、Host factory、正式 profile 與驗收證據齊備才啟用
固定期限profile 為 non-remembered,初次新認證建立固定絕對 deadline;12 小時只是產品示例。refresh、rotation 與無新 proof 的身分切換不得延長;Access exp 裁切至該 deadline,idle 不得更早。正式端重用 SessionPolicyBinding.validUntil,不能漏傳 binding 或改 browser app 字串繞過
發行與替換同 Cookie scope 再登入或換人成功時,權威 store 交易原子建立新 family、撤銷 Cookie 證明的前一 family 及其全部 Access;失敗 rollback,另一裝置 family 不連帶撤銷。metadata 經框架驗證後映射產品 sessionId/期限,不要求應用解碼 JWT
入口封閉login/issue、refreshSession、代行進出與 pending activateSession 都受同 profile 約束。舊 family 缺 deadline/policyId 不符時,refresh 在發行前 401 refresh-session-invalid 並清相符 Cookie;activation 不重設期限。deprecated JWT refresh(String) 在新 profile 下拒絕,不產生 Access
撤銷權威每次新的互動式資源授權決定查持久 family store,撤銷 commit 後的新請求立即拒絕;不靠可停用 blacklist、active 快取或延遲 replica 放行。接受每請求 lookup 成本與 store outage 回 503、未知缺陷回 500;不誤報 credential 401,不回滾已開始的交易
principal/actorprincipal revoke 匹配 subject 或 actorSubject,使用 application-scoped 不可變 account ID。AuthPrincipal 的 username 預設與 DefaultAuthPrincipal 的隱式 fallback 都不能滿足新 profile;後者必須明示 subject。純 revoke 針對 commit 時既存集合;帳號安全事件須結合 credential/eligibility 變更與最後發行的交易式版本/鎖檢查
Token 分類正向 sessionKind 區分 interactive 與 client-credentials;interactive 必須具有效 family。舊無 marker Token 退出,M2M 重取但不進 Refresh;Basic/API key/FEDERATED 不屬此 Session 撤銷保證

本輪實作審閱補正持有證明:replacement 與 Cookie-only logout 都以結構合法 Cookie 內的高熵秘密 selector 定位 family(legacy 無 selector 時要求完整 credential hash 相符)。因此目前 verifier、已輪替的舊 verifier、同秘密 selector 但未見過的 verifier,在這兩種操作都證明持有同一 family;expectedFamilyId 只做 fence,公開 family UUID 本身不是證明。新 proof 完成且新 family 建立成功後才撤舊,失敗須 rollback。refresh 另要求 current verifier;帶同秘密 selector 的非目前 verifier 仍走 replay 拒絕/撤銷,不能取得新 Access。

Cookie 協調與 drain。 Web 公開 SessionCoordinator 統一處理 issue、refresh、revoke、identity-change;同 scope 全部 context 使用相同 active protocol。MSW 先登記整個 handler,Native 子請求追蹤至 response/body settle;lease 不重入 refresh 鎖。排隊取消不送請求;已開始的 UI 取消/response timeout 可先回 availability,但不得以 UI deadline 的 Promise.race 釋放 Web Lock。協調器必須等產品請求、handler 與 Native 子請求全部 drain 才釋鎖,取消後不得再開新的 Cookie 子請求。此規則僅在 opt-in profile 取代原 callback 逾時釋鎖策略,原基線的已完成測試不作為新 drain 已驗證的證據。

family fence 與恢復。 排隊時快照 expected family;有效 credential 與期望 family 不符時回 409 session-changed,不 rotation、不撤銷、不 Set-Cookie。真正 credential replay 仍優先走原安全政策。回應套用與 401 重放亦須比對 family,不能把舊請求改用新登入者 Token 重送。已確認 family 的 reconcile 帶 fence;只有沒有 expectation 且沒有 orphan 的乾淨狀態可無 fence bootstrap。owner 終止後保存的非機密 expectation/generation 用於恢復,不保存 credential、subject 或 user。

恢復結果新 profile 的前端出口
已確認 family 的 refresh 401 refresh-session-invalid 或 409 session-changedreauthentication-required,保留草稿,不自動採用另一 Cookie 身分
乾淨 bootstrap 無 Cookie 或 refresh 401 refresh-session-invalidsigned-out
任一 bootstrap 的 409 session-client-upgrade-requiredupgrade-required,保留 expectation/草稿,不當成重新認證、不循環 reload
unconfirmed/orphan 且沒有已確認 family不做無 fence bootstrap;使舊排隊意圖失效,允許新的 proof login,不永久封鎖 scope
transport/timeout/5xx 或 live owner 仍在 drainavailability,保留 expectation;不因逾時清成功狀態或平行修改 Cookie

殘留 Session 是已接受的取捨。 reauthentication-required 不等於 logout。unconfirmed/orphan 時 Cookie/family 可能仍有效,清除 site storage 後可經乾淨 bootstrap 恢復;UI 不得宣稱已安全登出。共享裝置須完成 known-family logout 或受保護 principal revoke。首版只提供新 proof replacement,沒有獨立 Cookie-only discard;兩者在 Cookie 證明撤銷 family 的權限上沒有額外差異,這是操作範圍取捨,不是宣稱 discard 本質不安全。審閱者已於第 3 輪接受此代價,詳兩稿 Decision Log。

遷移與版本隔離。 同 Cookie scope 只允許一個 active protocol(首版 1);所有 Cookie mutation controller/Native bridge 在 proof 消耗與副作用前驗 X-Appfuse-Session-Protocol,缺少/不符回 409 session-client-upgrade-required 且零 mutation。此 header 不是 credential。舊 bundle 只認 refresh 401,收到 409 可能仍保留 Session/舊畫面、操作持續失敗,不保證有更新提示;部署須安排人工重載/關閉舊頁,或先交付可提示的過渡版。只換 Server gate 不能撤回切換前已在途的 Set-Cookie;仍依 drain、orphan 與 family fence 處理。same-origin、非機密 storage 與跨頁 Web Locks 是支援前提;single-context 明確不支援多分頁,DEV reusable 不是 strict 驗收替代品。


實作順序 (Implementation Slices)​

Slice 1:先消除 false positive,維持現行 token 形狀​

  1. Server refresh 只把明確 credential rejection 映成 401 refresh-session-invalid;未知例外保留 5xx。
  2. ProblemDetailFactory 與 auth entry points 統一提供 stable type/errorCode。
  3. Framework 匯出 Refresh failure classifier;stable refresh-session-invalid 回 null,無 code 的 legacy 401 記 warning 後亦回 null 以避免無出口;transport/timeout/5xx 原樣拋出。 尚未升級到含 classifier 版本的 reference app 可保留等價、明確標記的 rollout shim;framework 發版並完成依賴升級後即移除,不把它視為第二個 policy SoT。
  4. MSW 與真 Server 契約對齊:bare X-Refresh-Token、查無 subject 仍回同形 401、不洩漏原因。
  5. 將 SessionExpiredModal 的事件與使用者文案改為 Reauthentication Required 語意。

此 slice 可獨立交付並直接修正目前問題,不需要先固定 seed ID,也不需要先完成 Refresh Session store。

Slice 2:建立 Refresh Session capability​

  1. 定義 RefreshSessionStore、atomic rotate/revoke 契約與 typed result。
  2. Reference implementation 建立 app-owned persistence adapter 與過期清理。
  3. 登入引擎以 SessionOptions 簽發 Access Token + opaque Refresh Credential。
  4. 將 refresh/logout 從 JWT blacklist 逐步遷移到 Session family;Access Token blacklist 僅保留必要的 即時撤銷用途。

Slice 3:Cookie、remember 與所有登入入口​

狀態:已完成(2026-08-11;產品 API/US/SBE 已在同一變更集收斂)。

  1. Browser 改用 HttpOnly Cookie,Access Token 改為 memory-only。
  2. 密碼、OTP、OIDC、signed-link 與 reauthentication 全部傳遞共同 Session policy。
  3. 修訂 US-015 與相關 API/SBE,使其不再與 US-001 衝突。
  4. 實作 30 天滑動 idle expiry 與 non-remembered session cookie。

Slice 4:rotation、replay 與多分頁​

狀態:已完成(2026-08-11;限本 ADR 宣告的單一 Browser origin、Web Locks 可用拓樸)。

  1. Web client 加跨分頁 refresh 鎖與完成通知。
  2. Server 啟用 strict compare-and-rotate、replay detection 與 Session family revoke。
  3. 驗證並行 refresh、response 遺失、舊 credential replay、跨節點 store 一致性。

目前鎖定 registry appfuse-web alpha.73 的 reference apps 保留明確標記的 Web Locks rollout shim; shim 會以 requiresReauthenticationAfterRefresh export 做 capability detection,在新版 framework 存在時自動停用,避免同名 Web Lock 不可重入死鎖。framework 發布並完成 /upgrade-appfuse-web 後, 仍必須與 Slice 1 classifier shim 一起刪除,避免形成第二個長期 policy SoT。

若 navigator.locks 不存在,client 會發出一次診斷 warning,但只剩單分頁 best-effort refresh;strict rotation 的多分頁安全性不在支援範圍內。此時不得把 warning 解讀成已完成跨分頁協調,也不得把 合法競態造成的 family revoke 宣稱為自然 Session expiration。採 fail-closed 會讓單分頁也無法 refresh, 因此目前以 secure-context deployment invariant + observable warning 管理,而不是把它誤分類成 reauthentication。

Slice 5:移除相容層與文件收斂​

  1. 觀察期後移除 Web Storage Refresh Token 與 X-Refresh-Token legacy path。
  2. 更新 auth guide、OpenAPI、Bruno、E2E、MSW 與錯誤碼參考表。
  3. 刪除不再使用的「固定 seed ID 以延續 token」方案;保留 seed 冪等性測試。

Slice 6:跨環境 bounded profile(框架已配對發布,待應用採用)​

狀態:設計已接受,框架 runtime 實作與測試已通過人審,Web/Server 配對 artifact 已發布;應用採用待完成。 既有 Slice 3/4 的完成不包含本 slice。

  1. Server 補 Host factory、bounded issuance/metadata、replacement、fence、family filter/store 能力及兩種參考 type 接線。
  2. Web 補 Vite Native 與公開 coordinator/bridge、完整 handler/Native drain、reconcile、protocol gate 及 HTTP client family guard。
  3. Vite/Host/正式 Server 執行相同期限、換人、撤銷與 Cookie 案例;另驗 DefaultAuthPrincipal fallback、pending/deprecated 入口、account 事件並行邊界及新舊 bundle。
  4. 配對發布 Web 19.0.0-alpha.136/Server 4.0.0-alpha.39 並記錄參考 type/commit、設定與實際驗收證據;齊備後產品才可啟用。框架實作與配對 artifact 發布已完成,兩稿已由 accepted 更新為 implemented;產品啟用仍須自己的接線與驗收。

驗證矩陣​

場景Server 結果前端預期
Access Token 正常到期一般 API 401;refresh 200不顯示錯誤,重放一次
Refresh credential 到期/撤銷401 + refresh-session-invalidreauthentication-required
Gateway/舊版 Server 回無 code Refresh 401401 + unknown problemwarning + reauthentication-required,不得靜默卡住
Refresh DB/cache 暫時失敗503 + stable code保留 Session,顯示 Server unavailable
Refresh 未預期 NullPointerException500 + correlation ID保留 Session;不得顯示 Session Expired
一般 API 權限不足403 + insufficient-permission顯示權限不足,不 refresh
三個同分頁請求同時遇到 401一次 refresh三請求各最多重放一次
兩個同源分頁同時需要 refresh跨分頁序列化;各自依序取得 access token,共享 Cookie 最後停在最新 generation不誤判 replay;N 個未共享 memory token 的分頁可產生 N 次連續 rotation
DEV 的兩個不同 port 前端同時 refresh(reusable)兩次原子 credential-use 都可依序成功;credential 不輪替兩邊取得 access token,不發生 replay revoke;不得把此模式帶到 SIT/production
未 opt-in profile 的 refresh/proxy half-open 超過 callback deadline既有 client abort callback 並釋放 origin lock;排隊期限獨立計算timeout availability;保留 Session、不要求重新認證、不列入業務 mutation 對帳
舊 Refresh Credential 真正重放Session family revoke + audit下一次 refresh 明確要求重新認證
rotation 成功但 response 遺失Cookie 已落地則可用新 generation 重試;未落地而重送舊 credential 則依 replay policy 撤銷 familytransport error 當下保留 Session;確認 replay 後才要求重新認證
remembered 第 29 天成功 refreshrotation + idle expiry 往後 30 天重開瀏覽器可恢復
non-remembered 關閉瀏覽器session cookie 消失回完整登入
僅 Server signing key rotation舊 access 401;refresh 200Session 延續
開發 DB/Refresh store 重建refresh 401要求重新登入,但不宣稱自然到期
Account 刪除後以同 username 重建舊 subject 查無,refresh 401不轉綁新 Account
重新認證帳號的 Email 與 username 不同password login 仍以 current username 驗證保留原 route/表單,不以 Email 取代登入身分
explicit logout request 失敗Session family 與 HttpOnly Cookie 可能仍有效保留本機登入狀態並提示重試,不 reload 後自動恢復登入
navigator.locks 不存在strict rotation 契約不變發 diagnostic warning;多分頁拓樸不受支援,部署不得宣稱符合本 ADR baseline
M2M Access Token 到期client 重新執行 client_credentials不進互動式 reauthentication UI

驗證證據​

決策八/Slice 6 已新增獨立執行證據:Web coordinator/HTTP lease/Native 單元測試、Server security 測試、兩個參考 type 的 SQL 交易與跨 JVM 重啟測試,以及 Vite Mock/Host Mock/正式 JWT 三環境 Browser 契約測試。共同輸入與生命週期案例位於 test-contracts/mock-access-sessions.json,重播入口為 test-contracts/verify-session-contracts.py;精確命令、輸出及限制隨實作審閱 20261024-session-runtime-implementation 核對。實作人審已通過(20261024-session-runtime-implementation/r3-e496f38f4c412908,ada@leandev.io,2026-10-25T01:08:33Z);5 條意見均閉合,最終 security 197 與 Browser 24 通過,兩種參考 Server 完整標準回歸各保留 8/5 個 skipped case。配對 artifact 已發布(見發布紀錄),參考應用仍未啟用新 profile;Browser fixture 也不代表產品 OTP、資料庫或 UAT 已通過。

以下保留既有基線的驗證證據,不能以既有 callback timeout 測試代替新 drain、fence 與即時撤銷的執行證據。

  • Framework http-client.spec.ts:同分頁 single-flight、同源 Web Lock 序列化、queue/callback timeout、 stable/legacy 401 classifier,以及 refresh timeout 不產生 request/outcome-unknown。
  • Reference server RefreshSessionPersistenceStoreIT:atomic rotate 與並行競態; RefreshTokenAuthorityIT 明確模擬「rotation 已提交但 response 遺失」,驗證舊 credential 重送後撤銷 family;tenant 與 tenantless variants 都執行相同契約。
  • Reference web auth-session-lifecycle.test.tsx:重新認證使用 username(Email 不同時仍正確),以及 logout transport failure 保留 current user、允許使用者重試。
  • Reference server AuthInfrastructureConfigTest 與 RefreshSessionCookieTest:缺 signing key fail fast、 development 臨時 key opt-in 與 production-safe Secure Cookie default。

權衡與後果 (Trade-offs & Consequences)​

正面​

  • Access Token、Refresh Session、Server availability 與 Account identity 不再混為單一「session expired」。
  • 既有基線只有 Server 可確認的 Refresh Session 拒絕會觸發重新認證;legacy 無 code 401 另有可觀測的 warning fallback,消除 catch-all 401 false positive 並避免無出口狀態。
  • 具備滑動 Remember Me、rotation、replay detection、即時 revoke 與安全 key rotation 的共同基礎。
  • Error contract 可供前端穩定分支,揭露程度由政策決定,內部原因維持可觀測。

代價​

  • 從純 JWT 增加有狀態 store、清理工作與跨節點一致性需求。
  • HttpOnly Cookie 需要 CSRF 與部署拓樸設計,不能只替換 header。
  • Strict rotation 會把跨分頁競態變成安全問題,因此 client coordination 與 Server atomicity 必須一起交付。
  • 現有 US、guide、mock 與 E2E 有多處矛盾,需要一次性收斂。

風險與緩解​

風險緩解措施
切換 Cookie 時破壞既有 client以版本化相容期保留 header path,觀察後移除
Session store 成為可用性依賴多節點共享、health/metrics、503 語意與保留 client state
Cookie 帶來 CSRFSameSite、Path 收窄、Origin/CSRF token、只允許 POST
Rotation 造成合法競態同分頁 single-flight + 跨分頁鎖 + Server atomic compare-and-rotate
開發多 origin 無法共用 Web Lock僅 app.env=DEV 明示 reusable;非 DEV fail fast;正式環境維持 strict 不變量
Web Locks 因非 secure context/browser capability 缺席production 必須使用 secure context;runtime warning 可觀測;缺鎖的多分頁拓樸不列為支援基線
最大揭露的錯誤碼與訊息揭示帳號狀態參考實作以技術透明度為預設;有遮蔽需求的應用設定 app.api.error-disclosure.mode=minimal,精確原因仍寫 audit

相關文檔 (References)​

內部文檔​

外部標準​


Open Questions​

  1. Non-remembered Refresh Session 的 Server 清理期限需要產品/維運共同選定;Browser 可見語意仍是 session cookie 隨工作階段結束。
  2. Header → Cookie 相容期長度與是否需支援非瀏覽器的互動式 client,需由 API consumer inventory 決定。

變更歷史 (Change Log)​

日期變更內容變更者
2026-10-25Web alpha.136/Server alpha.39 配對 artifact 發布並核對遠端指紋;兩 RFC 更新為 implemented,Slice 6 框架交付完成、應用採用另行追蹤AI Assistant(發布)
2026-10-25框架實作 peer-review 20261024-session-runtime-implementation 第 3 輪 r3-e496f38f4c412908 人審通過;5 條意見閉合。補同 family reconcile、秘密 selector 持有證明、legacy 回歸與 Bearer/API key/Basic 同鏈邊界;Slice 6 待配對發布與應用採用,兩案維持 accepted審閱者 ada@leandev.io;AI Assistant(結案落點)
2026-10-24Slice 6 runtime 草稿、共同案例、三環境 Browser 與兩種 SQL/重啟證據落地;以 20261024-session-runtime-implementation 獨立送實作人審。兩案維持 accepted,尚未發布或啟用參考應用AI Assistant(作者)
2026-10-24接受兩份 Session RFC 並完成 FU-151:新增 opt-in bounded profile,明定 drain 局部取代 callback 逾時釋鎖、共同期限/撤銷/replacement、fence/reconcile、已接受殘留 Session、protocol 遷移與配對交付;Slice 6 待實作,未 opt-in 基線保留框架維護者(使用者)+ AI Assistant
2026-08-12補充開發限定 reusable refresh 協定、非 DEV fail-fast、application-name Cookie 隔離,以及 production strict 拓樸不變量Development Team + AI Assistant
2026-08-11收斂產品 API/US/28 個 SBE、補上 username reauthentication、logout failure、production fail-fast 與 Slice 3/4 驗證證據;明確標示缺 Web Locks 的拓樸不受支援Development Team + AI Assistant
2026-08-11補強 Slice 4:rollout shim capability detection、分離 lock/callback deadline、缺 Web Locks 診斷、單一 Browser origin 部署不變量,並修正多分頁 generation 與 internal refresh timeout 語意Development Team + AI Assistant
2026-08-11完成 Slice 4:Web Locks 跨分頁序列化、秘密 selector/verifier、strict atomic rotation、replay family revoke 與並行 store 驗證Development Team + AI Assistant
2026-08-11完成 Slice 3:HttpOnly Refresh Cookie、memory-only access、remember policy、所有互動式登入入口、滑動 idle 與 impersonation family actor contextDevelopment Team + AI Assistant
2026-08-11完成 Slice 1/2:錯誤契約、opaque credential、持久 Session family、atomic primitive、logout revoke;strict rotation 依 rollout gate 留待 Slice 4Development Team + AI Assistant
2026-08-11初版:統一 Session 狀態機、錯誤 taxonomy、rotation、remember 與重啟語意Development Team + AI Assistant

文檔維護者: Development Team + AI Assistant 最後審閱: 2026-10-24(決策八承接兩稿已通過的設計審閱與維護者接受)