跳至主要内容

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

ADR 編號: 030 狀態: 已接受,分階段實作中 (Accepted) 決策日期: 2026-08-11 決策者: Development Team 取代: 無 被取代: 無


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 過期。正常契約只有 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前端實作只選 localStoragesessionStorage,不送 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 Token15 分鐘短效 Bearer JWT;可重建、不可續命JWT 簽章與 claims
Refresh Session可撤銷、可輪替、有閒置期限的互動式登入狀態RefreshSessionStore
Refresh CredentialBrowser 持有、用來證明 Refresh Session 的一次性高熵 secretHttpOnly Cookie + Server hash
Remember MeSession issuance policy,不是身分屬性登入/exchange request + Refresh Session
Reauthentication requiredRefresh Session 已被 Server 明確拒絕,需再次證明使用者身分Refresh API problem type

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

決策二:前端狀態機

必須遵守以下轉移:

事件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 作為既有相容欄位。
  • titledetail 是人類可讀文字,可翻譯、可調整,前端不得解析或比對
  • 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-credentialsusername-not-found 或實際帳號狀態;最小揭露為 invalid-credentials顯示 policy 選定的原因;client 不以 detail 分支
產品明定的暫時鎖定423login-temporarily-locked顯示可重試時間;不建立 Session
登入限流429authentication-rate-limitedRetry-After 等待
受保護資源未帶 credential401authentication-required有本地 Session 才嘗試 refresh
Access Token 無效、過期或被撤銷401access-token-invalid嘗試 refresh;尚不宣告 Session 失效
已認證但權限不足403insufficient-permission不 refresh、不清 Session
Refresh credential 缺少、無效、過期、被撤銷、replay、subject 不存在或不可用401refresh-session-invalid正常契約中唯一觸發 reauthentication-required 的回應
Refresh 依賴暫時不可用503authentication-service-unavailable保留 Session,進入 reconnect
未預期 Server 錯誤500internal-error保留 Session,記錄 correlation ID

受保護資源的 401/403 同時維持 RFC 6750 WWW-Authenticateinvalid_tokeninsufficient_scope 是標準 challenge code;ProblemDetail 的 app code 用來驅動本產品行為,兩者不得混用。 M2M token endpoint 繼續使用 RFC 6749 的 errorerror_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
credentialHashcredentialSelectorHashgeneration秘密 selector 定位 family、verifier hash 原子驗證與輪替;不保存明文 credential
rememberedSession policy
idleExpiresAtRemember Me 滑動閒置期限
createdAtlastRefreshedAtaudit 與清理
revokedAtrevokeReasonlogout、安全事件與 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 必須未撤銷且未過期,成功後只更新 lastRefreshedAtidleExpiresAt,回應仍寫回同一 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,不設 ExpiresMax-Age;關閉瀏覽器 工作階段後即無法恢復。Server 仍設定有限的清理期限,避免孤兒資料永久存在。
  • rememberMe=true:Refresh Credential 使用 persistent cookie;Max-Age 與 Server 的 30 天滑動 idle window 對齊,每次成功 refresh 一併更新。
  • Access Token 只存在記憶體;頁面啟動時以 Refresh Cookie bootstrap,不把 access/refresh token 放入 localStoragesessionStorage
  • 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,只有 development/test process 明確設定 app.security.jwt.allow-ephemeral-key=true 才可產生臨時 key; Refresh Cookie 預設 Secure=true,development HTTP 必須明確 opt out。這些預設不依賴部署人員記得 補上 production override。Refresh rotation 預設 strict,只有 app.env=DEV 可明示 reusable;Cookie 名稱未覆寫時由穩定的 spring.application.name 推導(例如 app-serverAPP_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 語意

實作順序 (Implementation Slices)

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

  1. Server refresh 只把明確 credential rejection 映成 401 refresh-session-invalid;未知例外保留 5xx
  2. ProblemDetailFactory 與 auth entry points 統一提供 stable typeerrorCode
  3. Framework 匯出 Refresh failure classifier;stable refresh-session-invalidnull,無 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 冪等性測試。

驗證矩陣

場景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
refresh/proxy half-open 超過 callback deadlineclient 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

驗證證據

  • 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 AuthInfrastructureConfigTestRefreshSessionCookieTest:缺 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 Lockapp.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-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-08-12