ADR-030: 互動式 Session 生命週期、Refresh Rotation 與錯誤契約
ADR 編號: 030 狀態: 已接受,分階段實作中 (Accepted) 決策日期: 2026-08-11 決策者: Development Team 取代: 無 被取代: 無
2026-08-21 錯誤揭露修訂:
refresh-session-invalidstable 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 暴露的不是單一例外映射錯誤,而是四個未被共同建模的概念:
- Access Token 失效不等於 Refresh Session 失效。
- Refresh 基礎設施失敗不等於使用者需要重新登入。
rememberMe究竟是前端 storage 選擇,還是 Server Session 政策,規格與實作互相矛盾。- Account 身分、Server 簽章金鑰與 Refresh Session 各有獨立生命週期,不能靠固定 seed UUID 混成一件事。
現況走查
| 層級 | 現況 | 風險 |
|---|---|---|
| Server refresh controller | 已知 AuthenticationException 與其他所有 Exception 都映成 401 | DB、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 行為會漂移 |
| Mock | Refresh header 的 bare token 可能被當作 Bearer header 解析;查無舊 subject 時回 404 | Mock 與 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 | 證明目前請求代表哪個 principal | Spring Security context |
| Access Token | 15 分鐘短效 Bearer JWT;可重建、不可續命 | JWT 簽章與 claims |
| Refresh Session | 可撤銷、可輪替、有閒置期限的互動式登入狀態 | RefreshSessionStore |
| Refresh Credential | Browser 持有、用來證明 Refresh Session 的一次性高熵 secret | HttpOnly Cookie + Server hash |
| Remember Me | Session issuance policy,不是身分屬性 | 登入/exchange request + Refresh Session |
| Reauthentication required | Refresh Session 已被 Server 明確拒絕,需再次證明使用者身分 | Refresh API problem type |
「Access Token expired」只代表應嘗試 refresh;「Session expired」只能描述 Refresh Session 的特定 拒絕原因。UI 的通用狀態與元件應命名為 Reauthentication Required,避免把 revoke、帳號刪除、 credential replay 或開發環境重設都誤稱為 expiration。
決策二:前端狀態機
必須遵守以下轉移:
| 事件 | Session state | Token storage | UI 行為 |
|---|---|---|---|
一般 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>相同,供現有 TypeScriptErrorResponse直接消費;新 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:
| 情境 | HTTP | errorCode | Client 決策 |
|---|---|---|---|
| 登入欄位無效 | 400 | validation-error | 留在表單並標示欄位 |
| 帳密錯、tenant 不符、帳號不存在或不可互動登入 | 401 | 最大揭露為 bad-credentials、username-not-found 或實際帳號狀態;最小揭露為 invalid-credentials | 顯示 policy 選定的原因;client 不以 detail 分支 |
| 產品明定的暫時鎖定 | 423 | login-temporarily-locked | 顯示可重試時間;不建立 Session |
| 登入限流 | 429 | authentication-rate-limited | 依 Retry-After 等待 |
| 受保護資源未帶 credential | 401 | authentication-required | 有本地 Session 才嘗試 refresh |
| Access Token 無效、過期或被撤銷 | 401 | access-token-invalid | 嘗試 refresh;尚不宣告 Session 失效 |
| 已認證但權限不足 | 403 | insufficient-permission | 不 refresh、不清 Session |
| Refresh credential 缺少、無效、過期、被撤銷、replay、subject 不存在或不可用 | 401 | refresh-session-invalid | 正常契約中唯一觸發 reauthentication-required 的回應 |
| Refresh 依賴暫時不可用 | 503 | authentication-service-unavailable | 保留 Session,進入 reconnect |
| 未預期 Server 錯誤 | 500 | internal-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 至少保存:
| 欄位 | 用途 |
|---|---|
sessionId | Session family 穩定識別 |
subject | AuthPrincipal.subject();Account row 生命期內穩定 |
tenantId | 多租戶查核與 audit |
credentialHash/credentialSelectorHash/generation | 秘密 selector 定位 family、verifier hash 原子驗證與輪替;不保存明文 credential |
remembered | Session policy |
idleExpiresAt | Remember Me 滑動閒置期限 |
createdAt/lastRefreshedAt | audit 與清理 |
revokedAt/revokeReason | logout、安全事件與 replay 撤銷 |
version | compare-and-rotate 併發控制 |
production-safe 的 strict 模式下,Refresh 成功必須在單一原子操作中完成:
- 以舊 credential hash 找到 Session 並鎖定/compare version。
- 檢查未撤銷、未逾期,並以
AuthPrincipalLookup重載目前帳號狀態與權限。 - 產生新高熵 credential,只保存新 hash;舊 credential 失效。
- Remembered Session 將
idleExpiresAt自本次成功 refresh 往後推 30 天。 - 回傳新 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 OTP | verify request 的 rememberMe,或建立 challenge 時由 Server 保存;兩者擇一後全程一致 |
| OIDC | initiation 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。
決策六:身分持久化與重啟語意
Account.id是該 persisted row 的身分,建立後穩定;刪除再建立是新身分,舊 Session 不得依 username 或 seed 名稱自動轉綁。- Seed 必須以業務 natural key 做冪等查找,讓既有資料庫中的 row 保留原 ID。只有產品明定的 bootstrap identity 才能使用指定 ID;「希望開發重啟後不用登入」不是足夠理由。
- 正式/測試環境必須使用持久化、可共享的 JWT signing keys 與 Refresh Session store;缺少必要設定 應 fail fast,不得默默產生每個 instance 不同的臨時狀態。
- 零設定開發模式可以使用臨時 key/store,但啟動時必須明確警告「重啟或重建資料後需重新登入」。 這是 environment reset,不應以「Session expired」呈現,也不以固定 seed UUID 假裝 Session 仍有效。
- 採用不透明 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-server → APP_SERVER_REFRESH),
避免同一開發 host 上輪流啟動不同 scaffold 專案時互相覆寫 credential。
決策七:框架與應用責任
| 層級 | 責任 |
|---|---|
appfuse-server | Session orchestration 契約、typed rejection、credential 產生/雜湊、store SPI、rotation 原語 |
app-*-server reference implementation | Entity/Repository adapter、HTTP endpoint、Cookie policy、ProblemDetail mapping、產品 TTL |
appfuse-web | 狀態機、single-flight、跨分頁協調、stable type/errorCode classifier 與 legacy 401 warning fallback |
app-office/mockup | Redux identity projection、登入 UI、reauthentication UI、與 Server/MSW adapter 接線 |
| MSW handlers | 與真 Server 相同的 header/cookie、status、errorCode 與 rotation 語意 |
實作順序 (Implementation Slices)
Slice 1:先消除 false positive,維持現行 token 形狀
- Server refresh 只把明確 credential rejection 映成
401 refresh-session-invalid;未知例外保留5xx。 ProblemDetailFactory與 auth entry points 統一提供 stabletype/errorCode。- Framework 匯出 Refresh failure classifier;stable
refresh-session-invalid回null,無 code 的 legacy401記 warning 後亦回null以避免無出口;transport/timeout/5xx原樣拋出。 尚未升級到含 classifier 版本的 reference app 可保留等價、明確標記的 rollout shim;framework 發版並完成依賴升級後即移除,不把它視為第二個 policy SoT。 - MSW 與真 Server 契約對齊:bare
X-Refresh-Token、查無 subject 仍回同形 401、不洩漏原因。 - 將
SessionExpiredModal的事件與使用者文案改為 Reauthentication Required 語意。
此 slice 可獨立交付並直接修正目前問題,不需要先固定 seed ID,也不需要先完成 Refresh Session store。
Slice 2:建立 Refresh Session capability
- 定義
RefreshSessionStore、atomic rotate/revoke 契約與 typed result。 - Reference implementation 建立 app-owned persistence adapter 與過期清理。
- 登入引擎以
SessionOptions簽發 Access Token + opaque Refresh Credential。 - 將 refresh/logout 從 JWT blacklist 逐步遷移到 Session family;Access Token blacklist 僅保留必要的 即時撤銷用途。
Slice 3:Cookie、remember 與所有登入入口
狀態:已完成(2026-08-11;產品 API/US/SBE 已在同一變更集收斂)。
- Browser 改用 HttpOnly Cookie,Access Token 改為 memory-only。
- 密碼、OTP、OIDC、signed-link 與 reauthentication 全部傳遞共同 Session policy。
- 修訂 US-015 與相關 API/SBE,使其不再與 US-001 衝突。
- 實作 30 天滑動 idle expiry 與 non-remembered session cookie。
Slice 4:rotation、replay 與多分頁
狀態:已完成(2026-08-11;限本 ADR 宣告的單一 Browser origin、Web Locks 可用拓樸)。
- Web client 加跨分頁 refresh 鎖與完成通知。
- Server 啟用 strict compare-and-rotate、replay detection 與 Session family revoke。
- 驗證並行 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:移除相容層與文件收斂
- 觀察期後移除 Web Storage Refresh Token 與
X-Refresh-Tokenlegacy path。 - 更新 auth guide、OpenAPI、Bruno、E2E、MSW 與錯誤碼參考表。
- 刪除不再使用的「固定 seed ID 以延續 token」方案;保留 seed 冪等性測試。
驗證矩陣
| 場景 | Server 結果 | 前端預期 |
|---|---|---|
| Access Token 正常到期 | 一般 API 401;refresh 200 | 不顯示錯誤,重放一次 |
| Refresh credential 到期/撤銷 | 401 + refresh-session-invalid | reauthentication-required |
| Gateway/舊版 Server 回無 code Refresh 401 | 401 + unknown problem | warning + reauthentication-required,不得靜默卡住 |
| Refresh DB/cache 暫時失敗 | 503 + stable code | 保留 Session,顯示 Server unavailable |
| Refresh 未預期 NullPointerException | 500 + 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 deadline | client abort callback 並釋放 origin lock;排隊期限獨立計算 | timeout availability;保留 Session、不要求重新認證、不列入業務 mutation 對帳 |
| 舊 Refresh Credential 真正重放 | Session family revoke + audit | 下一次 refresh 明確要求重新認證 |
| rotation 成功但 response 遺失 | Cookie 已落地則可用新 generation 重試;未落地而重送舊 credential 則依 replay policy 撤銷 family | transport error 當下保留 Session;確認 replay 後才要求重新認證 |
| remembered 第 29 天成功 refresh | rotation + idle expiry 往後 30 天 | 重開瀏覽器可恢復 |
| non-remembered 關閉瀏覽器 | session cookie 消失 | 回完整登入 |
| 僅 Server signing key rotation | 舊 access 401;refresh 200 | Session 延續 |
| 開發 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
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 帶來 CSRF | SameSite、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)
內部文檔
- Bearer Token Authentication
- Security 使用指南
- ADR-009:auth 雙模式 resource server
- ADR-017:框架不擁有 Entity
- ADR-018:auth 身分模型
- ADR-023:auth 登入引擎 capability
- ADR-025:M2M credential capability
外部標準
- RFC 9457:Problem Details for HTTP APIs
- RFC 6750:OAuth 2.0 Bearer Token Usage
- RFC 9700:Best Current Practice for OAuth 2.0 Security
- MDN:Web Locks API
Open Questions
- Non-remembered Refresh Session 的 Server 清理期限需要產品/維運共同選定;Browser 可見語意仍是 session cookie 隨工作階段結束。
- 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 context | Development Team + AI Assistant |
| 2026-08-11 | 完成 Slice 1/2:錯誤契約、opaque credential、持久 Session family、atomic primitive、logout revoke;strict rotation 依 rollout gate 留待 Slice 4 | Development Team + AI Assistant |
| 2026-08-11 | 初版:統一 Session 狀態機、錯誤 taxonomy、rotation、remember 與重啟語意 | Development Team + AI Assistant |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-08-12