第三方登入整合 2.0(Apple 原生;LINE / LinkedIn 延後 v2)Mockup PRD
本 PRD 為後端主導的功能升級規格,前端僅有局部 UI 變更(登入/註冊按鈕、設定頁登入方式區塊),不另行產出 Storybook mockup。
邊界:本文件僅描述「商業規則、流程、資料語義、UI 需求」,不指定 API 路徑、Token 格式、DB schema、provider SDK 介面、併發鎖策略、rate limit 實作層級、audit 信重試機制,那些屬於 RD 的 OpenAPI Spec 與系統設計範圍。
v1 範圍:本期僅交付 Google + Apple 兩種 provider。架構一致:Google 沿用既有「直連 Google OAuth」框架;Apple 為新增功能,亦採「直連 Apple Sign in with Apple」原生路徑(與 Google 同類型架構,token 驗證直接走 Apple JWKS / OIDC,不引入 Firebase Auth 或其他第三方託管層;理由見 §12.2 D4)。LINE / LinkedIn 延後至 v2(理由見 §3.2 與 §12.2 D4-defer)。
FE 互動鎖定:v1 兩個 provider 皆採 popup-only 模式,無 redirect callback 頁面(理由見 §10.3)。
1. 文件資訊
- 文件類型:Mockup 需求規格書(前後端共用視圖)
- 適用對象:後端開發(主要)、前端開發、產品、QA、Auth Provider Compliance
- 最後更新:2026/05/22
- 版本:1.7.0(2026/05/22 — 補入 iOS native app 範圍:確認 v1 同時含 Web + iOS native(Android app 尚未開發,不在 v1 範圍)。影響:(1) Apple ID Token
aud驗證需同時接受 Web Apple Services ID 與 iOS Bundle ID 兩個值;(2) §10.2.1 補 iOS native token 取得分支(ASAuthorizationAppleIDProvider,不使用 popup);(3) §10.3 Navigation Flow 補 mobile native 路徑說明;(4) §8.3 hard gate 補 nonce 強制驗證、rate limit 對驗章失敗請求計入 IP quota、step-up token client error 不消耗(TTL 內可重試)等三項安全細節。商業規則、BR-024~031、cascade 流程完全不變。詳見 §17.7) - 版本:1.6.0(2026/05/22 — Apple 接入方式由 Firebase 改回原生:依
pm_39-apple-auth-decision-brief-for-pm.md(2026/05/20)的決策複審結果,撤回 v1.2.0~v1.5.0「Apple via Firebase」此一架構決定,改為「Apple 直連 Sign in with Apple 原生」。影響:(1) Token Revocation 由「Provider / Firebase / wport」三層收斂為「Provider / wport」二層;(2) Account Deletion Cascade 移除 Firebase AdmindeleteUser步驟(Appleauth/revoke仍保留,App Store 5.1.1(v) 規範不變);(3) 移除 Firebase 後台設定、Firebase SDK 整合相關 sync-fix 任務;(4) Apple 端 token 驗證改為直接打 Apple JWKS(與 Google 同類型),不再經 Firebase Admin SDK;(5) BR-031 Apple S2S Notification 接收能力不變(屬 Apple 平台,非 Firebase 機制)。商業規則、UI、流程、文案、客服 SOP 完全不變。詳見 §17.6) - 歷史版本:
- 1.5.0(2026/05/20 — 資深後端 P0 spec gap 補強:(1) 新增 Apple Sign in with Apple Server-to-Server Notification 接收能力與 4 種 event_type 對應;(2) BR-029 由「wport session invalidation」擴展為「Provider / Firebase / wport」三層 Token Revocation 傳播;(3) BR-012 補上 Account Deletion 對 Firebase Admin
deleteUser+ Appleauth/revoke的 cascade,符合 GDPR / 個資法與 App Store 5.1.1(v) 規範) — 本版 Firebase 相關部分已於 v1.6.0 撤回 - 1.4.0(2026/05/20 — 資深後端 spec-readiness review 收斂:(1) Apple
provider_uid鎖定為 OIDC sub,不存 Firebase UID;(2) Step-up token 三元組綁定 + Firebase ID Tokenauth_time新鮮度 gate;(3) BR-029 session invalidate 範圍補強;(4) 新增 §10.5 錯誤碼 → UI 對應表;(5) 新增 §12.5 帳號分裂客服 SOP;(6)firstBoundAtnullable 與 §12.4 對齊;(7) BR-030 rate limit fail-open 改為 pre/post-verify 分層) - 1.3.0(2026/05/18 — 後端 review P0 收斂:(1) 架構雙軌定案 — Google 沿用既有直連 OAuth、不遷 Firebase;僅 Apple 走 Firebase(v1.6.0 已撤回 Apple via Firebase 此項);(2) FE 互動鎖定 popup-only,移除 callback redirect 路徑;(3) 全文清掃 LINE/LinkedIn 殘留範例)
- 1.2.0(2026/05/18 — scope 收斂:LINE/LinkedIn 延後 v2;新增「為何走 Firebase 而非 Apple 原生 SDK」決策(v1.6.0 已撤回此決策);migration
firstBoundAt推算修正;email normalize 能力補強) - 1.1.0(2026/05/18 — RD code review 收斂:新增 BR-026~030 安全 gate、Firebase Identity Platform OIDC 架構決策(v1.6.0 已撤回 Firebase 部分)、既有 Google 用戶 migration 策略)
- 1.5.0(2026/05/20 — 資深後端 P0 spec gap 補強:(1) 新增 Apple Sign in with Apple Server-to-Server Notification 接收能力與 4 種 event_type 對應;(2) BR-029 由「wport session invalidation」擴展為「Provider / Firebase / wport」三層 Token Revocation 傳播;(3) BR-012 補上 Account Deletion 對 Firebase Admin
- 對應 Storybook:變動項目極小,不另外實作,影響範圍為登入、註冊、個人帳號的登入方式。Apple 按鈕請依 Apple Human Interface Guidelines(“Sign in with Apple” Buttons) https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple ;Google 按鈕沿用現行 wport 樣式
- Storybook 連結:N/A
- 建立日期:2026/05/18
- Coda Backlog Row:
i-R_41yO3CdG(名稱:login 同步) - PM 編號:PM_39
2. 開發進度與設計來源
開發進度
- 前端 PR #:TBD(FE 改動範圍小,列為次要交付)
- 後端 PR #:TBD(主要交付)
設計來源
- Figma 連結:待補(登入頁按鈕、設定頁區塊)
- Pixsole / 既有畫面:登入頁(既有 Google 按鈕模板)、帳號設定頁(既有「個人資料」分頁)
3. 模擬頁面摘要(Mockup Summary)
3.1 功能概述
- 功能名稱:第三方登入整合 2.0(v1:Apple 原生;v2:LINE / LinkedIn 原生)
- 主要使用者角色:所有 wport 使用者(求職者、企業成員)—— 沿用統一帳號設計
- 核心目標(v1):
- 擴充第三方登入選項,從現有 Google 1 種擴充為 2 種(Google + Apple);LINE / LinkedIn 延後 v2
- 建立明確的「唯一信箱原則」帳號模型,避免同一人多個分裂帳號(多 provider 資料模型須一次到位,v2 可直接擴充)
- 實作「絲滑合併」(作法 B),讓跨 provider 使用相同 email 的用戶能無痛切換登入方式
- 提供「防呆解綁機制」,避免用戶不慎將自己鎖在帳號外(幽靈帳號)
- 將既有 Google 用戶 migration 到新的多綁定模型,作為 v2 LINE/LinkedIn 上線的前置
3.2 範圍界定
-
包含範圍(v1 In Scope):
- 平台範圍:Web + iOS native app(Android app 尚未開發,不在本 PRD 範圍;詳見排除範圍)
- 新增 Apple Sign-In(Web 端:Apple Sign in with Apple JS(Web),
usePopup: true;iOS native:ASAuthorizationAppleIDProvider,不使用 popup,直接取得identityToken。後端直接以 Apple JWKS 驗 Apple ID Token;不引入 Firebase Auth 或其他第三方託管層,理由見 §12.2 D4) - 沿用既有 Google 登入流程(直連 Google OAuth;token 驗證路徑與 session 簽發邏輯不變,僅在使用者帳號模型上擴充至多綁定
User Identity Binding,見 §12.4) - Apple 與 Google token 驗證同類型:兩條路徑各自打 provider JWKS(Google JWKS / Apple JWKS)驗 OIDC ID Token;後端在「取得 (provider, provider_uid, email, email_verified)」這層匯流,再進入共用的情境判定(見 §10.2)
- Apple ID Token
aud雙值接受:Web 路徑aud= Apple Services ID;iOS native 路徑aud= iOS Bundle ID;BE 必須同時將兩值列入白名單,並依 client 來源(client_type欄位或 tokenaud自動判斷)分派(見 §8.3) - 新用戶首次登入後的 Onboarding Flow(個人資料填寫,沿用 BR-002)
- 既有用戶以新 provider(Apple)登入時的「自動合併」(作法 B,強制
email_verified=true,見 BR-026) - 帳號設定頁新增「登入方式」區塊(顯示已綁定 provider 清單 + 解綁/新增綁定)
- 解綁時的防呆檢查(不能解除唯一登入方式)
- 解綁、新增綁定的 step-up authentication(重做 OAuth 或重輸 email+password,見 BR-028)
- 解綁後 invalidate 該 provider 維度的所有 sessions(見 BR-029)
- OAuth 登入 endpoint 的 rate limiting(見 BR-030)
- 登入後一次性 Toast 通知(告知用戶 Apple 已自動連結到原帳號)
- 既有 Google 用戶 migration(v1 完成):將
user.google_id既有欄位轉為 User Identity Binding 第一筆;migration 期間採雙寫策略(見 §12.4) - 資料模型一次到位:User Identity Binding 採多綁定 N:1 模型;v1 雖只接 2 provider,schema 與 API 已可直接擴充 v2 LINE/LinkedIn
- Apple 首次授權 email 持久化保存:Apple 規範下 email 僅在首次授權回傳;後端必須在「驗完 token 後 → 寫 binding」這條路徑加上「寫入失敗持續重試到成功(含 DLQ)」之保護,避免單次 DB 故障導致該用戶 email 永久遺失(見 §5.2 能力約束、§10.2 補充說明)
- Apple Sign in with Apple Server-to-Server Notification 接收:訂閱並處理 Apple 端發起的
email-disabled/email-enabled/consent-revoked/account-delete四種事件(見 BR-031、§12.6) - Token Revocation 二層傳播能力:解綁與帳號刪除時,依序處理 Provider 端(Google OAuth revoke / Apple
auth/revoke)、wport 端(session invalidate)二層撤銷(見 BR-029、§12.7) - Account Deletion Cascade:BR-012 帳號刪除流程補上對 Apple
auth/revoke的同步 cascade,符合 GDPR / 個資法與 App Store Review Guideline 5.1.1(v)(見 §12.8)
-
延後範圍(v2,下一期):
- LINE Login:延後理由見 §12.2 D4-defer——LINE email scope 需 LINE 商業審核(時程不可控);無 email 則無法套用 BR-024 唯一信箱原則,會出現「LINE 用戶完全無法合併」的退化體驗
- LinkedIn OAuth / OIDC:延後理由見 §12.2 D4-defer——LinkedIn 雖已支援 OIDC(直接打 LinkedIn issuer + JWKS),但 RD 仍需 spike 驗證實際 scope / profile 行為對齊;v2 啟動前另開 PRD
- LINE / LinkedIn 的 Compliance 檢核(LINE Login terms、LinkedIn Developer Agreement)一併延後
- 註:v2 LINE / LinkedIn 採與 Apple 同類型架構(直連 provider OIDC、不引入第三方託管層),與 v1 共用同一份帳號模型與安全 gate(BR-024~031)
-
排除範圍(Out of Scope,任何期都不收):
- Android app:Android app 尚未開發;Apple Sign-In 亦無官方 Android native SDK。Android 如有第三方登入需求,待 Android app 立項時另開 PRD,架構上沿用本 PRD 的資料模型與 BR-024~031
- Email + Password 登入流程的改動(沿用既有設計)
- 2FA / MFA(屬於另一期帳號安全強化)
- 企業 SSO(SAML / OIDC for Enterprise)—— 屬於企業客製範圍
- 第三方 API Token 簽發(屬於 PM_37 Enterprise API v1 範圍)
- 帳號刪除流程的調整(沿用 BR-012,僅補充第三方綁定一併刪除)
- WeChat / Facebook / Twitter / GitHub 等其他 provider
- 行銷端「以 email 邀請新用戶」流程的改動(BR-007 維持)
3.3 成功條件
- S1:用戶可在登入/註冊頁,以 Google、Apple 任一 provider 完成 sign-up;新用戶能順利進入 Onboarding 完成個人資料
- S2:用戶以相同 email 但不同 provider 登入時,系統自動合併到既有帳號,並顯示一次性 Toast 通知
- S3:用戶可在帳號設定 > 登入方式區塊,看到全部已綁定的 provider,並能新增/解除綁定
- S4:當用戶只剩唯一登入方式時,系統明確阻擋解綁,並顯示解綁限制原因
- S5:Apple Hide My Email 等邊界情境,有明確的 UI 回饋與資料一致性處理
- S6:既有 Google 用戶 migration 完成後無感(不需重新登入、不需重綁)
- S7:本 PRD 不指定後端實作細節(rate limit 實作、併發鎖、idempotency key 構成、audit 信重試機制皆由 RD 在 OpenAPI Spec / 系統設計中決定)
- S8:用戶在 Apple 端撤銷 / 刪除 Apple ID 時,wport 透過 Apple S2S Notification 在合理時間內(PRD 立場:≤ 24h,實際 SLA 由 RD 在系統設計中決定)反映此狀態;用戶刪除 wport 帳號時,外部 provider 端的授權 / refresh token 同步撤銷(PII 不殘留)
4. 商業規則對齊(Business Rules Alignment)
4.1 本功能使用到的業務規則
| 規則 ID | 規則摘要 | 是否關鍵 | 備註 |
|---|---|---|---|
| BR-001 | 第三方登入用戶不需額外 email 驗證(provider 已驗證) | ✅ 是 | 沿用既有;v1 擴充 Apple;LINE/LinkedIn 延後 v2 |
| BR-002 | 第三方登入後仍須完成個人資料才能新增履歷 | ✅ 是 | Onboarding Flow 直接接此規則 |
| BR-003 | 已註冊 vs 未註冊使用者區分流程 | ✅ 是 | 第三方登入觸發 sign-up vs sign-in 判定 |
| BR-012 | 帳號刪除前 Owner 檢查;刪除時一併移除第三方綁定 | ✅ 是 | 沿用;v1 補上 Apple 綁定清除 + Apple auth/revoke(§12.8);v2 再補 LINE/LinkedIn |
| BR-018 | 電子郵件通知規則 | ✅ 是 | 自動合併除 in-app Toast 外,另寄送 audit 通知信(符合「帳號變更須通知用戶」的稽核標準);解綁、新增綁定、Apple S2S 觸發的 provider 端撤銷/刪除亦寄送 audit 信 |
4.2 新增規則建議(待 PRD 收斂後回寫 business-rules.md)
建議 BR-024:唯一信箱原則(Unique Email per Human Identity)
- 描述:一個 email 在 wport 系統中,最多對應到一個使用者帳號。任何第三方 provider 回傳的 email,若已存在於系統,必須合併到既有帳號,不可建立新帳號。
- 適用範圍:所有 sign-up / 第三方綁定流程
- 例外:Apple Hide My Email relay email(
*@privaterelay.appleid.com)視為獨立身份,因為它本來就是匿名化的 unique identifier,與用戶的真實 email 屬於不同實體。 - 設計依據(Design Rationale):
- 選項對比:
- A. 一個 email 可對應多個帳號(鬆散)→ 帳號分裂、客服困擾、無法做忠誠用戶辨識
- B. 一個 email 對應一個帳號(嚴格)✅ 採用
- SaaS 對標:Google Account、Stack Overflow、Linear、Slack 皆採此模型
- wport context:人力銀行場景中,履歷的求職者身份必須與企業端認知的個人身份一致,否則應徵歷史會分裂
- 選項對比:
建議 BR-025:防呆解綁機制(Unlink Safety)
- 描述:使用者在設定頁解除第三方登入綁定時,系統必須確保使用者至少保留一種可用的登入方式(第三方綁定 ≥ 1 種,或 已設定 email+password)。若解綁後將沒有任何登入方式,系統必須阻擋此操作並顯示理由。
- 適用範圍:帳號設定 > 登入方式 > 解除綁定
- 設計依據:
- 選項對比:
- A. 允許解綁到 0 個(用戶責任)→ 產生「幽靈帳號」,客服需介入恢復登入
- B. 至少保留 1 個(系統防呆)✅ 採用
- C. 解綁前強制改用 email+password → 體驗繁瑣,違反「絲滑至上」原則
- SaaS 對標:GitHub、Vercel、Linear 皆採 B
- wport context:用戶經常因「換 LINE 帳號」等生活事件想解綁,但忽略自己沒有其他登入方式
- 選項對比:
建議 BR-026:自動合併必須 email_verified=true(Auto-Merge Email Verification Gate)
- 描述:自動合併(作法 B)只能在 provider 回傳的
email_verified=true時觸發。若email_verified=false,系統必須 block auto-merge,並要求用戶走 email+password 流程(或既有已驗證的第三方 provider)登入後,才能在設定頁手動完成綁定。 - 適用範圍:所有自動合併分支(§10.2 System Flow 的 merge 分支)
- v1 攻擊面狀態:v1 僅 Google + Apple,兩者
email_verified皆恆為 true(Apple Hide My Email 屬獨立身份,不走 merge),實際攻擊面為 0。BR-026 仍須在 v1 實作(hard gate),目的是「v2 加入 LINE 時不需要回頭改 auth 核心」。 - 設計依據(Design Rationale):
- 攻擊情境(v2 適用,v1 dormant):攻擊者以 LINE 註冊新帳號,將 email 填為受害者的
victim@gmail.com(LINE 不強制驗證 email)→ 系統 auto-merge → 攻擊者直接接管受害者帳號 - 選項對比:
- A. 接受未驗證 email 就 auto-merge → 嚴重帳號接管漏洞
- B. 強制
email_verified=true才可 auto-merge ✅ 採用 - C. 一律不 auto-merge,全部走確認流程 → 違反「絲滑至上」原則
- SaaS 對標:Google Account、Auth0、Apple Sign in with Apple、Auth0 Identity Linking 預設皆要求 verified email 才合併
- wport context:帳號接管會直接導致履歷、應徵歷史外洩,屬於最高等級安全風險
- 攻擊情境(v2 適用,v1 dormant):攻擊者以 LINE 註冊新帳號,將 email 填為受害者的
建議 BR-027:新增綁定的 Cross-Account Email 衝突(Cross-Account Email Block)
- 描述:使用者在設定頁新增 provider 綁定時,若該 provider 回傳的 email 已是另一個 wport 帳號的 primary email,系統必須阻擋此次綁定並回傳明確錯誤碼。
- 適用範圍:ACT-7 新增綁定流程
- 設計依據:
- 衝突情境:用戶 A primary email =
a@gmail.com,A 在設定頁綁 LinkedIn,LinkedIn email =b@linkedin.com,但b@linkedin.com已是用戶 B 的 primary email → 若允許綁定,等同兩個 wport 帳號共用b@linkedin.com的識別性,破壞 BR-024 唯一信箱原則 - 選項對比:
- A. 允許綁定(保留 binding 但不影響 primary)→ 破壞唯一信箱原則的反面情境
- B. 阻擋並要求用戶先在另一帳號處理 ✅ 採用
- SaaS 對標:GitHub、Linear 皆採阻擋策略
- 衝突情境:用戶 A primary email =
建議 BR-028:Sensitive Action Step-Up Authentication
- 描述:解綁、新增綁定屬於 sensitive action,除「當前 session 未過期」外,必須額外通過 step-up authentication:
- 新增綁定:天然 step-up(必然重做 OAuth 一次)
- 解綁:必須擇一通過 — (a) 重做該 provider 的 OAuth 流程;(b) 重輸 email + password(限該帳號已設定 password 時可選)
- 適用範圍:ACT-7 新增綁定、ACT-8 解綁
- 設計依據:
- 攻擊情境:攻擊者透過 XSS / 借走未鎖屏裝置取得 session token → 任意改用戶綁定 → 接管帳號
- 選項對比:
- A. 僅 session 有效即可(原 §8.3 立場)→ session 竊取即可接管
- B. Sensitive action 強制 step-up ✅ 採用
- SaaS 對標:GitHub、Linear、Vercel 在解綁 / 新增 SSH key 等操作皆強制 step-up
建議 BR-029:解綁/帳號刪除的 Token Revocation 二層傳播(Token Revocation Propagation)
v1.6.0 修訂:v1.5.0 曾將 BR-029 擴展為「Provider / Firebase / wport」三層傳播,係因當時 Apple 經 Firebase Auth 託管,需多撤銷 Firebase 端 cached state。v1.6.0 Apple 改為原生路徑(不經 Firebase),中介層自然消失,BR-029 收斂為「Provider / wport」二層傳播。
- 描述:使用者解綁某 provider 或刪除 wport 帳號時,系統必須依「Provider 端 → wport 端」二層依序傳播 token revocation:
- L1 — Provider 端撤銷(外部一致性層)
- Google 路徑:呼叫
https://oauth2.googleapis.com/revoke(撤銷 OAuth grant;best-effort,失敗不阻擋) - Apple 路徑:呼叫
https://appleid.apple.com/auth/revoke(撤銷 Apple refresh token;解綁時 best-effort,帳號刪除時必須成功,理由:App Store Review Guideline 5.1.1(v) 強制要求)
- Google 路徑:呼叫
- L2 — wport 端 session invalidation(內部一致性層,沿用原 BR-029 語意)
- 依
Session Provider Index找出該 provider 來源之所有 active sessions + 其派生 child token,全部 invalidate - 必須成功(原 BR-029 既有 hard gate)
- 依
- L1 — Provider 端撤銷(外部一致性層)
- 適用範圍:ACT-8 解綁、ACT-9 帳號刪除、ACT-10 處理 Apple S2S
consent-revoked/account-delete事件 - 失敗處理矩陣:
層 解綁(unbind) 帳號刪除(delete) L1 best-effort + audit log 必須成功(重試 queue;Apple 端受 App Store 5.1.1(v) 強制;Google 端為 GDPR 資料極小化要求) L2 必須成功 必須成功 - 設計依據:
- 攻擊情境(僅清 L2 的漏洞):用戶在 wport 解綁 Apple,但若 wport 內部已切斷而未呼叫 Apple
auth/revoke,Apple 端授權鏈仍存在 → 用戶在 Apple ID 設定看到「wport 仍可登入」會產生混淆;雖然 wport BE 此時查無 binding 會 reject sign-in,但 Provider 端授權狀態殘留違反用戶意願 - 客服情境:用戶在 PC 解綁 Apple 後,手機端以 Apple 登入產生的 session 仍可使用 → 用戶誤以為「解綁失效」而向客服反映
- 合規情境:帳號刪除時若不撤銷 Provider 端 OAuth grant → Provider 端仍持有用戶授權記錄 → 不符合個資法與 GDPR Article 17(被遺忘權)的「通知處理者」要求
- 資料前提:需要 session 紀錄能依「provider 維度」查找(具體實作由 RD 決定)
- 選項對比:
- A. 只清 L2 → wport 內部一致但 Provider 端授權殘留;違反 App Store 規範與 GDPR
- B. 二層皆清 ✅ 採用
- SaaS 對標:Google Account(刪除時連動撤銷所有 OAuth grants)、GitHub(unlink 時呼叫 provider revoke)、Notion / Slack / Linear(皆呼叫 Apple
auth/revoke)
- 攻擊情境(僅清 L2 的漏洞):用戶在 wport 解綁 Apple,但若 wport 內部已切斷而未呼叫 Apple
建議 BR-031:Apple Sign in with Apple Server-to-Server Notification 處理
- 描述:wport 必須提供 Apple S2S Notification webhook endpoint,接收並處理 Apple 端發起的 4 種 lifecycle event:
event_type Apple 端動作 wport 系統行為 email-disabled用戶在 Apple 端關閉 Hide My Email 轉信 標記該 Apple binding 「relay 失效」flag;下次用戶登入時顯示 nudge「請至設定頁更新可收信 email」;不改動 binding 本身 email-enabled用戶重新啟用 Hide My Email 轉信 清除上述 flag consent-revoked用戶在 Apple ID 設定撤銷對 wport 的「Sign in with Apple」授權 觸發 ACT-10:等同 ACT-8 解綁 Apple 路徑(含 BR-029 二層傳播),但不需 step-up(事件來源為 Apple 端 authoritative);若 Apple 是該用戶唯一登入方式 → 不阻擋(外部已 authoritative),改為標記帳號「需客服恢復」並寄通知信(見 §12.5) account-deleteApple ID 被刪除 觸發 ACT-10:移除 Apple binding + 二層傳播;wport 帳號保留(用戶可用其他登入方式取回);寄通知信告知用戶;若 Apple 為唯一登入方式 → 進入「需客服恢復」狀態 - 適用範圍:ACT-10 處理 Apple S2S;§12.6 細節流程
- 安全性要求(PRD 語意層):
- webhook payload 為 Apple 簽發的 signed JWT,必須驗證簽章(透過 Apple JWKS
https://appleid.apple.com/auth/keys) - 必須驗證
iss = https://appleid.apple.com與aud = wport 的 Apple Services ID - 必須以
transaction_id做 idempotency check(Apple 會重試,重複收到同一通知不可重複執行 unbind / cascade) - webhook handler 必須在 ≤ 5 秒內回 200(否則 Apple 視為失敗會重試),重活動排程到後端 queue 異步處理
- webhook payload 為 Apple 簽發的 signed JWT,必須驗證簽章(透過 Apple JWKS
- 設計依據:
- 不接收 S2S 的後果:
- 用戶在 Apple 端撤銷授權 / 刪除 Apple ID → wport 完全無感 → binding 永久殘留 → 違反用戶意願與個資法
- Hide My Email 關閉 → wport 寄信長期失敗 → 用戶誤以為 wport 故障 → 客服爆量
- Apple 平台政策:Apple Developer 規範要求所有支援 Sign in with Apple 的 service「應」訂閱 S2S Notification(雖非強制 enforcement,但帳號治理品質要求)
- 選項對比:
- A. 不接收 → 上述後果,永久 data drift
- B. 接收並處理 ✅ 採用
- SaaS 對標:Notion、Slack、Linear、Discord 皆訂閱 Apple S2S Notification(GitHub issue 與 dev forum 多有提及)
- wport context:人力銀行帳號常綁定真實履歷,binding 與 Apple 端狀態不一致會直接導致「用戶刪除 Apple ID 卻仍收到 wport 通知信」此類隱私事件
- 不接收 S2S 的後果:
建議 BR-030:OAuth Login Rate Limiting
- 描述:所有第三方登入 endpoint 必須有 rate limit,防止帳號探測 / 暴力嘗試:
- per IP:60 次 / 分鐘
- per email(以 provider 回傳的 email 為 key):10 次 / 分鐘
- per provider UID:10 次 / 分鐘
- 觸發 limit 後回 429,window 為滑動窗口;異常 spike(單一 IP > 600 次 / 5 分鐘)應觸發 alert 給 oncall
- 適用範圍:所有 sign-in / sign-up / merge 三合一 endpoint(§8 HINT-MUTATION-1);ACT-7 新增綁定 endpoint 同等
- 設計依據(資深後端視角):
- 攻擊情境:
- 帳號列舉:攻擊者以同 email 大量試登入,藉錯誤碼差異推測「該 email 是否已註冊 wport」→ 洩漏求職者隱私
- Provider token 試錯:對 provider OIDC 端點(Google JWKS / Apple JWKS)暴力打 → 影響 wport 配額
- Credential stuffing:拿外洩 OAuth token 灌水
- 選項對比:
- A. 不設 limit → 帳號列舉攻擊面
- B. 僅 IP 維度 → 攻擊者使用 IP 池 / 雲端 IP 即可繞過
- C. IP + email + UID 三維度 ✅ 採用
- 實作參考:建議使用 Redis-based sliding window(如
redis-cell或rate-limit-flexible),key 為auth:{dimension}:{value};error response 帶Retry-Afterheader - 降級策略(分層):rate limit 服務(如 Redis)不可用時,依檢查位置採不同策略:
- Pre-token-verify 層(per IP)→ fail-open(不阻擋)。理由:擋在 token 驗證之前的最外層 limit 若 fail-closed,會把所有登入直接擋掉,等於登入服務癱瘓;per IP 維度誤殺合法流量風險亦高(企業 NAT 共用 egress IP)
- Post-token-verify 層(per email / per provider UID)→ fail-closed(阻擋並回 503)。理由:此層已知 user 身份,誤殺面有限;且這層才是真正阻擋帳號探測 / credential stuffing 的關鍵,fail-open 等於把 BR-030 的安全價值整個丟掉
- 兩層降級狀態皆須記錄 alert metric,觸發 oncall 介入
- 若 RD 因實作成本選擇單層 limit,PRD 立場:寧可 post-verify fail-closed 也不要 pre-verify fail-closed
- SaaS 對標:Auth0、Cloudflare Access、Okta 皆採多維度 rate limit;GitHub 對 OAuth callback 採 IP+app 雙維度
- wport context:人力銀行帳號常綁定真實身份,暴力探測會洩漏「某 email 是否為求職者」此類隱私訊號
- 攻擊情境:
4.3 補充說明
- BR-001 既有條文「使用第三方註冊(如 Google、Facebook)的使用者不需要額外的電子郵件驗證」中,Facebook 應移除(不在 wport 支援範圍),v1 加入 Apple,v2 補上 LINE/LinkedIn。Sync-fix 清單會列出此項。
- BR-012 既有條文「第三方登入綁定(User Providers):刪除所有第三方登入綁定記錄(Google、Facebook 等)」中:
- 需將 Facebook 改為 Apple;v2 再補 LINE/LinkedIn
- v1.5.0 補強(v1.6.0 修訂):刪除流程須擴充為「對外 cascade」—— 同步呼叫 Provider 端撤銷(Google revoke / Apple
auth/revoke),以符合個資法與 GDPR Article 17(被遺忘權),以及 App Store Review Guideline 5.1.1(v)(Sign in with Apple 帳號刪除時必須呼叫 Apple revoke 並從 server 移除 user record)。v1.6.0 起 Apple 採原生路徑,已不再有 Firebase AdmindeleteUser步驟。詳細 cascade 步驟見 §12.8 - Sync-fix 清單會列出此項
- BR-018 既有條文需補上「帳號變更類事件(自動合併、新增綁定、解綁、Apple S2S 觸發之撤銷/刪除)必須寄送 audit 通知信」。Sync-fix 清單會列出此項。
5. 資料實體與欄位(Data Entities & Fields)
本節僅描述「需要追蹤什麼資料、為什麼」,不指定欄位名、JSON 結構、儲存方式。RD 自行決定。
遵守 prd-gen Rule 30:PRD vs API Spec boundary 為 hard gate。
5.1 實體列表(語意層)
| 實體名稱 | 需要追蹤的資訊 | 為什麼需要 |
|---|---|---|
| User Identity Binding | (使用者, provider 類型, 該 provider 內的唯一識別碼) 三元組;首次綁定時間(允許 null + migrated=true,見 §12.4);最近登入時間 | 支援「一個 wport 用戶 N 個 provider」的多綁定模型;用於登入時辨識用戶;用於設定頁顯示綁定清單 |
| User Primary Email | 每個 wport 使用者帳號對應一個 primary email | 唯一信箱原則的核心 invariant;自動合併時的 lookup key |
| Provider Email at Binding Time | 綁定當下,該 provider 回傳的 email;以及該 email 是否為已驗證 | 用於 audit;用於辨識「provider email 是否與 wport primary email 一致」;用於 Apple Hide My Email 的特殊判定 |
| Login Method Diversity | 使用者目前共有幾種可用的登入方式(綁定數 + email/password 是否已設定) | 解綁防呆檢查的判斷依據(BR-025) |
| Merge Event Log | 自動合併事件:時間、舊綁定、新綁定、觸發 provider | 用於 Toast 顯示、客服回溯、安全稽核 |
| Session Provider Index | 每筆 active session 對應的 provider 來源(哪一種 provider 登入產生此 session) | 解綁時 invalidate 該 provider 維度的所有 sessions(BR-029) |
| Auth Audit Notification Log | 帳號變更事件(合併、新增綁定、解綁)的 audit email 寄送紀錄 | 對應 BR-018;用於客服回溯「用戶是否收到通知信」 |
| Step-Up Verification Token | sensitive action(解綁、新增綁定)通過 step-up 後的一次性短效 token;必須綁定 (user_id, action, target_provider) 三元組(見 §5.2 能力約束) | 對應 BR-028;避免「session 仍有效但 step-up 未完成」就執行操作;避免一份 token 跨 action / 跨 provider 重用 |
| Apple Provider Lifecycle Event Log | Apple S2S Notification 處理紀錄:transaction_id(idempotency key)、event_type、原始 payload(已驗章)、處理狀態(pending / done / failed)、對應的 wport user_id(若 lookup 成功)、處理時間 | 對應 BR-031;用於 idempotency 去重、處理失敗重試、客服回溯「用戶聲稱已刪 Apple ID 為何 wport 帳號還能收到通知」此類爭議 |
| Revocation Propagation Job | 二層 revocation(L1 Provider / L2 wport)的執行狀態追蹤:所屬 user_id、provider、觸發來源(unbind / delete / s2s_event)、各層執行結果(success / pending / failed / not_applicable)、重試次數、最終完成時間 | 對應 BR-029;用於確保「L2 必須成功、delete 情境 L1 亦必須成功」之最終一致性;用於客服查證「帳號刪除是否已對外完成 cascade」(GDPR 舉證需要) |
| Apple Relay Email Status | 該 Apple binding 的 relay email 目前是否仍可轉信(active / disabled);最近狀態變更時間(由 S2S email-disabled / email-enabled 事件驅動) | 對應 BR-031;用於登入後 nudge 用戶更新 primary email;避免 wport 持續寄信到無效 relay |
Provider UID 識別主鍵語意
本節為 PRD 語意層的硬約束:哪個欄位作為「provider 內的唯一識別碼」屬於資料模型語意,影響到 v2 是否能無痛擴充至 LINE / LinkedIn 等其他 provider,因此寫入 PRD(DB 欄位名稱、index 仍由 RD 決定)。
| Provider | wport DB provider_uid 採用值 | 取得來源 | 不採用值(明確排除) |
|---|---|---|---|
| Google(直連 OAuth) | Google OIDC sub | Google JWKS 驗證後的 ID Token sub claim | 無 |
| Apple(直連 Sign in with Apple) | Apple OIDC sub | Apple JWKS(https://appleid.apple.com/auth/keys)驗證 Apple ID Token 後的 sub claim | 無(v1.6.0 已移除 Firebase 路徑;歷史版本曾規定「禁存 Firebase UID」,因 Firebase 已不在架構中,此排除條款不再適用) |
理由:
- Apple sub 是 OIDC 規範下的 stable identifier,跨 vendor / 跨架構皆不變
- v2 LINE / LinkedIn 將採同類型架構(直接以 provider 自己的 OIDC issuer + JWKS 為驗證來源),仍以各自的 OIDC
sub為provider_uid
假設(外部 invariant):本 PRD 假設 Google / Apple 的 OIDC sub 在帳號生命週期內 immutable。若 provider 後續變更 sub(極少數帳號合併場景),屬於「外部 invariant 破壞」,採客服介入路徑(見 §12.5)。
5.2 實體欄位定義
本期不在 PRD 提供 TypeScript interface,請 RD 在 OpenAPI Spec 中定義並回覆 PM 確認。
PRD 僅約束「必須能追蹤的資料項目」,命名、型別、欄位順序均由 RD 決定。
約束(必須能達成的能力):
- 系統能在「給定 provider + provider 內 UID」時,找到對應的 wport 使用者
- 系統能在「給定 email」時,找到是否有 wport 使用者已使用此 email(用於合併判定)
- 系統能列出「某使用者目前綁定了哪些 provider」(用於設定頁)
- 系統能判定「某使用者目前還剩幾種可用登入方式」(用於 BR-025 解綁防呆)
- 系統能在用戶刪除帳號時,一併刪除所有 provider 綁定(BR-012 補充)
- 系統能在 provider 回傳
email_verified時保存此狀態,並在 merge 判定時讀取(BR-026) - 系統能在「給定 email + 排除某 user_id」時,判斷此 email 是否為「他人」的 primary email(BR-027 cross-account 衝突檢查)
- 系統能列出「某使用者所有以特定 provider 建立的 active sessions」(BR-029 解綁時 invalidate)
- 系統能簽發一次性短效 step-up token,並驗證後消耗(BR-028);token 必須綁定
(user_id, action, target_provider)三元組,使一份 token 無法跨 action / 跨 provider 重用 - 系統能在「Apple / Google ID Token 用於 sensitive action 之 step-up 證明」時,驗證該 token 的
auth_timeclaim 距今 ≤ 5 分鐘,以鎖死「重做 OAuth」的新鮮度(避免 client-side SDK 從既有 session 直接 reissue 老 token 即視為通過 step-up)。auth_time為 OIDC 標準 claim,Apple / Google 兩條路徑同此規範 - 系統能在「Apple 首次授權回傳 email 後寫入 binding」這條路徑加上失敗持久化保護:寫入 DB 失敗時必須排入 retry queue 持續重試(含 DLQ),不可放任單次失敗導致該 user email 永久遺失(Apple 第二次起不再回傳 email;見 §10.2 補充說明)
- 系統能對「IP / email / provider UID」三個維度執行 sliding window rate limit(BR-030)
- 系統能接收 Apple Sign in with Apple Server-to-Server Notification webhook,驗證 JWT 簽章(Apple JWKS)、
iss、aud,並以transaction_id達成 idempotency;webhook handler 必須在 ≤ 5 秒內回 200,重活動排到 queue 異步處理(BR-031) - 系統能依 Apple S2S
event_type分派處理:email-disabled/email-enabled僅更新 relay 狀態;consent-revoked/account-delete觸發 BR-029 二層 revocation propagation(BR-031) - 系統能執行 Token Revocation 二層傳播,並追蹤每層完成狀態:L1(Provider 端 revoke)、L2(wport session invalidate);L2 失敗時必須排程重試到成功,L1 失敗策略依觸發來源(unbind 為 best-effort、delete 為必須成功)區分(BR-029)
- 系統能在 wport 帳號刪除時,cascade 呼叫所有 binding 對應的 Provider 端 revoke(Google revoke / Apple
auth/revoke),並確保 Apple 路徑必須成功(App Store 5.1.1(v) 強制)(BR-012 補強、§12.8) - 系統能對 email 套用一致且可審計的 normalize 規則:lowercase 為最低要求;其他規則(Gmail dot/plus alias 是否 strip、Unicode NFC、punycode domain、trailing dot 處理)由 RD 決定演算法,但必須:(a) 全系統共用同一份 normalize 函式,避免 lookup 與 store 用不同規則;(b) 記錄演算法版本,未來變更時可重跑回填;(c) BR-024 / BR-026 / BR-027 比對皆以 normalized email 為基準
Idempotency Key 構成:RD 自行決定(PRD 無偏好)。本 PRD 僅要求「自動合併必須 idempotent」此能力(§8.3),實作 key、TTL、儲存形式由 RD 在 OpenAPI Spec 中明示。
Idempotency 等價判定範圍(PRD 語意層硬約束):
- 「等價」的判定基準為 (user_id, provider, provider_uid) 三元組的最終狀態,而非 request payload hash。同一用戶於並發情境下對同一 provider 連續觸發 auto-merge / sign-up,第二個請求應依「該三元組此刻已存在 binding」走 sign-in 分支,不可重複寫入
Merge Event Log/ 重複寄送 audit 通知信。 - 並發保護:BR-025 解綁防呆、BR-027 cross-account email 檢查、auto-merge binding 寫入,皆要求「不可被併發繞過」此語意。具體鎖實作(DB unique constraint、
SELECT FOR UPDATE、分散式鎖、樂觀鎖)由 RD 決定,但最終可觀察行為必須等價於 serializable。
6. 畫面區塊與資料需求(UI Sections & Data Needs)
6.1 區塊列表
| 區塊 ID | 區塊名稱 | 所在頁面 | 變動類型 | 說明 |
|---|---|---|---|---|
| SEC-1 | 登入頁第三方按鈕區 | 既有登入頁 | 增量 | 既有 Google 按鈕保留,v1 新增 Apple 按鈕;LINE/LinkedIn 按鈕延後 v2 |
| SEC-2 | 註冊頁第三方按鈕區 | 既有註冊頁 | 增量 | 同 SEC-1,按鈕完全一致 |
| SEC-3 | Onboarding 個人資料頁 | 既有頁面 | 無變動 | 沿用 BR-002 流程,第三方登入後若 BR-002 必填欄位不完整則導向此頁 |
| SEC-4 | 帳號設定 > 登入方式區塊 | 帳號設定頁 | 新增 | 顯示已綁定 provider 清單;提供新增/解綁;顯示 primary email |
| SEC-5 | 自動合併 Toast | 全站共用 toast | 新增 | 登入成功後一次性顯示,告知「Apple 已連結到 john@gmail.com 帳號」 |
| SEC-6 | 解綁確認對話框 | 設定頁 modal | 新增 | 二次確認;當試圖解除唯一登入方式時,顯示阻擋訊息 |
6.2 各區塊資料需求
SEC-1 / SEC-2 登入註冊頁第三方按鈕區
- 顯示元件(v1):2 個 provider 按鈕,由上而下:Apple → Google
- 排序理由:Apple 須位於 OAuth 群組首位(iOS HIG / App Store Review Guideline 4.8)
- v2 規劃:補上 LinkedIn、LINE,排序變為 Apple → Google → LinkedIn → LINE(v2 PRD 重新確認)
- 資料需求:無需後端資料;按鈕本身為靜態
- 互動行為(皆為 popup-only,無頁面跳轉,理由見 §10.3):
- Google 按鈕:沿用既有 Google OAuth popup 流程(不變更 SDK 與 callback 處理路徑)
- Apple 按鈕:以 Apple 原生 Sign in with Apple JS(Web) 觸發 popup(
AppleID.auth.signIn({ usePopup: true })或同等 API),後端直接以 Apple JWKS 驗證回傳的 Apple ID Token
- 錯誤狀態:若 provider OAuth 失敗(用戶拒絕授權、provider 服務中斷、popup 被瀏覽器擋下),顯示對應錯誤訊息並回到登入頁
- 空狀態:N/A
SEC-3 Onboarding 個人資料頁
- 沿用 BR-002 既有頁面與必填欄位(姓名、生日、手機、居住地、國籍、身分類型、外籍工作身份)
- 進入條件:第三方登入後,若 BR-002 必填欄位有任一未填 → 強制導向
- 跳脫條件:所有必填欄位填寫完成 → 回到原本要進入的頁面(首頁 / deep link 目標)
SEC-4 帳號設定 > 登入方式區塊
- 顯示資料:
- 使用者 primary email(不可編輯,僅顯示)
- 已綁定 provider 清單,每行顯示:provider 名稱 + provider icon + 綁定的 email(若 Apple Hide My Email 則顯示
*@privaterelay.appleid.com並加註解釋)+ 首次綁定時間 - 每個 provider 行末有「解除綁定」按鈕
- 區塊下方有「新增登入方式」按鈕,點擊後展開未綁定的 provider 清單
- 資料需求:
- 查詢:使用者目前的綁定清單(GET)
- 操作:新增綁定(觸發 OAuth)、解除綁定
- 狀態邏輯:
- 若僅綁定 1 種 provider 且未設 email+password → 「解除綁定」按鈕應 disabled 並 hover 顯示 “請先新增其他登入方式”
- 若綁定 ≥ 2 種 → 「解除綁定」可點擊,點擊後進入 SEC-6
SEC-5 自動合併 Toast
- 觸發條件:第三方登入成功 + 當次登入造成了 provider 自動綁定(非既有綁定的 sign-in)
- 顯示內容:
已將 {provider 名} 登入連結到你的 wport 帳號({primary email mask}) - 顯示時間:5 秒;可手動關閉;同一用戶下次登入不再顯示(cookie / localStorage 標記)
- CTA:可點擊「查看登入方式」直達 SEC-4
SEC-6 解綁確認對話框
- 進入條件:用戶在 SEC-4 點擊任一「解除綁定」按鈕
- 顯示內容:
- 標題:
解除 {provider 名} 綁定? - 說明:
解除後將無法以 {provider 名} 登入 wport。你仍可使用其他登入方式({剩餘 provider 列表})。 - CTA:「確認解綁」(紅色)+ 「取消」
- 標題:
- 阻擋情境(BR-025):若這是唯一登入方式,對話框改為純資訊框:
- 標題:
無法解除綁定 - 說明:
這是你唯一的登入方式。請先新增其他登入方式(例如 Apple、Google,或設定 email + 密碼),再來解除此項。 - CTA:「新增其他登入方式」(直達 SEC-4 的新增綁定流程)
- 標題:
7. 使用者動作與後端需求(User Actions & Backend Needs)
| 動作 ID | 動作名稱 | 類型 | 需要後端 | 涉及實體 | 說明 |
|---|---|---|---|---|---|
| ACT-1 | 以 provider 登入(既有用戶且已綁定此 provider) | Write/Read | ✅ 是 | User, User Identity Binding, Session Provider Index | 標準 sign-in 流程;建立 session 時記錄來源 provider |
| ACT-2 | 以 provider 登入(既有用戶但首次以此 provider;email match) | Write | ✅ 是 | User Identity Binding, Merge Event Log, Auth Audit Notification Log | 自動合併(作法 B);必須先檢查 email_verified=true(BR-026);合併後寄 audit 信 |
| ACT-3 | 以 provider 登入(全新用戶;email 不存在) | Write | ✅ 是 | User, User Identity Binding | sign-up + 進入 Onboarding |
| ACT-4 | 以 Apple Hide My Email 登入(全新 relay email) | Write | ✅ 是 | User, User Identity Binding | 視為獨立新帳號(D1) |
| ACT-5 | 新情境:merge 對象 email 未驗證(email_verified=false) | Read | ✅ 是 | — | 後端 block auto-merge,回 EMAIL_NOT_VERIFIED;FE 提示用戶以原 provider 登入後到設定頁手動綁(BR-026)。v1 攻擊面為 0(Google/Apple email_verified 恆為 true),仍須實作此 gate 以利 v2 |
| ACT-6 | 查看設定頁 > 登入方式 | Read | ✅ 是 | User Identity Binding | 列出當前綁定清單 |
| ACT-7 | 新增綁定(已登入用戶綁第二個 provider) | Write | ✅ 是 | User Identity Binding, Auth Audit Notification Log | 天然 step-up(必然重做 OAuth);provider email 若已是他人 primary email → block(BR-027);UID 衝突亦 block;綁定成功寄 audit 信 |
| ACT-8 | 解除綁定 | Write | ✅ 是 | User Identity Binding, Login Method Diversity, Session Provider Index, Step-Up Verification Token, Auth Audit Notification Log, Revocation Propagation Job | 必須通過 step-up auth(重做 OAuth 或重輸 email+pwd,BR-028);通過後檢查 BR-025;刪除 binding 後執行 BR-029 二層 revocation propagation(L1 Provider revoke best-effort、L2 wport session invalidate 必須成功);寄 audit 信 |
| ACT-9 | 帳號刪除(含 provider 綁定清理 + 對外 cascade) | Write | ✅ 是 | User Identity Binding, Revocation Propagation Job, Auth Audit Notification Log | 沿用 BR-012;補充清除所有 provider 綁定;對外 cascade(§12.8 / BR-012 v1.5.0 補強):(a) 對每個 binding 觸發 BR-029 二層傳播;(b) L1 對 Apple 路徑必須成功(App Store 5.1.1(v))、Google 路徑必須成功(GDPR 資料極小化);(c) 完整流程須 GDPR / 個資法 audit-trail 留證 |
| ACT-10 | 處理 Apple S2S Notification | Write | ✅ 是 | Apple Provider Lifecycle Event Log, Apple Relay Email Status, User Identity Binding, Revocation Propagation Job, Auth Audit Notification Log | 接收並驗證 Apple webhook(簽章、iss、aud、idempotency by transaction_id);依 event_type 分派:email-disabled/enabled → 更新 relay 狀態;consent-revoked/account-delete → 觸發 BR-029 二層傳播(不需 step-up,事件來源 authoritative)+ 寄通知信;唯一登入方式情境改為「需客服恢復」狀態(§12.5);handler ≤ 5s 回 200,重活動異步處理(BR-031) |
真正的 API path/method 由 RD 在 OpenAPI Spec 中定義,此表僅標示「是否需要後端」與「應該涉及哪些資料概念」。
8. API Hints(提示用,非最終 API 設計)
8.1 資料讀取需求(Read)
- HINT-GET-1:取得目前登入使用者的綁定清單
- 對應區塊:SEC-4
- 期望回傳:provider 清單,每筆含 provider 類型、provider 內 email、首次綁定時間
- 可解綁 flag:後端可預先計算「此筆是否為唯一登入方式」,前端據此決定按鈕狀態(避免 FE 重新算 BR-025)
8.2 資料寫入需求(Write)
-
HINT-MUTATION-1:第三方登入(sign-in / sign-up / 合併三合一)
- 輸入:provider 類型 + provider token + client_type(
web/ios;iOS native app 須傳入,Web 預設;具體欄位命名由 RD 在 OpenAPI Spec 決定) - 期望行為:
- 後端依 provider 類型走對應驗證路徑(兩條皆為直連 provider OIDC,不經第三方託管):
provider=google→ 沿用既有 Google JWKS / OIDC 驗證路徑provider=apple, client_type=web→ 直接以 Apple JWKS(https://appleid.apple.com/auth/keys)驗證 Apple ID Token;驗iss = https://appleid.apple.com、aud = wport Apple Services ID、exp/nonce(強制驗證,見 §8.3)provider=apple, client_type=ios→ 同上 Apple JWKS 驗章路徑;差異:aud = wport iOS Bundle ID(由 RD 從 config 讀取,不寫死於 PRD);token 來源為 iOS nativeASAuthorizationAppleIDProvider回傳的identityToken(JWT 格式與 Web 相同)
- 取得 provider UID + email + email_verified(兩條路徑匯流為共同格式)
- 判定情境(見 §10.2 System Flow)
- 若進入 merge 分支:先檢查
email_verified=true(BR-026);若為 false → 回 4xxEMAIL_NOT_VERIFIED,不建立 binding - 回應「登入成功」+「是否觸發了合併」(後者用於 FE 決定要不要顯示 SEC-5 Toast)
- 若觸發合併或新增 binding → 寫入
Auth Audit Notification Log+ 排程寄送 audit 信 - 建立 session 時記錄來源 provider(
Session Provider Index) - Apple 首次授權 email 持久化保護:若該次 Apple ID Token 內含
emailclaim(即「Apple 首次回傳 email」),binding 寫入失敗時必須排入 retry queue 持續重試到成功(不可僅回 5xx 給 FE 後就放棄);FE 視為「登入失敗,請重試」由用戶自然觸發再次登入;後端在背景持續推進至 binding 建立成功
- 後端依 provider 類型走對應驗證路徑(兩條皆為直連 provider OIDC,不經第三方託管):
- 邊界處理:
- Apple Hide My Email relay email → 視為獨立 user,不執行合併(D1)
- 任一 provider
email_verified=false且擊中 merge 分支 →EMAIL_NOT_VERIFIED(BR-026)
- 輸入:provider 類型 + provider token + client_type(
-
HINT-MUTATION-2:新增綁定(已登入狀態)
- 輸入:provider 類型 + provider token
- 期望行為:
- 驗證 token → 取得 provider UID + email
- 若該 UID 已綁在別的 wport 帳號 → 回 4xx
PROVIDER_UID_TAKEN - 若該 email 已是他人 primary email → 回 4xx
EMAIL_BELONGS_TO_OTHER_ACCOUNT(BR-027) - 加入綁定清單,寫入 audit log,排程寄送 audit 信
-
HINT-MUTATION-3:解除綁定
- 輸入:要解除的 provider 類型 + step-up verification token(BR-028)
- 期望行為:
- 驗證 step-up token;若無效、過期、已消耗、或
(user_id, action, target_provider)不匹配 → 回 4xxSTEP_UP_REQUIRED,FE 引導用戶完成 step-up 後重試 - 檢查 BR-025(這是唯一登入方式嗎?)→ 若是回 4xx
LAST_LOGIN_METHOD - 刪除該綁定
- 觸發 BR-029 二層 Revocation Propagation(同步回應 FE 「解綁完成」即可,二層傳播實際執行可異步,但 L2 失敗須重試到成功;詳見 §12.7):
- L1(best-effort):呼叫 Provider 端 revoke endpoint(Google
oauth2.googleapis.com/revoke/ Appleappleid.apple.com/auth/revoke) - L2(必須成功):依
Session Provider Indexinvalidate 該 provider 來源所有 active sessions + 派生 child token
- L1(best-effort):呼叫 Provider 端 revoke endpoint(Google
- 寫入
Revocation Propagation Job與 audit log,排程寄送 audit 信
- 驗證 step-up token;若無效、過期、已消耗、或
-
HINT-MUTATION-4:簽發 step-up verification token(BR-028 配套)
- 輸入:
- sensitive action 類型(unbind / add-binding 等)
- target_provider(要對哪一個 provider 解綁 / 新增綁定;必須明示)
- step-up 證明(OAuth re-auth 結果 或 password 重輸結果)
- 期望行為:
- 驗證證明:
- 若為 OAuth re-auth → 該 provider 的 ID Token 必須通過簽章驗證(Google JWKS / Apple JWKS),且
auth_time距今 ≤ 5 分鐘(避免 client-side SDK 從既有 session 拿出舊 token 矇混 step-up,見 §5.2 能力要求) - 若為 password 重輸 → 必須驗證該帳號目前的 password hash
- 若為 OAuth re-auth → 該 provider 的 ID Token 必須通過簽章驗證(Google JWKS / Apple JWKS),且
- 簽發短效一次性 token:
- 綁定
(user_id, action, target_provider)三元組;換 action 或換 target 都不可重用 - TTL ≤ 5 分鐘
- 一次性消耗(驗證通過即作廢;無論操作成功或失敗皆不可二次使用,重試需重新 step-up)
- 綁定
- 回傳 token 給 FE 作為 HINT-MUTATION-3 的輸入
- 驗證證明:
- 輸入:
-
HINT-MUTATION-5:Apple Sign in with Apple S2S Notification 接收(外部 webhook,Apple → wport)
- 輸入:Apple JWT payload(包含
iss/aud/eventsclaim;events為 JWT-encoded 內層 payload,含type、sub、event_time、transaction_id) - 期望行為:
- 驗證外層 JWT 簽章(Apple JWKS
https://appleid.apple.com/auth/keys);簽章不過 → 回 401,記 security log - 驗證
iss = https://appleid.apple.com;aud = wport Apple Services ID;不符 → 回 401 - 解析內層
eventsclaim 取得type+sub(Apple OIDC sub,作為provider_uid查 binding)+transaction_id - Idempotency check:若
transaction_id已記錄於Apple Provider Lifecycle Event Log且狀態為done→ 直接回 200(不重做) - 必須在 ≤ 5 秒內回 200:寫入
Apple Provider Lifecycle Event Log(狀態pending)後立即回 200;重活動排程到後端 queue 異步處理 - 異步 handler 依
event_type分派:email-disabled→ 更新Apple Relay Email Status = disabled;標記 user nudge flagemail-enabled→ 更新Apple Relay Email Status = active;清除 nudge flagconsent-revoked→ 移除 binding(不需 step-up)+ 觸發 BR-029 二層傳播 + 寄通知信account-delete→ 移除 binding + 觸發 BR-029 二層傳播 + 寄通知信;wport user 帳號保留(用戶可從其他 provider 取回);若 Apple 為唯一登入方式 → 將帳號標記為「needs customer service intervention」狀態(§12.5)
- handler 完成後更新
Apple Provider Lifecycle Event Log狀態done;失敗則failed+ 排程重試
- 驗證外層 JWT 簽章(Apple JWKS
- 邊界處理:
sub查無對應 binding(用戶已自行解綁 / 帳號已刪)→ 記錄事件後標記not_applicable,回 200- 處理過程 BR-029 任一層失敗 → event log 狀態
failed,重試 queue 持續推進至成功 - 同一用戶短時間內收到多個 event(例如先
email-disabled再account-delete)→ 依時間序處理,後者覆蓋前者
- 輸入:Apple JWT payload(包含
-
HINT-MUTATION-6:刪除 wport 帳號(含對外 Cascade)
- 輸入:當前用戶 session(沿用 BR-012 既有 owner check + 二次確認流程)
- 期望行為(與既有 BR-012 流程並行 / 補強):
- 沿用 BR-012 既有 pre-check(Owner 檢查、進行中應徵、企業 owner 狀態等)
- 對該 user 的每一個
User Identity Binding觸發 BR-029 二層 Revocation Propagation,但所有層皆設為「必須成功」(含 L1,因 Apple 路徑受 App Store 5.1.1(v) 規範;Google 路徑為 GDPR 資料極小化要求) - 沿用 BR-012 既有 user 軟刪除 / 履歷處理 / 應徵歷史等步驟
- 寫入
Revocation Propagation Job+Auth Audit Notification Log;寄送帳號刪除完成通知信 - 完整 cascade 結果必須可供 GDPR / 個資法 audit 查詢(用戶若申訴「為何 Apple 端仍可看到 wport 授權?」可舉證已執行 revoke)
- 失敗處理:
- L1 Apple
auth/revoke失敗 → 不對用戶端回成功,排程重試至成功才標記帳號刪除完成;用戶 UI 顯示「處理中」狀態 - L1 Google revoke 失敗 → 重試 N 次後若仍失敗則 fallback 為 audit log + 進 admin DLQ 人工介入(接受外部一致性殘餘風險,但須在 GDPR audit trail 留證已盡力撤銷)
- L1 Apple
Token 驗證機制、provider SDK 整合方式、internal endpoint 命名等實作細節 → RD OpenAPI Spec。
8.3 必須在 API contract 中明確的項目(hard gate)
依 prd-gen Rule 21 / Rule 26,下列項目不得 TBD:
| 項目 | PRD 立場 |
|---|---|
| Token TTL | 沿用 wport 既有 session/JWT 策略(Google 登入已實作的同一套),RD 不需重新設計。v1 架構下,Google / Apple session 皆由後端在驗完 provider OIDC ID Token 後簽發 wport 自家 session(與既有 Google 同一套),前後端對 session 的處理為單一語意 |
Apple ID Token aud 白名單 | BE 必須同時接受兩個合法 audience:(1) Web Apple Services ID(Web 路徑,client_type=web);(2) iOS Bundle ID(iOS native 路徑,client_type=ios)。兩個值皆由 RD 從環境 config 讀取,不可寫死於程式碼。token aud 不在白名單內 → 驗章失敗,回 4xx |
| Apple / Google ID Token nonce 驗證 | 強制驗證。FE 在發起 OAuth 前產生 random nonce,hash(SHA256)後帶入 OAuth request;Apple / Google 將 nonce hash 簽進 ID Token。BE 驗章時必須比對 nonce;nonce 不匹配 → 驗章失敗,回 4xx。且 BE 驗章通過後必須以 Redis SETNX(或等價 atomic 操作)將 nonce hash 標記為已消耗,TTL ≤ 15min;同一 nonce 第二次出現一律拒絕(NONCE_ALREADY_USED 422)。目的:防止 ID Token 被中間人攔截後重放——單純驗 nonce claim 不足以防重放(攻擊者側錄 ID Token 後 TTL 內可任意重放並登入),必須加上「一次性消耗」才有實質防護。iOS native ASAuthorizationAppleIDProvider 同樣支援 nonce,實作方式與 Web 相同 |
| Rate limiting — 驗章失敗計 IP quota | per IP rate limit(BR-030)必須對 token 驗章失敗的請求同樣計入 quota(即使尚未取得 email / UID)。理由:攻擊者持續送無效 token 永遠進不到 post-verify 層,等於只受 per IP 60/min 限制而不觸發 per-email / per-UID 保護 |
| Step-up token 消耗時機 | Step-up verification token 僅在操作成功後消耗(作廢);若後端回傳 client error(如 LAST_LOGIN_METHOD、EMAIL_BELONGS_TO_OTHER_ACCOUNT 等業務阻擋),token 不作廢,TTL 內可持原 token 重試。例外:後端回 5xx 或 token 驗章本身失敗 → token 一律作廢(需重新 step-up)。原條文「無論操作成功失敗皆作廢」修訂為此 |
| Token revoke 策略 | 沿用既有;解綁與帳號刪除時依 BR-029 二層 Revocation Propagation 處理(L1 Provider 端 revoke / L2 wport session invalidate)。失敗處理矩陣見 BR-029;最終一致性由 Revocation Propagation Job 追蹤至完成 |
| Expiration 行為 | 沿用既有 session 過期 → 重新登入 |
| Idempotency | 自動合併必須 idempotent:用戶連點兩次「以 Apple 登入」不應產生兩條 Merge Event Log。Key 構成、TTL、儲存形式由 RD 決定(PRD 無偏好) |
| 即時 auth re-check | 解綁、新增綁定屬於 sensitive action,除「當前 session 未過期」外,必須通過 step-up authentication(BR-028)。新增綁定為天然 step-up;解綁需擇一:(a) 重做該 provider OAuth;(b) 重輸 email+password |
| Step-up token 綁定範圍 | Step-up verification token 必須綁定 (user_id, action, target_provider) 三元組,消耗時機見「Step-up token 消耗時機」列。TTL ≤ 5 分鐘。禁止簽發只綁 action(無 target)的寬鬆 token(會導致用戶完成 step-up 後可解綁任意 provider) |
| Step-up OAuth 新鮮度 | 若 step-up 證明採「重做該 provider OAuth」,該 provider ID Token 的 auth_time claim 距今必須 ≤ 5 分鐘。Google / Apple 兩條路徑同此規範(client-side SDK 可能從既有 session 直接 reissue 老 token,必須以 auth_time 而非 iat 為新鮮度依據) |
| Apple 首次授權 email 持久化保護 | Apple 規範下 email 僅在首次授權回傳;後端從「驗完 token」到「binding 已寫入 DB」這段路徑必須具備失敗持久化保護(retry queue + DLQ),避免單次 DB 故障導致該 user email 永久遺失。若仍無法寫入 binding,整個登入結果回 5xx 給 FE,後端在背景持續推進,用戶下次再次點 Apple 登入時自然 reissue(見 §5.2 / §10.2 補充說明) |
| email_verified gate | Auto-merge 必須 email_verified=true,否則回 EMAIL_NOT_VERIFIED(BR-026)。為硬性安全 gate,不得放寬 |
| Cross-account email block | 新增綁定時,provider email 若已是他人 primary email → 回 EMAIL_BELONGS_TO_OTHER_ACCOUNT(BR-027) |
| Rate limiting | 所有第三方登入 + 新增綁定 endpoint 必須有 rate limit:per IP 60 次/min、per email 10 次/min、per provider UID 10 次/min(BR-030)。觸發回 429 + Retry-After。降級策略分層:pre-verify per IP → fail-open;post-verify per email / UID → fail-closed(回 503)。兩層失敗皆 alert oncall |
| Session 來源紀錄 | 每筆 active session 必須能識別「來源 provider」(schema 細節由 RD 決定),以支援 BR-029 的精準 invalidate |
| BR-029 session invalidate 範圍(L2) | 解綁 provider X 後,至少 invalidate「以 X 為來源 provider 建立的所有 active sessions」。v1 保守策略採此最小範圍;若該 session 在解綁前曾派生 child token(OAuth grant、API token、refresh token),同樣 invalidate 至 child token 層級。不主動 invalidate「以其他 provider 建立的 sessions」,避免誤殺合法在線用戶。已知殘餘風險:若攻擊者已透過 X session 派生其他 provider 的 session(業務面少見、需通過 step-up),該 session 不會被解綁動作觸及,需依賴 BR-028 step-up 攔截;該 trade-off 屬已知 accepted risk,記入 §15 凍結表 |
| BR-029 二層傳播失敗處理 | L1(Provider revoke):unbind 為 best-effort + audit log;delete 為必須成功(Apple 為 App Store 5.1.1(v) 強制;Google 為 GDPR 資料極小化要求,重試 N 次仍失敗則 fallback 為 audit log + admin DLQ 人工介入)。L2(wport session invalidate):必須成功。所有失敗皆寫入 Revocation Propagation Job 並排程重試,DLQ 設計由 RD 決定。同步回 FE 「成功」的時點可在 L2 完成後,L1 異步處理(unbind 情境);delete 情境須等 L1+L2 全部完成才能回成功 |
| Apple S2S Notification 接收 | webhook 必須驗證 Apple JWT 簽章(Apple JWKS)+ iss + aud;以 transaction_id 做 idempotency;handler ≤ 5 秒回 200(超時 Apple 視為失敗會重試);重活動排到 queue 異步處理。失敗事件不可丟棄,須持續重試至 done 或經人工標記放棄(BR-031) |
Apple S2S consent-revoked / account-delete 處理 | 觸發等同 ACT-8 解綁路徑(含 BR-029 二層傳播),但不需 step-up(事件來源 Apple 端 authoritative);不適用 BR-025 防呆(外部已 authoritative);若 Apple 為唯一登入方式 → 帳號標記為「needs customer service intervention」並依 §12.5 SOP 處理;皆寄 audit 通知信(BR-018) |
| Account Deletion 對外 Cascade | wport 帳號刪除(ACT-9 / HINT-MUTATION-6)必須對外 cascade:(a) 對 Apple binding 呼叫 https://appleid.apple.com/auth/revoke(必須成功,App Store 5.1.1(v));(b) 對 Google binding 呼叫 https://oauth2.googleapis.com/revoke(必須成功,GDPR Article 17 / 個資法;重試 N 次仍失敗 fallback admin DLQ);完整 cascade audit-trail 必須可供日後查證(§12.8) |
| Audit 通知信 | 自動合併、新增綁定、解綁、Apple S2S 觸發之撤銷/刪除皆寄送 audit email(BR-018 補強)。寄送為 best-effort,失敗應重試但不阻斷主流程 |
9. 導航 / Storybook Map
- Storybook:本期不產出獨立 Storybook(FE 為既有頁面增量變更)
- 對應 production 路由:
- SEC-1 / SEC-2:
/login、/register(既有) - SEC-3:
/onboarding(既有,沿用) - SEC-4 / SEC-6:
/settings/account(新增子區塊,路由不變) - SEC-5:全站 toast layer,無獨立路由
- SEC-1 / SEC-2:
10. Flowcharts(User / System / Navigation)
10.1 User Flow(使用者操作流程)
flowchart TD
Start([使用者開啟 wport])
Choice{已登入?}
LoginPage[登入頁 SEC-1]
PickProvider{選擇 provider}
OAuth[provider OAuth 流程]
OAuthOK{OAuth 授權成功?}
OAuthFail[顯示錯誤,回登入頁]
BE[後端 token 驗證 + 情境判定]
Scene{情境}
SignIn[既有用戶 sign-in]
Merge[自動合併 + Toast SEC-5]
SignUp[新用戶建立帳號]
NeedProfile{個人資料已完整?<br/>BR-002}
Onboarding[Onboarding SEC-3]
Home[進入主頁/原 deep link]
Settings[帳號設定 > 登入方式 SEC-4]
Unlink{欲解除綁定}
CheckOnly{唯一登入方式?<br/>BR-025}
BlockUnlink[阻擋並提示<br/>新增其他登入方式]
ConfirmUnlink[SEC-6 確認對話框]
DoUnlink[執行解綁]
AddBinding[新增其他 provider 綁定]
Start --> Choice
Choice -->|否| LoginPage
Choice -->|是| Home
LoginPage --> PickProvider
PickProvider --> OAuth
OAuth --> OAuthOK
OAuthOK -->|否| OAuthFail
OAuthOK -->|是| BE
BE --> Scene
Scene -->|新 email| SignUp
Scene -->|已有 email + 新 provider| Merge
Scene -->|已有 provider 綁定| SignIn
SignUp --> Onboarding
Merge --> NeedProfile
SignIn --> NeedProfile
NeedProfile -->|否| Onboarding
NeedProfile -->|是| Home
Onboarding --> Home
Home -.可訪問.-> Settings
Settings --> Unlink
Settings --> AddBinding
AddBinding --> OAuth
Unlink --> CheckOnly
CheckOnly -->|是| BlockUnlink
CheckOnly -->|否| ConfirmUnlink
ConfirmUnlink --> DoUnlink
BlockUnlink --> AddBinding
共享子流程:OAuth 節點與 BE 情境判定 同時被「登入流程」與「新增綁定流程」共用(避免重複節點樹,符合 prd-gen 流程圖品質規則)。
Entry vs in-flow 邊界:
- Entry-time:OAuth 失敗 → 阻擋在登入頁,未建立任何帳號
- In-flow:session 過期、解綁衝突 → 在進行中流程中以 modal/toast 提示,保留當前狀態
Re-entry 路徑:
- 若 OAuth 中途斷網 → 用戶可重新點擊 provider 按鈕,後端以 provider UID 為自然冪等鍵
- 若 Onboarding 中途離開 → 下次登入後再次強制導向 Onboarding(沿用 BR-002 行為)
10.2 System Flow(前後端與 Provider 資料流)
架構一致:Google / Apple 兩者皆採「直連 provider OIDC + 後端打 provider JWKS」的同類型路徑,不引入第三方託管層。下方以「Token 取得(FE 端,分軌)」與「Token 驗證 + 情境判定(BE 端匯流)」兩段呈現。
10.2.1 Token 取得(FE 端,分軌)
sequenceDiagram
participant U as 使用者
participant FE as wport FE
participant G as Google OAuth
participant A as Apple OAuth
alt provider = google(既有路徑,不變)
U->>FE: 點擊 Google 按鈕
FE->>G: 既有 Google OAuth popup
G-->>U: 授權同意頁
U->>G: 同意授權
G-->>FE: 回傳 Google ID Token
else provider = apple(Web,v1 新增)
U->>FE: 點擊 Apple 按鈕(Web)
FE->>A: Sign in with Apple JS popup(AppleID.auth.signIn, usePopup: true)
A-->>U: 授權同意頁
U->>A: 同意授權
A-->>FE: 回傳 Apple ID Token(首次含 email;含 sub);aud = Apple Services ID
else provider = apple(iOS native,v1 新增)
U->>FE: 點擊 Apple 按鈕(iOS app)
FE->>A: ASAuthorizationAppleIDProvider requestedScopes: [.fullName, .email](含 nonce)
A-->>U: 系統層 Apple 授權彈窗
U->>A: 同意授權
A-->>FE: 回傳 identityToken(Apple ID Token;首次含 email;含 sub);aud = iOS Bundle ID
end
10.2.2 Token 驗證 + 情境判定(BE 端,匯流為單一邏輯)
sequenceDiagram
participant FE as wport FE
participant BE as wport BE
participant GJWKS as Google JWKS
participant AJWKS as Apple JWKS
participant DB as wport DB
FE->>BE: 提交 (provider 類型, token)
alt provider = google
BE->>GJWKS: 驗證 Google ID Token 簽章(既有路徑)
GJWKS-->>BE: 回傳 sub (google_uid) + email + email_verified
else provider = apple
BE->>AJWKS: 驗證 Apple ID Token 簽章(iss / aud / exp / sub)
AJWKS-->>BE: 回傳 sub (apple_uid) + email(首次)+ email_verified
end
Note over BE: 兩條路徑匯流為共同 (provider, provider_uid, email, email_verified)
alt Apple Hide My Email
Note over BE,DB: 第一次:以 Apple sub + email 建立 binding<br/>第二次起:Apple 不再回 email,僅靠 Apple sub (UID) lookup binding
BE->>DB: 以 Apple UID 查找 binding
alt binding 已存在
BE->>DB: 視為既有用戶 sign-in
else binding 不存在(首次)
BE->>DB: 建立新用戶(relay email 為 primary)+ 建立 binding
end
BE-->>FE: 登入成功(不顯示合併 toast)
else 一般 email
BE->>DB: 查找此 provider UID
alt 已綁定
BE->>DB: sign-in 既有用戶
BE-->>FE: 登入成功(無合併)
else 未綁定,查 email
alt email 已存在
alt email_verified=false(BR-026 攔截)
BE-->>FE: 4xx + 錯誤碼 EMAIL_NOT_VERIFIED
FE-->>U: 顯示「請先以原登入方式登入後到設定頁手動綁定」
else email_verified=true
BE->>DB: 將此 provider 綁定到既有用戶<br/>寫入 Merge Event Log + Audit Log
BE-->>FE: 登入成功 + merge=true
FE-->>U: 顯示 SEC-5 Toast
Note over BE: 排程寄送 audit 通知信(BR-018)
end
else email 不存在
BE->>DB: 建立新用戶 + 綁定
BE-->>FE: 登入成功 + isNewUser=true
FE->>FE: 導向 Onboarding(若 BR-002 未滿足)
end
end
end
System Flow 補充說明:
- 架構一致性(Google 與 Apple 同類型):v1 兩個 provider 皆採「直連 provider OIDC + 後端打 provider JWKS」的同類型路徑,不引入第三方託管層。Google 沿用既有框架(token 驗證、session 簽發、user table
google_id欄位皆已成熟);Apple 為新增功能,但接入模式與 Google 同類型,RD 可在既有 Google 驗章管線基礎上新增 Apple 分支,不需引入新的 SDK / 後台。v2 LINE/LinkedIn 將沿用同一套接入模式(各自的 OIDC issuer + JWKS),與 v1 共用同一份帳號模型。 - Session 簽發共用:不論 token 由哪條路徑驗出,最終都由 wport BE 簽發同一套 wport session(沿用既有策略),FE 後續所有 API 呼叫無感知差異。provider 自家 ID Token 僅作為各條路徑的「對外驗證信物」,不會替代 wport session。
- Apple email 只給一次 → 後端寫入持久化保護:Apple 規範下,使用者授權後 Apple 只在第一次 回傳 email(後續授權只回 Apple sub)。後端從「驗完 token」到「binding 已含 email 寫入 DB」這段路徑必須具備失敗持久化保護:寫入失敗時必須排入 retry queue 持續重試到成功(含 DLQ),整個登入流程回 5xx 給 FE 由用戶自然觸發再次登入。Apple 在用戶授權狀態未變的情況下,下次仍會把同一份 email 隨 ID Token 簽出(OIDC ID Token 是即時簽發,包含當下用戶授權範圍內的 claims),所以「失敗 → 用戶再登入一次」即可補上。真正不能容忍的是「驗了就標 binding 成功但沒寫入 email」;只要寫入路徑有持久化保護,這條風險就可被 close(見 §5.2 / §8.3 / §10.4)。
- Apple 第二次起的 lookup:由於 Apple 第二次起不再回 email,binding 建立時必須保存 Apple sub (UID),第二次起的登入完全以 UID 為 lookup key。PRD §5.1 的「(使用者, provider 類型, provider 內 UID)」三元組能力已涵蓋此需求。
- email_verified gate 攔截位置:注意
EMAIL_NOT_VERIFIED攔截只發生在「已存在的 email 嘗試 merge」分支;若 email 不存在於系統(純 sign-up),則不擋(沿用 BR-001:第三方 provider 已驗證過),直接建立新帳號。v1 攻擊面為 0(Google/Appleemail_verified恆為 true),但 gate 仍須實作以利 v2。 - Rate limit 位置:BR-030 rate limit 應發生在最外層;具體分層(pre-token-verify per IP / post-token-verify per email/UID)由 RD 在系統設計中決定。
10.3 Navigation Flow(v1 定案:popup-only,無 redirect callback)
決策:v1 兩個 provider,Web 端皆採 popup-only,iOS native 端採系統層原生彈窗,兩者皆不引入
/auth/callback/redirect 路徑。Web 路徑:Google 沿用既有 popup 處理路徑;Apple 採 Apple Sign in with Apple JS(Web)的 popup 模式(
AppleID.auth.signIn({ usePopup: true }))。iOS native 路徑:Apple 使用
ASAuthorizationAppleIDProvider(系統層原生彈窗,非 popup/WebView),不適用 popup-only 說明。Google 使用 Google Sign-In iOS SDK(GIDSignIn.sharedInstance.signIn),同為系統層授權,不使用 popup。兩者皆由 iOS FE 取得 ID Token 後,POST 給 BE 同一個 HINT-MUTATION-1 endpoint(帶client_type=ios)。理由(Web popup):
- 避免 SPA 狀態被打斷:redirect callback 會中斷 React/Next.js 的記憶體狀態(route guard、deep link 目標、未送出表單),須額外處理 state rehydration、CSRF token、redirect URL whitelist,FE 工程量級顯著上升。
- Apple 原生 JS 已支援 popup 模式:早期 Apple Web 僅支援 redirect + form POST callback 是現已過時的限制;現行 Sign in with Apple JS 已正式支援
usePopup: true,行為與 Google popup 相同,不需中間託管層也能達到「無頁面跳轉」。- 與 §6.2 SEC-1/SEC-2「無頁面跳轉」一致:避免互動描述與導航圖打架。
- 降級路徑(v1.x 視需要再開):若實測 Safari ITP / popup blocker 造成 Apple popup 失敗率過高,再評估 Apple 專用 的 redirect fallback(Apple Sign in with Apple JS
usePopup: false,配合自家/auth/callback/applePOST 接收端)。屆時為 Apple-only、不波及 Google,且須另開 PRD 補上 callback 路徑、state 處理、edge cases。
flowchart LR
LoginP[/login/] --> Popup[Provider OAuth popup<br/>Google / Apple:皆為原生 popup]
Popup --> Onboarding[/onboarding/]
Popup --> Home[/home/]
Onboarding --> Home
Home -.-> Settings[/settings/account/]
Settings --> Popup
- Popup 失敗時的行為:popup 被瀏覽器阻擋或使用者關閉 → FE 視為「未登入」,停留在當前頁並顯示錯誤 toast;不自動退化為 redirect(v1 不支援)。
10.4 Edge Case Coverage(必填)
| 類別 | 情境 | 系統行為 | UI 回饋 | 可重試 | 資料一致性策略 | 備註 |
|---|---|---|---|---|---|---|
| Network & Performance | provider OAuth 重導超時(> 30s) | 取消等待,回登入頁 | 顯示「登入逾時,請重試」 | Yes | 未建立任何綁定/帳號 | 沿用既有 Google 行為 |
| Network & Performance | 用戶連點兩次 provider 按鈕 | 第一次請求進行中時禁用按鈕;後端以 provider UID 為冪等鍵 | 按鈕 disabled + spinner | Yes | Merge Event Log 不重複 | Idempotency hard gate |
| Network & Performance | Apple OIDC 服務中斷(Apple JWKS / 授權端點不可用;僅影響 Apple;Google 走既有路徑不受影響) | 後端對 Apple 流程回 5xx,FE 重試 1 次後放棄;Google 仍可正常登入 | Apple 按鈕:「Apple 登入服務暫時無法使用,請稍後再試或改用 Google」 | Yes | 不寫入任何資料 | v1 兩條路徑各自獨立:任一 provider 故障時另一個自動成為 fallback |
| Data & Input | Apple 首次授權回傳 email 後寫入 binding 失敗:驗 token 成功但寫 DB / binding 發生例外 | 整體登入流程回 5xx 給 FE;後端寫入操作排入 retry queue 持續重試到成功(含 DLQ) | 用戶看到「登入失敗,請重試」;下次點 Apple 登入即可成功(Apple 仍會把 email 簽進 ID Token) | Yes | 不可只標 binding 已建立、卻沒成功保存 email | §5.2 / §8.3 / §10.2 補充說明 |
| Network & Performance | popup 取得 token 後,FE 提交 BE 過程中斷網路 | 用戶可手動重試;後端以 (provider, provider_uid) 為自然冪等鍵 | 顯示 retry 按鈕 | Yes | 以 provider UID 為自然冪等鍵 | popup-only 架構下無 callback 路徑(§10.3) |
| Data & Input | Apple Hide My Email 用戶後續修改 Apple 設定停用 relay | 既有 relay email 失效,但 wport 帳號仍存在 | 用戶無法收信;下次登入仍可(以 Apple UID 為憑) | N/A | 不主動清理;用戶可手動更改 primary email | 屬於 Apple 平台限制 |
| Data & Input | 解綁後 provider email 與 primary email 同步處理 | wport primary email 不因解綁而改變 | 設定頁不變 | N/A | primary email 為 wport 內部欄位,不隨綁定變動 | |
| Data & Input | Apple 回傳的 email 與 wport primary email 大小寫不同(JOHN@gmail.com vs john@gmail.com) | 後端以 lowercased email 為比對基準 | 視為相同 email,正常合併 | N/A | normalize 後比對 | 沿用既有 Google 處理;§5.2 normalize 能力一致 |
| User Interruption | OAuth 過程中關閉 popup / 切換 tab | 後端未收到 token,視為未登入 | 回登入頁 | Yes | 無資料寫入 | |
| User Interruption | Onboarding 填寫到一半離開 | 已填欄位保留(沿用 BR-002 既有行為);下次登入仍導向 Onboarding | 下次登入顯示「請完成個人資料」 | Yes | partial state 保留 | |
| Auth & Lifecycle | session 過期,用戶在設定頁試圖解綁 | 後端 401,FE 導回登入頁,登入後返回設定頁 | ”登入已過期,請重新登入” | Yes | 操作未執行 | |
| Auth & Lifecycle | 用戶 A 在 tab1 解綁 Apple,tab2 同時試圖解綁 Google(剩餘只有這兩個) | 第一個請求成功;第二個請求觸發 BR-025 阻擋 | tab2 顯示阻擋訊息 | N/A | 後端以最終狀態判定,避免兩請求都過 | Concurrency hard gate |
| Auth & Lifecycle | 用戶刪除帳號(沿用 BR-012)但有多個 provider 綁定 | 一併清除所有 provider 綁定資料 | 沿用 BR-012 既有 UI | N/A | 軟刪除 user,硬刪除綁定列 | 補充 BR-012 |
| Logical Inconsistency | 用戶 X 已用 Google a@gmail.com 註冊;用戶 Y 也用 Apple Hide My Email 拿到 abc@privaterelay.appleid.com;後來 X 嘗試新增 Apple 並碰到同一組 relay email(碰撞,極低機率) | 後端以 lowercased email 比對 → 發現與用戶 Y 衝突;拒絕綁定 | ”此 email 已被另一個 wport 帳號使用” | N/A | 唯一信箱原則優先 | 接近不可能但需有錯誤碼 |
| Logical Inconsistency | 用戶試圖綁定一個已被別人綁定的 provider UID(不同 wport 帳號用同一個 Google 帳號) | 後端拒絕,回 PROVIDER_UID_TAKEN | ”此 {provider} 帳號已綁定其他 wport 帳號” | N/A | 每個 provider 內 UID 唯一(跨 provider 不保證;同一 provider 下 UID 在系統內僅能對應一個 wport 帳號) | |
| Logical Inconsistency | 跳步進入:未登入用戶直接打 /settings/account | 沿用既有 auth guard,導向登入 | ”請先登入” | Yes | N/A | 沿用 |
| Auth & Lifecycle | Auto-merge 對象 email 未驗證(v2 適用攻擊面,v1 dormant):攻擊者以 LINE 註冊新帳號,將 email 填為受害者 victim@gmail.com,但 LINE 未驗證此 email | 後端 block auto-merge,回 EMAIL_NOT_VERIFIED | ”此 email 尚未經 {provider} 驗證,無法自動連結。請先以原登入方式登入後到設定頁手動綁定” | No(重試無用,須換流程) | 不建立 binding,不寫入 Merge Event Log | BR-026 安全 gate(v1 須實作,v2 開始有實際攻擊面) |
| Auth & Lifecycle | 解綁時未通過 step-up auth:用戶 session 仍有效但未重做 OAuth/重輸密碼 | 後端回 STEP_UP_REQUIRED,FE 觸發 step-up 流程後重試 | Modal「為了你的帳號安全,請先重新驗證身份」+ 顯示 step-up 選項(重做 OAuth / 輸入密碼) | Yes(完成 step-up 後) | 操作未執行 | BR-028 |
| Auth & Lifecycle | 解綁後其他裝置 session 處理:用戶在 PC 解綁 Apple,手機端以 Apple 登入產生的 session 仍 active | 後端依 Session Provider Index invalidate 該 provider 來源的所有 active sessions | 手機端下次操作即被踢出登入頁 | N/A | session table 須有 provider 維度 | BR-029 |
| Network & Performance | Rate limit 觸發:同 IP / email / UID 超過 BR-030 閾值 | 後端回 429 + Retry-After header | ”登入嘗試過於頻繁,請於 N 秒後再試” | Yes(等待 window) | 不建立任何資料 | BR-030 |
| Network & Performance | Rate limit 服務不可用(pre-verify 層):per IP 檢查 Redis 失聯 | Fail-open(不阻擋)+ alert | 用戶無感 | N/A | 接受短期暴露 IP 維度攻擊面,oncall 介入 | BR-030 分層降級 |
| Network & Performance | Rate limit 服務不可用(post-verify 層):per email / UID 檢查 Redis 失聯 | Fail-closed(回 503 + Retry-After) | “登入服務暫時繁忙,請稍後再試。“ | Yes | 不放行未經 limit 把關的 per-user 流量 | BR-030 分層降級 |
| Logical Inconsistency | 新增綁定時 provider email 屬於他人:用戶 A 綁 Apple,Apple 回傳的 email 已是用戶 B 的 primary email | 後端拒絕,回 EMAIL_BELONGS_TO_OTHER_ACCOUNT | ”此 email 已是另一個 wport 帳號的主信箱,請聯絡客服協助合併” | No(需客服介入) | 不建立 binding | BR-027 唯一信箱反面 |
| Data & Input | Migration 期間既有 Google 用戶登入:user.google_id 既有欄位與新 binding 表並存 | 後端雙寫:既有欄位寫入 + binding 寫入;read 優先讀 binding | 用戶無感 | Yes | 雙寫策略,migration 完成後移除舊欄位 | §12.4 |
| Auth & Lifecycle | Step-up token 不匹配 target provider:用戶完成解綁 Apple 的 step-up,但以該 token 嘗試解綁 Google | 後端比對 token 內綁定的 (action, target_provider) 與請求不符 → 回 STEP_UP_REQUIRED | ”請重新驗證身份” | Yes(重做對應 provider 的 step-up) | 不執行解綁;token 不消耗(語意上未啟用該 token) | BR-028;§8.3 step-up token 綁定範圍 |
| Auth & Lifecycle | Step-up OAuth 不新鮮:用戶在解綁時重新觸發 provider 登入 popup,但 SDK 從既有 session 直接 reissue 老 token(auth_time > 5 分鐘) | 後端驗 auth_time 不通過 → 回 STEP_UP_REQUIRED + sub-reason STALE_AUTH | ”請重新登入該 {provider} 帳號” → FE 先觸發 sign-out(清掉 provider 端 session)再重打 popup | Yes | 不執行解綁;token 不簽發 | §8.3 step-up OAuth 新鮮度 |
| Auth & Lifecycle | Onboarding 中途進設定頁解綁:用戶剛 sign-up 進 Onboarding(單一 binding、無 password),透過 deep link 進入 /settings/account 試圖解綁該唯一 provider | 沿用 BR-025:「唯一登入方式」防呆觸發,按鈕 disabled | ”請先完成個人資料後新增其他登入方式” + 導回 Onboarding | N/A | 操作未執行 | BR-025;Onboarding flow 不阻擋進入設定頁但動作受限 |
| Logical Inconsistency | Apple sub 取錯 claim:誤將其他 Apple claim 或 client 自填值存為 provider_uid → 後續 lookup 失準 | 預防:DB write path 強制以 Apple JWKS 驗章成功後的 sub claim 為 provider_uid 來源;不接受 client 端傳入的 sub | N/A(預防性 invariant) | N/A | code review + 單元測試覆蓋 | §5.1 Provider UID 識別主鍵語意 |
| Logical Inconsistency | 跨帳號分裂(Apple Hide My Email + Google 同人不同帳號):用戶以 Apple relay 註冊帳號 A1,後續以 Google 註冊帳號 A2 | 系統不主動合併(Apple relay 為獨立身份)→ 兩帳號平行存在 | 用戶察覺後可從設定頁提交「合併帳號」客服工單 | N/A(不自助) | 由客服依 §12.5 SOP 介入 | §12.5 帳號分裂客服 SOP |
| Auth & Lifecycle | Apple S2S consent-revoked 到達:用戶在 Apple ID 設定撤銷對 wport 的授權 | webhook handler 異步觸發 unbind + BR-029 二層傳播;不需 step-up;不適用 BR-025 防呆 | 用戶在 wport 端下次操作(其他 provider 登入)後設定頁顯示「Apple 已由 Apple 端撤銷」+ 收到 audit 通知信 | N/A | binding 刪除;二層傳播至完成 | BR-031;§12.6 |
| Auth & Lifecycle | Apple S2S account-delete + Apple 為唯一登入方式:用戶刪除 Apple ID,wport 收到通知,但該用戶在 wport 沒有其他登入方式 | binding 刪除 + 二層傳播 + wport user 標記為「needs customer service intervention」;用戶下次嘗試登入(任何 provider 皆無法)→ 引導客服恢復 SOP | 寄通知信至 primary email(若為 relay 則寄不到,視為已知缺口);客服 SOP 接管 | N/A(不自助) | wport user 軟保留,依 §12.5 SOP 處理 | BR-031;§12.5;§12.6 |
| Data & Input | Apple S2S email-disabled 到達:用戶在 Apple ID 關閉 Hide My Email 轉信 | 更新 Apple Relay Email Status = disabled;user nudge flag 設定 | 用戶下次登入後 banner 顯示「你的 Apple 匿名信箱已停用,請設定可收信 email 以接收重要通知」 | N/A | binding 不變;僅狀態標記 | BR-031;§12.6 |
| Data & Input | Apple S2S webhook idempotency:Apple 重試或網路重送同一 transaction_id | 後端以 transaction_id 去重,第二次直接回 200 不重做 | 用戶無感 | N/A | 不重複處理、不重複寄信 | BR-031;§5.1 Apple Provider Lifecycle Event Log |
| Auth & Lifecycle | 二層傳播 L1 失敗(unbind 情境):unbind 完成但 Provider 端 revoke 連線失敗 | wport 端回 FE「解綁完成」(L2 已成功);Revocation Propagation Job 標記 L1 failed → queue 重試(unbind 情境為 best-effort,不阻擋用戶感知,但仍盡力推進) | 用戶 UI 顯示解綁完成;後端 metric / oncall 監控未完成 job | N/A(後端自動重試) | binding 已刪、wport session 已 invalidate;Provider 端最終會被撤銷 | BR-029;§12.7 |
| Auth & Lifecycle | 二層傳播 L1 Apple revoke 失敗 + delete 流程:帳號刪除時 Apple auth/revoke 連線失敗 | 不回 FE「刪除完成」;UI 顯示「處理中」;queue 持續重試 L1 至成功 | 「帳號刪除處理中,請稍候。完成後將寄信通知。」 | Yes(自動) | wport 端不可提前進入 deleted 狀態(避免內部已標記刪除但 App Store policy 仍未滿足) | BR-012 v1.5.0 補強;§12.8 |
| Logical Inconsistency | Apple S2S webhook payload 偽造:攻擊者偽造 webhook 試圖讓系統解綁他人 Apple binding | 簽章驗證失敗 → 回 401,記 security log + alert | N/A(純後端) | 不影響任何 user state | BR-031;§12.6 | |
| Logical Inconsistency | wport 帳號刪除後 Apple S2S 仍到達:用戶先刪 wport 帳號,幾分鐘後刪 Apple ID,Apple S2S 通知到達但 wport binding 已不存在 | handler lookup binding 失敗 → 標記 event log not_applicable 後回 200 | N/A | N/A | 不報錯、不重複動作 | BR-031;HINT-MUTATION-5 |
10.5 錯誤碼 → UI 對應表(FE 對齊用)
本表為 FE 工程師對接 BE 錯誤碼的 single source of truth。錯誤碼字串以 RD OpenAPI Spec 為準(若與此表名稱有差異,以 OpenAPI 為主,PM 同步更新本表);FE 行為(訊息文案、CTA、是否可重試)為 PRD 規範範疇。
| 錯誤碼 | 觸發來源 | HTTP | FE UI 行為 | 主要文案(zh-TW) | CTA | 可重試 | BR / 對應章節 |
|---|---|---|---|---|---|---|---|
EMAIL_NOT_VERIFIED | HINT-MUTATION-1 進 merge 分支但 email_verified=false | 422 | Modal | 「此 email 尚未經 {provider} 驗證,無法自動連結。請先以原登入方式登入後到設定頁手動綁定。」 | 「我知道了」+ 導回登入頁 | 否(須改流程) | BR-026 |
EMAIL_BELONGS_TO_OTHER_ACCOUNT | HINT-MUTATION-2 新增綁定,provider email 已是他人 primary email | 409 | Modal | 「此 email 已是另一個 wport 帳號的主信箱,請聯絡客服協助合併。」 | 「聯絡客服」(直達 §12.5 合併工單入口)+「取消」 | 否(須客服介入) | BR-027;§12.5 |
PROVIDER_UID_TAKEN | HINT-MUTATION-2 新增綁定,該 provider UID 已綁在別的 wport 帳號 | 409 | Modal | 「此 {provider} 帳號已綁定其他 wport 帳號。如為同一人,請聯絡客服協助轉移。」 | 「聯絡客服」+「取消」 | 否 | §10.4;§12.5 |
STEP_UP_REQUIRED | HINT-MUTATION-3 / 2 缺 step-up token、token 過期、已消耗、或 (action, target_provider) 不匹配 | 403 | Modal + 觸發 step-up flow | 「為了你的帳號安全,請先重新驗證身份。」 | 「重做 {provider} 登入」/「輸入密碼」二選一 | 是(完成 step-up 後重試) | BR-028 |
STEP_UP_REQUIRED + sub_reason=STALE_AUTH | HINT-MUTATION-4 驗到 provider ID Token 的 auth_time > 5 分鐘 | 403 | Modal | 「請重新登入 {provider} 帳號。」 | 「重新登入 {provider}」(FE 觸發 provider sign-out 再重打 popup) | 是 | §8.3 step-up OAuth 新鮮度 |
LAST_LOGIN_METHOD | HINT-MUTATION-3 解綁但會導致 0 登入方式 | 409 | 改為純資訊框(SEC-6 阻擋變體) | 「這是你唯一的登入方式。請先新增其他登入方式,再來解除此項。」 | 「新增其他登入方式」(直達 SEC-4 add binding) | 否 | BR-025 |
RATE_LIMITED | 第三方登入 / 新增綁定 endpoint 觸發 BR-030 | 429 | Toast | 「登入嘗試過於頻繁,請於 {N} 秒後再試。」(N 由 Retry-After header 提供) | 「我知道了」 | 是(等待 window) | BR-030 |
OAUTH_FAILED | provider OAuth 失敗、popup 被擋、用戶拒絕授權 | N/A(FE 端錯誤) | Toast | 「{provider} 登入失敗,請重試或改用其他登入方式。」 | 「重試」+「使用其他方式」 | 是 | §6.2 SEC-1/2 錯誤狀態 |
OAUTH_TIMEOUT | OAuth 流程 > 30s 未完成 | N/A | Toast | 「登入逾時,請重試。」 | 「重試」 | 是 | §10.4 Network & Performance |
SESSION_EXPIRED | 任何受保護 endpoint 收到過期 session | 401 | 自動導向 /login,登入後回原頁 | 「登入已過期,請重新登入。」 | 自動跳轉 | 是 | §10.4 Auth & Lifecycle |
ACCOUNT_DELETION_IN_PROGRESS | HINT-MUTATION-6 已啟動但 L1 cascade 尚未完成(Apple auth/revoke / Google revoke 重試中) | 202 / 409 | 全頁狀態畫面 + 自動 polling | 「帳號刪除處理中,完成後將寄信通知,期間請勿關閉本頁。」 | N/A(後端自動推進) | 是(被動等待) | BR-012 v1.5.0 補強;§12.8 |
BINDING_REVOKED_BY_PROVIDER | 用戶在 Apple 端撤銷授權後嘗試以 Apple 登入;wport 端 binding 已被 S2S 觸發移除 | 404 / 401 | Toast + 引導至「新增登入方式」 | 「你已在 Apple 端取消對 wport 的授權,請改用其他登入方式或重新綁定 Apple。」 | Yes(改用其他方式) | 是 | BR-031;§12.6 |
ACCOUNT_NEEDS_CUSTOMER_SERVICE | Apple S2S consent-revoked / account-delete 後,Apple 為唯一登入方式,用戶嘗試以任何方式登入 | 423 | 全頁 | 「為了保護你的帳號,請聯繫客服協助恢復登入。」 | 「聯絡客服」 | No(須客服介入) | BR-031;§12.5;§12.6 |
FE 對應原則:
- 所有「否(須客服介入)」的錯誤碼,CTA 都必須提供「聯絡客服」直達連結(指向 §12.5 工單入口);不可只顯示錯誤訊息讓用戶卡死
- 所有「是」的錯誤碼,FE 應防止「無限重試導致 BR-030 觸發」:本地端 retry 上限 3 次,第 4 次起改顯示「請稍後再試」
- 文案中
{provider}動態替換為使用者可理解的中文名稱(Google / Apple),不可直接顯示 internal slug
11. Mock Data Schema(Mock 資料結構)
- 資料來源:本期不產出 Storybook mockup;FE 開發 SEC-4 期間若 BE API 未就緒,可使用以下 fixture 進行 UI 對齊。
- CRUD 行為:fixture 僅供 FE 視覺與互動驗證,正式整合須串接後端 API(欄位命名以 BE OpenAPI Spec 為準)。
- 情境切換:FE 可依
scenario切換不同情境驗證 UI 狀態。
11.1 SEC-4「登入方式」綁定清單 Fixture(範例)
重要:以下 JSON 僅為 FE 開發階段對齊用,欄位命名屬範例性質,正式 contract 以 RD OpenAPI Spec 為準。
canUnlink由後端預先計算(避免 FE 重算 BR-025)。⚠️ 命名規則:正式 API 契約一律使用 snake_case(例:
primary_email/has_password/provider_email/is_hide_my_email/first_bound_at/last_login_at/can_unlink/relay_email_status),對齊 wport 既有 API 風格。本節 fixture 使用 camelCase 僅為 PRD 撰寫便利,不代表實際欄位命名。
{
"scenario": "multi-provider-with-password",
"primaryEmail": "john.doe@gmail.com",
"hasPassword": true,
"bindings": [
{
"provider": "google",
"providerEmail": "john.doe@gmail.com",
"isHideMyEmail": false,
"firstBoundAt": "2024-08-12T03:21:00Z",
"lastLoginAt": "2026-05-17T11:08:00Z",
"canUnlink": true
},
{
"provider": "apple",
"providerEmail": "abc123xyz@privaterelay.appleid.com",
"isHideMyEmail": true,
"firstBoundAt": "2025-11-30T19:00:00Z",
"lastLoginAt": "2026-05-10T07:22:00Z",
"canUnlink": true
}
]
}
v1 僅 Google + Apple 兩個 provider;v2 LINE/LinkedIn 上線時,fixture 再行擴充。
情境切換建議(FE 自行 mock):
| scenario | bindings 數 | hasPassword | 預期 UI |
|---|---|---|---|
single-provider-no-password | 1 | false | 該 binding canUnlink=false,按鈕 disabled + hover tip |
single-provider-with-password | 1 | true | binding 可解綁,因 email+password 為 fallback |
multi-provider-no-password | 2+ | false | 全部可解綁 |
multi-provider-with-password | 2+ | true | 全部可解綁 |
apple-hide-my-email-only | 1 (Apple relay) | false | providerEmail 顯示遮罩 + 加註解釋 |
FE 開發時,登入按鈕為純導向 OAuth,無 mock 需求;解綁/新增綁定 endpoint 由 BE 提供 stub 環境。
12. 實作備註(Implementation Notes)
12.1 邊界遵守
- ❌ 本 PRD 不指定:
- API endpoint path / HTTP method
- Token 格式(JWT claims、Apple identity token 結構、LINE access token 結構、step-up token 結構)
- DB schema(資料表名稱、欄位名稱、index 策略)
- Provider SDK 整合方式(Apple Sign in with Apple JS 版本、Google ID Services 版本、實際 npm package 選擇)
- Email normalize / hash 演算法
- Session / Cookie 策略(但要求 session 紀錄須能依 provider 維度查找,見 BR-029)
- Rate limit 實作框架(Redis-cell / rate-limit-flexible / nginx limit_req 皆可,見 BR-030)
- Idempotency key 的構成與儲存形式
- ✅ 本 PRD 指定:
- 商業規則(BR-001
018 對齊 + BR-024031 提議) - 資料語義(要追蹤什麼、為什麼)
- 使用者流程與 UI 行為
- 邊界情境與錯誤訊息
- 安全 gate(email_verified、step-up、rate limit、Token Revocation 二層傳播、Apple S2S 驗章、Apple 首次 email 持久化保護)
- 對外平台合規語意(App Store 5.1.1(v) / GDPR Article 17 / 個資法第 11 條)
- 錯誤碼語意(具體字串由 RD 對齊既有 convention)
- 商業規則(BR-001
12.2 設計決策依據(Design Rationale 摘錄)
| 決策 | 選項對比 | 採用 | SaaS 對標 | wport context 理由 |
|---|---|---|---|---|
| D1 Apple Hide My Email 處理 | A 拒絕 / B 視為獨立身份 | B | Linear, Slack | Apple App Store 規範禁止強迫分享真實 email;強制拒絕會違反平台政策 |
| D2 LINE email scope | A 強制 / B 可選 fallback | A | CakeResume, 104 | 唯一信箱是 wport 帳號模型的核心 invariant,無 email 無法套用 BR-024 |
| D3 自動合併是否顯示確認 | A 確認 modal / B 靜默 / C 自動執行 + Toast | C | Google Account, Stack Overflow | 兼顧「絲滑至上」與透明度,避免用戶日後困惑 |
| 唯一信箱原則 | A 鬆散 / B 嚴格 | B | Google, Stack Overflow, Linear | 履歷身份必須與企業端認知一致,否則應徵歷史會分裂 |
| 防呆解綁機制 | A 開放 / B 至少保留 1 / C 強制改密碼 | B | GitHub, Vercel | 用戶常因「換 LINE 帳號」想解綁,需避免幽靈帳號 |
| Provider 排序 | 按使用率 / 按 platform 規範 | 平台規範優先 | iOS HIG 4.8 | Apple 按鈕必須位於 OAuth 群組首位 |
| D4 Apple 接入方式 | A Apple 原生 Sign in with Apple JS / B Firebase Auth 的 Apple provider | A(v1.6.0 撤回 v1.2.0~v1.5.0 的 B 決定) | Google Sign-In(直連)、Slack(自家 Apple OAuth 接入)、現有 wport Google 路徑 | 詳見下方 D4 補充:(1) Firebase 的兩個原始理由(email 持久化、SPA redirect 痛苦)皆已不再成立;(2) 走 Firebase 反而增加 Token Revocation L2 層 + Account Deletion deleteUser 等工作量;(3) 接 Firebase 多一個 Google 服務保管使用者個資,個資法面多一個負擔 |
| D4-defer LINE/LinkedIn 延後 | A v1 一次全做 / B v1 只做 Apple,LINE/LinkedIn 延後 v2 | B | 階段式上線策略 | 詳見下方 D4-defer 補充:LINE email scope 商業審核時程不可控;LinkedIn 雖支援 OIDC 但仍需 RD spike 驗證 scope / claims 行為 |
| D5 Auto-merge email 驗證 | A 接受未驗證 / B 強制 email_verified=true | B | Google, Auth0, Apple Sign in with Apple | 接受未驗證 email 即為帳號接管漏洞,安全紅線不容妥協 |
| D6 解綁 step-up | A 僅 session 有效 / B 重做 OAuth 或重輸密碼 | B | GitHub, Linear, Vercel | session 竊取即可接管帳號,sensitive action 須額外驗證 |
| D7 解綁後 session 處理 | A 不處理 / B Invalidate 該 provider 來源 sessions | B | Google, GitHub | 不 invalidate 會造成客服困擾,且邏輯與用戶心智不符 |
| D8 自動合併稽核通知 | A 僅 in-app Toast / B 額外寄 audit email | B | Google, AWS, GitHub | 帳號變更類事件須符合 wport 既有稽核標準(BR-018) |
| D9 Rate limit 維度 | A 不設 / B 僅 IP / C IP + email + UID 三維度 | C | Auth0, Okta, Cloudflare Access | 帳號列舉攻擊面 + 攻擊者 IP 池可繞過單維度 |
| D10 Token Revocation 範圍 | A 僅 wport session 層 / B 二層傳播(Provider / wport) | B(v1.6.0 由 v1.5.0 的「三層含 Firebase」收斂為二層) | Google Account, GitHub, Notion / Slack / Linear(皆呼叫 Apple auth/revoke) | 僅清 wport 層會造成 Provider 端授權殘留(GDPR / App Store 5.1.1(v) 風險);改回原生後 Firebase 中介層已不存在,自然收斂為二層 |
| D11 Account Deletion Cascade 範圍 | A 僅 wport DB / B 二層 cascade(Provider revoke + wport 軟刪除) | B(v1.6.0 由 v1.5.0「三層 cascade + Firebase user 刪除」收斂為二層 cascade) | Notion, Slack, Linear(皆呼叫 Apple auth/revoke) | App Store Review Guideline 5.1.1(v) 強制 Apple auth/revoke;GDPR Article 17 被遺忘權;不 cascade 會在 Apple Developer 後台留下殘餘授權,違反平台政策。改回原生後 Firebase Admin deleteUser 步驟自然消失 |
| D12 Apple S2S Notification 訂閱 | A 不訂閱 / B 訂閱並處理 4 種事件 | B | Notion, Slack, Linear, Discord | 不訂閱會造成 wport 與 Apple 端永久 data drift(用戶撤銷後 wport 不知道、Hide My Email 失效後 wport 仍寄信) |
D4 補充:為何 Apple 走原生,而非 Firebase Auth(v1.6.0 撤回 v1.2.0 決定)
背景:v1.2.0~v1.5.0 曾以下列兩個理由選擇 Firebase Auth 的 Apple provider。v1.6.0 由 PM × RD 在 5/20 複審後撤回此決定,改為原生路徑。原始兩個理由與本次複審結論並列如下。
原理由一(已過時 / 言過其實):Apple「Email 只給你一次」
當使用者點擊「使用 Apple 登入」時,Apple 只在「使用者第一次同意該 app 授權」這一次回傳 email;後續授權只回 Apple sub。
- 原 PRD 主張:走 Apple 原生時,若 wport BE 寫 DB 失敗就會永遠遺失該用戶 email,帳號變廢
- 複審結論:這個風險被講得太嚴重。真正需要的保護只是「後端寫 binding 路徑具備持久化保護(retry queue + DLQ)」,無論寫入失敗幾次都不可標 binding 已完成。只要這個保護存在,極端情況下用戶下次再點 Apple 登入,Apple 仍會在用戶授權範圍內的 ID Token 中帶 email(OIDC 即時簽發)。走 Firebase 帶來的保險很有限,且 Firebase 不在這條路徑上保證資料一致性也得自己處理寫 DB 失敗
原理由二(已過時):Apple 原廠 Web 登入需 redirect,打斷 SPA
- 原 PRD 主張:Apple Web 登入只能 redirect + POST callback,徹底打斷 SPA 狀態
- 複審結論:這個理由已過時。Apple 現行 Sign in with Apple JS 已正式支援
usePopup: true,可達成與 Google 相同的 popup UX,完全不需要再經由 Firebase 抽象層
v1.6.0 採用原生的綜合理由:
- 開發量更省:BR-029 由三層收斂為二層、BR-012 cascade 移除 Firebase Admin
deleteUser,最複雜易錯的撤銷層次自然消失 - 架構一致:與既有 Google 路徑同類型(直連 OIDC + JWKS),不再有「Google 一套、Apple 另一套」的雙軌
- 個資法面更單純:少一個 Google 服務(Firebase)保管使用者個資
- 使用者體驗不變:popup UX 兩條路徑相同
- 對應決策複審文件:
pm_39-apple-auth-decision-brief-for-pm.md(2026/05/20)
對應 §10.2 補充說明的「Apple email 只給一次 → 後端寫入持久化保護」與「Apple 第二次起的 lookup」段落。
D4-defer 補充:為何 LINE / LinkedIn 延後 v2
理由一:LINE email scope 需 LINE 商業審核(時程不可控)
LINE Login 預設不會回傳 email;需要在 LINE 開發者後台申請 email permission,由 LINE 進行商業審核(包含說明用途、用戶條款檢核等)。審核時程通常數週至數月,且可能要求補件、調整 UX。
- 若 v1 一起做:上線時程被 LINE 審核 block,整個 PRD 卡關
- 若 LINE 審核未過就上線:所有 LINE 用戶無 email → 無法套用 BR-024 唯一信箱原則 → 變成「LINE 用戶完全無法合併」的退化體驗,違反「絲滑至上」原則
理由二:LinkedIn OIDC 仍需 RD spike 驗證
LinkedIn 已正式支援 “Sign In with LinkedIn using OpenID Connect”(直連 OIDC + JWKS),架構上與 Apple / Google 同類型,但實際接入仍需 RD spike:
- LinkedIn issuer URL、JWKS 端點實際打通的 spike
- scope 行為(
profile、email、openid)與 wport 實際需要的 profile 欄位對齊 - LinkedIn 對 OAuth client 的審核 / app review 要求
v1 不背這個 spike 風險,先用 Google + Apple 把資料模型、安全 gate、Migration 都跑通,v2 LINE 與 LinkedIn 各自接原生 OIDC(每個 provider 為一個 SDK,原則上一期一個)。
v1 → v2 的銜接:
- 資料模型(User Identity Binding)為 N:1 多綁定,v1 就已支援,v2 不需要 schema migration
- BR-024~031 安全 / 生命週期 gate v1 全部實作,v2 直接生效,不需回頭改 auth 核心
- v2 主要工作收斂為「provider OIDC issuer / JWKS 設定」+「FE 加按鈕」+「Compliance 檢核」+「該 provider 自己的 lifecycle webhook 對應條文(如 LINE / LinkedIn 各自的 token revocation 通知機制,v2 PRD 補上)」
12.3 業務規則回寫提議
依 prd-gen Rule 4 / 11,下列規則須在本 PRD 收斂後回寫 business-rules.md:
- BR-024 唯一信箱原則 — 新增
- BR-025 防呆解綁機制 — 新增
- BR-026 Auto-merge 須 email_verified — 新增
- BR-027 新增綁定 cross-account email 衝突 — 新增
- BR-028 Sensitive action step-up auth — 新增
- BR-029 Token Revocation 二層傳播(v1.5.0 由「解綁後 provider session invalidation」擴展為三層;v1.6.0 因 Apple 改回原生收斂為二層)— 新增
- BR-030 OAuth login rate limiting — 新增
- BR-031 Apple Sign in with Apple Server-to-Server Notification 處理 — 新增(v1.5.0)
- BR-001 修訂 — 將 Facebook 改為 Apple/LINE/LinkedIn(移除 Facebook 例舉)
- BR-012 修訂 — (a) 將 Facebook 改為 Apple/LINE/LinkedIn(綁定清除清單);(b) v1.5.0 補上「對外 cascade」條文(Apple
auth/revoke+ Google revoke)+ App Store 5.1.1(v) / GDPR Article 17 引用(v1.6.0 修訂:移除原 FirebasedeleteUser步驟,因 Apple 改回原生路徑無 Firebase user record) - BR-018 修訂 — 補上「帳號變更類事件(含 Apple S2S 觸發之撤銷/刪除、帳號刪除 cascade 完成)須寄 audit 通知信」
12.4 既有 Google 用戶 Migration 策略(本期完成)
重要釐清:本節 migration 僅指「DB 內帳號模型」從單欄位
user.google_id轉為多綁定User Identity Binding表。整個 v1 架構不引入 Firebase(v1.6.0 已撤回 Apple via Firebase 此項決定),Google / Apple 兩條路徑皆直接打 provider JWKS 驗 OIDC ID Token,session 簽發邏輯亦不變(見 §10.2)。
現況:既有 Google 用戶在 DB 內以 user.google_id 欄位形式儲存(單欄位、1:1 模型)。
目標:將既有用戶轉為新的 User Identity Binding 多綁定模型(N:1),但不改變 Google 的 token 驗證與 session 路徑。
Migration 步驟:
-
準備階段(不影響 prod)
- 新建
User Identity Binding結構,與user.google_id並存 - 撰寫 idempotent migration script(可重跑、可中斷)
- 新建
-
雙寫階段(migration 期間)
- 既有 sign-in 流程:同時寫入
user.google_id(既有)與User Identity Binding(新) - 既有 lookup:優先讀
User Identity Binding,找不到時 fallback 讀user.google_id - v1 新增的 Apple binding 只寫入新表(v2 LINE/LinkedIn 上線時亦同)
- 既有 sign-in 流程:同時寫入
-
回填階段
- 執行 migration script:將既有
user.google_id全量回填為User Identity Binding的第一筆(provider=google, UID=既有值) firstBoundAt推算規則(避免錯誤推算):- 若 DB 有
user.google_linked_at(或類似專屬欄位)→ 直接採用 - 否則 → 不要用
user.created_at推算(會把「先 email 註冊、後綁 Google」的用戶誤標為從 day 1 就有 Google),改為留 null + 標記migrated=true,UI 顯示「綁定時間:未知(migration 紀錄)」
- 若 DB 有
firstBoundAt在資料模型層的語意(與 §5.1 對齊):- 欄位允許 null(migration 紀錄);非 migration 來源的新 binding 必須有值
- 必須同時存在
migrated(或等義)flag 標記資料來源;查詢 / UI 不可只看firstBoundAt為 null 即推論「unknown」,須以 flag 為準 - 全系統不可以「
COALESCE(firstBoundAt, user.created_at)」這類偷懶推算覆蓋 null(直接違反本節原意)
- 對帳:count(distinct google_id where not null) == count(binding where provider=google)
- 不修改
user.google_id既有欄位,僅新增 binding 紀錄
- 執行 migration script:將既有
-
切換階段
- lookup 邏輯改為「僅讀 binding 表」(移除 fallback)
- 雙寫期暫時保留,以便 rollback
-
清理階段(本期 release 後 1 sprint)
- 觀察一週無異常 → 移除
user.google_id寫入路徑 - 觀察一個月無異常 → drop
user.google_id欄位(或標為 deprecated 後續移除)
- 觀察一週無異常 → 移除
Owner:RD(PM 僅標示 scope 與時點)
Rollback 策略:雙寫期任何階段都可 rollback;切換階段若發現問題,恢復 fallback 即可
12.5 帳號分裂客服 SOP(v1 必須具備)
存在理由:BR-024 唯一信箱原則是「以 email 為錨」的合併;但以下情境無 email 可錨,會自然分裂出兩個 wport 帳號,且系統設計刻意不允許自助合併(避免誤合併不同人):
- 同一人先以 Apple Hide My Email 註冊(relay email A)、後以 Google 註冊(真實 email B),兩者視為獨立身份
- 同一人在不同公司期間用不同 work email 註冊(B 改 C),多份履歷分散
- 同一 provider UID 被前任用戶轉手給新用戶(Google 帳號買賣 / 公司 SSO 帳號回收),新用戶試圖綁定但碰到
PROVIDER_UID_TAKEN- provider 端極少數的
sub變更(如 Google 帳號合併)上述情境不可讓系統自動合併(會打破 BR-024 / BR-027 的安全邊界),但必須有救援路徑,否則 wport 履歷會永久分裂、業務面傷害大。
12.5.1 觸發入口
- 設定頁 SEC-4「登入方式」區塊底部新增「帳號合併申請」連結,導向客服工單系統
- HINT-MUTATION-2 / HINT-MUTATION-3 在回傳
EMAIL_BELONGS_TO_OTHER_ACCOUNT/PROVIDER_UID_TAKEN時,FE 必須提供「聯絡客服」CTA(見 §10.5)
12.5.2 工單必填資訊(用戶端)
- 申請人 wport user_id(從 session 自動帶)
- 欲合併之另一帳號識別(任一即可):另一 primary email、另一 binding 的 provider + 該 provider email
- 自證為同一人:擇一 — (a) 從另一帳號登入並按下「我是同一人」連結;(b) 提供身份證明(依 wport 既有客服身份驗證流程)
12.5.3 客服端處理流程
- 核驗身份:依 wport 客服 SOP 確認兩個帳號擁有者為同一人(沿用既有 KYC 流程)
- 預檢衝突:兩帳號是否有矛盾資料(同時為求職者 + 企業 owner?同時擁有不同進行中應徵?)→ 有則人工裁定保留方向
- 執行合併(後端提供 internal admin 工具,PRD 不規範 admin API,僅要求工具具備下列能力):
- 指定 source / target 帳號
- 將 source 的所有
User Identity Binding轉移到 target - 將 source 的履歷、應徵歷史依「保留方向」歸戶到 target
- source 帳號軟刪除(沿用 BR-012 軟刪除語意)
- 寫入
Auth Audit Notification Log,動作類別account_merge_by_support - 寄送 audit 通知信至 source 與 target 兩端 primary email
- 不可逆性說明:合併執行前客服必須出示確認頁;合併後 7 天內可由客服 rollback(軟刪除復原),7 天後 hard delete source
12.5.4 已知 accepted risk
- 合併操作為客服權限,存在內部濫用風險 → 依賴 wport 既有 admin audit log + 雙人複核流程(不在本 PRD 範疇,沿用既有 admin governance)
- 合併過程是非同步操作(可能耗時數秒到數十秒),期間用戶可能仍以 source 登入 → admin 工具在啟動合併時應先 invalidate source 全部 session 並標記「合併中」狀態,HINT-MUTATION-1~3 對該帳號回 503
12.5.5 v1 上線標準
- ✅ 客服工單表單上線(包含必填欄位 §12.5.2)
- ✅ Admin 合併工具具備 §12.5.3 第 3 步全部能力
- ✅ 客服 FAQ 與內訓文件涵蓋 SOP
- ⚠️ Admin 工具 UI 可延後一週(暫以 RD 手動 script 執行),但能力必須 v1 就具備
- 📌 v1 必做「能力」的具體形式可採 CLI(例如
npm run admin:merge-accounts/npm run admin:rollback-merge):CLI 形式工本最低、攻擊面最小(OS-level 權限隔離),同時AccountMergeService抽出共用 service,未來 AMS UI 或 HTTP endpoint 接手時呼叫同一份 service、0 程式重寫。HTTP admin endpoint / AMS UI 屬 v1.x 範圍
12.6 Apple Sign in with Apple Server-to-Server Notification 處理(BR-031)
存在理由:Apple 為「Sign in with Apple」訂閱者提供 server-to-server lifecycle webhook,用以告知 wport「用戶在 Apple 端做了什麼影響此 binding 的事」。不訂閱會造成 wport 與 Apple 端永久 data drift(用戶撤銷後 wport 不知道;Hide My Email 失效後 wport 持續寄信)。
12.6.1 webhook 註冊
- 端點選擇:PRD 不指定 endpoint 路徑(屬 RD OpenAPI Spec);建議為 wport 後端自有 endpoint(不經中介轉發),以利日誌與 metric 收斂
- 註冊位置:Apple Developer Portal > Certificates, Identifiers & Profiles > Identifiers > [wport Services ID] > Sign In with Apple > Server-to-Server Notification Endpoint
- owner:RD lead(與 Apple Developer Portal Services ID 設定同期執行;見 §16 #16)
12.6.2 安全性驗證(PRD 語意層必須達成)
- JWT 簽章驗證:以 Apple JWKS(
https://appleid.apple.com/auth/keys)驗外層 JWT 簽章;簽章不過 → 401 iss驗證:必須 =https://appleid.apple.comaud驗證:必須 = wport 的 Apple Services ID(PRD 不寫死值,RD 從 config 讀)- 內層
eventsclaim 解析:Apple 包了兩層 JWT;外層為 transport,內層events才是真正 payload - Idempotency:以內層
transaction_id為 key 去重(Apple Provider Lifecycle Event Log) - 時間視窗:
event_time若距今 > 7 天視為過期事件,記錄但不執行 cascade(防止 replay / 補送的舊事件造成意外副作用)
12.6.3 4 種 event_type 處理矩陣
| event_type | 是否影響 binding | 是否觸發 BR-029 二層傳播 | 是否寄通知信 | UI 影響 |
|---|---|---|---|---|
email-disabled | 否(僅標記 relay 狀態) | 否 | 否(屬於低優先資訊) | 下次登入後 banner nudge |
email-enabled | 否 | 否 | 否 | 清除 banner |
consent-revoked | 是(移除 binding) | 是(含 L1 Apple revoke best-effort、L2 wport session invalidate) | 是 | 設定頁該 binding 消失;用戶下次嘗試以 Apple 登入會碰到 BINDING_REVOKED_BY_PROVIDER |
account-delete | 是(移除 binding) | 是(同 consent-revoked) | 是 | 同上;額外:若 Apple 為唯一登入方式 → 帳號標記 needs_customer_service_intervention |
12.6.4 唯一登入方式的特殊情境
當 consent-revoked / account-delete 觸發時,若該用戶 Apple 為唯一登入方式:
- 不沿用 BR-025 防呆(外部已 authoritative,不能阻擋 Apple 端決定)
- 執行 binding 移除 + 二層傳播
- 將 wport user 標記為
needs_customer_service_intervention狀態,下次任何 sign-in 嘗試(不可能成功,因為已無 binding)會被導向 §12.5 客服恢復路徑 - 寄通知信至 primary email;若 primary email 為 Apple relay 已失效 → 信件會被退回 / 用戶收不到,屬已知缺口(記入 §15 accepted risk)
12.6.5 處理時效與 SLA
- webhook handler 必須 ≤ 5 秒回 200(Apple 超時視為失敗會重試,最壞會 spam)
- 重活動(binding 移除 + 二層傳播 + 寄信)排到後端 queue 異步處理
- end-to-end SLA:Apple 發送 → wport 完成處理 ≤ 24 小時(PRD 立場;實際數值由 RD 在系統設計中收斂)
12.7 Token Revocation 二層傳播(BR-029)
存在理由:v1 兩條 provider 路徑(Google / Apple)皆採直連原生,OAuth 授權鏈僅存在「Provider 端」與「wport 端」兩層 cached state。
v1.5.0 → v1.6.0 變更:v1.5.0 曾因 Apple via Firebase 引入「Provider / Firebase / wport」三層;v1.6.0 Apple 改回原生後,Firebase 中介層消失,BR-029 收斂為二層。
12.7.1 二層定義
| 層 | 對象 | 動作 | 適用 provider |
|---|---|---|---|
| L1 — Provider 端 | Google / Apple 自家 OAuth grant 與 refresh token | 呼叫 provider revoke endpoint | Google:https://oauth2.googleapis.com/revoke;Apple:https://appleid.apple.com/auth/revoke |
| L2 — wport 端 | wport 自家 session / API token / refresh token | invalidate Session Provider Index 中該 provider 來源所有 records + 派生 child token | 全部 provider |
12.7.2 失敗處理矩陣
| 層 | unbind(ACT-8) | delete(ACT-9 / HINT-MUTATION-6) | S2S consent-revoked / account-delete(ACT-10) |
|---|---|---|---|
| L1 | best-effort + audit log | 必須成功(Apple:App Store 5.1.1(v) 強制;Google:GDPR 資料極小化,重試 N 次仍失敗則 fallback 為 audit log + admin DLQ) | best-effort(事件來源已 authoritative,再呼叫 revoke 是 defensive 補強而非合規要求) |
| L2 | 必須成功 | 必須成功 | 必須成功 |
12.7.3 對 FE 的回應時機
- unbind:L2 成功即可回 FE「解綁完成」;L1 異步處理由
Revocation Propagation Job追蹤 - delete:必須 L1+L2 全部完成才能回 FE「刪除完成」;期間 FE 顯示
ACCOUNT_DELETION_IN_PROGRESSpolling 狀態 - S2S 觸發:無 FE 直接互動;用戶在下次登入或設定頁訪問時看到狀態變更
12.7.4 觀測性需求(不強制 v1 指標數值)
- 必須能查詢「某 user 某次 unbind / delete 對應的二層完成狀態」(GDPR audit 舉證)
- 必須有 metric:L1 / L2 失敗率、重試次數分佈、超過 N 次仍失敗的 job 進 DLQ alert
- L1(delete 情境)/ L2 失敗 job 累積到一定閾值 → oncall alert
12.8 Account Deletion Cascade(BR-012 v1.5.0 補強;v1.6.0 由三層收斂為二層)
存在理由:BR-012 既有條文只規範「wport DB 內 binding 刪除」;對外 cascade 缺失會違反 App Store Review Guideline 5.1.1(v)(Sign in with Apple 帳號刪除規範)與 GDPR Article 17(被遺忘權)/ 個資法第 11 條(資料刪除請求)。
v1.6.0 變更:v1.5.0 曾因 Apple via Firebase 規定 cascade 中含 Firebase AdmindeleteUser;v1.6.0 Apple 改回原生後,此步驟自然消失(無 Firebase user record 需清除),cascade 收斂為「Provider revoke + wport 軟刪除」二層。
12.8.1 完整 cascade 步驟(HINT-MUTATION-6 期望行為)
- 既有 BR-012 pre-check:Owner 檢查、進行中應徵、企業 owner 狀態(不變)
- 對每個 binding 觸發 BR-029 二層傳播,且所有層皆為「必須成功」:
- Apple binding:L1
auth/revoke必須成功(App Store policy)+ L2 session invalidate 必須成功 - Google binding:L1 Google revoke 必須成功(GDPR data minimization;重試 N 次仍失敗則 fallback 為 audit log + admin DLQ)+ L2 session invalidate 必須成功
- Apple binding:L1
- wport DB 處理:依 BR-012 既有流程處理 user 軟刪除、履歷、應徵歷史等
- audit log:寫入
Revocation Propagation Job(最終狀態)+Auth Audit Notification Log(cascade 完成記錄) - 通知信:寄送帳號刪除完成通知至 primary email
12.8.2 合規依據
| 規範 | 條文要點 | 對應 cascade 步驟 |
|---|---|---|
| App Store Review Guideline 5.1.1(v) | 「Apps that support account creation through third-party login services (such as Sign in with Apple) must call the appropriate API to delete the user’s record from the app’s database」 | Apple L1 auth/revoke(撤銷 Apple 端授權 + refresh token)+ wport 端 user record 軟刪除 |
| GDPR Article 17(Right to Erasure) | 用戶有權要求 controller 刪除其個人資料,且 controller 須在合理時間內通知所有持有該資料的 processor | Apple/Google L1 對外撤銷;wport 端軟刪除;audit trail 留證 |
| 個資法(台灣)第 11 條第 3 項 | 個人資料正確性有爭議或目的消失時,當事人可請求刪除,公務機關或非公務機關應主動刪除、停止處理或利用 | 同上 |
12.8.3 失敗處理與 UI
- L1 Apple
auth/revoke失敗 → 不標記帳號刪除完成;queue 重試;UI 顯示ACCOUNT_DELETION_IN_PROGRESS,禁止讓用戶以為「已刪但其實 Apple 端還持有授權」 - L1 Google revoke 失敗 → 重試 N 次後 fallback 為 audit log + admin DLQ 人工介入(accepted residual risk;wport 內部已切斷且 Google 不在 App Store 規範下強制)
- DLQ 中超過 SLA 仍未完成的 cascade → 進 admin 人工介入清單(沿用 §12.5 admin tool 體系)
12.8.4 與 §12.5 客服 SOP 的介接
- 客服合併操作(§12.5.3)中的 source 帳號軟刪除步驟,同樣須跑完整 cascade(merge 也是一種 delete 變形)
- admin tool 必須支援「補執行 cascade」action,供 DLQ 中 stuck job 由人工推進
12.8.5 v1 上線標準
- ✅ HINT-MUTATION-6 cascade 步驟全部上線(含 L1 Apple
auth/revoke必須成功、L1 Google revoke 必須成功) - ✅
Revocation Propagation Job追蹤表 + 重試 queue 上線 - ✅
ACCOUNT_DELETION_IN_PROGRESSUI 上線(FE) - ✅ Audit trail 可供 GDPR / 個資法舉證(具體查詢介面可後 v1 一週上線,但資料必須 v1 留存)
13. 下一步(Next Steps)
v1(本期)
- 本 PRD 簽核 — Eric 確認後進入 Stage 9(Coda 回寫 + git push)
- RD 撰寫 OpenAPI Spec — 依本 PRD 的 §5 資料語義、§7 動作清單、§8 hard gate,定義 endpoint、payload、錯誤碼(含
EMAIL_NOT_VERIFIED/EMAIL_BELONGS_TO_OTHER_ACCOUNT/STEP_UP_REQUIRED/LAST_LOGIN_METHOD/PROVIDER_UID_TAKEN/ 429 等) - RD Apple Developer Portal 設定 — Apple Services ID 申請、Sign in with Apple Key / Team ID 設定、Service Domain & Return URL 填入、Server-to-Server Notification Endpoint 註冊(取代原 v1.5.0 的「Firebase 後台啟用 Apple provider」任務)
- RD Migration script — 撰寫既有 Google 用戶
user.google_id回填腳本(§12.4,注意firstBoundAt推算規則) - FE 設計圖 — Figma 補上 SEC-4 / SEC-5 / SEC-6 的設計稿(含 step-up modal)
- business-rules.md 回寫 — 新增 BR-024~031,修訂 BR-001 / BR-012 / BR-018
- QA 撰寫 E2E — 至少覆蓋 §10.4 中所有 Logical Inconsistency 情境,含 BR-026~031 新增安全 / 生命週期 gate(含 Apple S2S 偽造防禦、二層 revocation L1/L2 失敗、Apple 首次 email 寫入失敗持久化、帳號刪除 cascade 中斷)
- Compliance 檢核(v1) — Apple App Store 4.8 / 5.1.1(v) 規範審查
- 客服文件補充 — 設定頁登入方式如何使用、解綁後如何救援、Apple Hide My Email 是什麼
- Email Template — 新增「自動合併」、「新增綁定」、「解綁」、「Apple 端撤銷/刪除觸發」、「帳號刪除 cascade 完成」五種 audit 通知信模板
- 監控 dashboard — 上線 BR-030 rate limit 觸發、BR-026 merge_blocked、BR-029 二層傳播 L1/L2 失敗率、BR-031 Apple S2S handler 處理 lag、HINT-MUTATION-6 cascade DLQ、Apple 首次 email 寫入 retry queue 深度等安全 metric
- GDPR / 個資法合規 review — Legal 確認 §12.8 cascade 步驟符合個資法第 11 條與 GDPR Article 17 舉證要求;audit trail 保留期由 Legal 給出建議值
v2(下一期,建立獨立 PRD 接續,LINE / LinkedIn 各自一期,不一次做完)
- LINE email scope 商業申請 — 先行送件,等審核期(不 block v1)
- LinkedIn OIDC spike — RD 直接以 LinkedIn issuer + JWKS 驗證 ID Token 在 dev 環境跑通;確認
profile/email/openidscope 行為符合 wport 需求 - v2 PRD(建議分兩期,LinkedIn 與 LINE 各一期,依商業優先順序與審核就緒度決定先後) — 補上該 provider 的按鈕、edge case、Compliance 章節(LINE Login terms / LinkedIn Developer Agreement);BR-024~031 沿用 v1,不重新設計(注意:BR-031 Apple S2S 為 Apple 平台專屬,LINE / LinkedIn 各自的 lifecycle webhook / token 失效通知機制須在各自 v2 PRD 補上對應條文)
- v2 Compliance — LINE Login terms 對齊、LinkedIn Developer Agreement 對齊
14. 一致性表(Stage 6 必填)
14.1 欄位對照表(Field Mapping)
| 概念 | 本 PRD 用詞 | Storybook | business-rules.md | SoT | 版本 |
|---|---|---|---|---|---|
| 第三方綁定 | User Identity Binding | N/A | ”第三方登入綁定 / User Providers” (BR-012) | PRD | v1 |
| 唯一信箱 | Primary Email | N/A | 待 BR-024 新增 | PRD | v1 |
| 自動合併紀錄 | Merge Event Log | N/A | 待新增 | PRD | v1 |
| 登入方式數 | Login Method Diversity | N/A | 待 BR-025 新增 | PRD | v1 |
| Provider 來源 session 索引 | Session Provider Index | N/A | 待 BR-029 新增 | PRD | v1 |
| Audit 通知信紀錄 | Auth Audit Notification Log | N/A | 待 BR-018 修訂 | PRD | v1 |
| Step-up 一次性 token | Step-Up Verification Token | N/A | 待 BR-028 新增 | PRD | v1 |
| Apple S2S 事件紀錄 | Apple Provider Lifecycle Event Log | N/A | 待 BR-031 新增 | PRD | v1 |
| 二層撤銷追蹤 | Revocation Propagation Job | N/A | 待 BR-029 新增(v1.5.0 曾擴展為三層、v1.6.0 收斂為二層) | PRD | v1 |
| Apple relay 狀態 | Apple Relay Email Status | N/A | 待 BR-031 新增 | PRD | v1 |
14.2 常數表(Constants)
| 常數 | 數值 | 來源 | 備註 |
|---|---|---|---|
| 自動合併 Toast 顯示秒數 | 5 秒 | PRD §6.2 SEC-5 | FE 常數 |
| OAuth 超時 | 30 秒 | PRD §10.4 | 沿用既有 Google 設定 |
| Apple OAuth 重試次數(FE 端 popup 失敗 / token 取得 5xx 時) | 1 次 | PRD §10.4 | RD 確認 |
| 支援 provider 數(v1) | 2 (Google + Apple) | PRD §3.2 | v2 擴充至 4 |
| Provider 按鈕排序(v1) | Apple → Google | PRD §6.2 SEC-1/2 | Apple 須首位(iOS HIG 4.8);v2 擴充為 Apple → Google → LinkedIn → LINE |
| Apple relay email domain | *@privaterelay.appleid.com | Apple 官方 | 用於 D1 判定 |
| Rate limit per IP | 60 次 / 分鐘 | PRD §4.2 BR-030 | sliding window;起步值,以 prod traffic 觀察後微調(討論交由 RD + DevOps,企業 NAT 共用 egress IP 可能需放寬) |
| Rate limit per email | 10 次 / 分鐘 | PRD §4.2 BR-030 | sliding window |
| Rate limit per provider UID | 10 次 / 分鐘 | PRD §4.2 BR-030 | sliding window |
| Rate limit spike alert | 單一 IP > 600 次 / 5 分鐘 | PRD §4.2 BR-030 | 觸發 oncall alert |
| Step-up token TTL | ≤ 5 分鐘 | PRD §8.2 HINT-MUTATION-4 | 一次性;RD 可調整下限 |
| Apple S2S webhook 回應時限 | ≤ 5 秒 | PRD §12.6 | Apple 超時視為失敗會重試 |
| Apple S2S end-to-end SLA | ≤ 24 小時(Apple 發送 → wport cascade 完成) | PRD §12.6 / S8 | PRD 立場;實際數值由 RD 在系統設計收斂 |
Apple S2S event_time 過期視窗 | > 7 天視為過期 | PRD §12.6.2 | 防 replay;過期事件記錄但不執行 cascade |
| BR-029 二層傳播重試策略 | L2 必須持續至成功;L1 unbind 為 best-effort + audit log;L1 delete(Apple)必須成功;L1 delete(Google)重試 N 次仍失敗 fallback admin DLQ | PRD §12.7 | DLQ 設計由 RD 決定 |
| Account Deletion cascade UI polling 間隔 | 由 FE 決定(建議 3–5 秒) | PRD §10.5 ACCOUNT_DELETION_IN_PROGRESS | FE 常數 |
14.3 事件字典(Event Dictionary)
| 事件名稱 | 觸發來源 | 後端寫入 | payload 概念 | 用途 |
|---|---|---|---|---|
auth.signin.success | provider OAuth 完成 + token 驗證通過 | 否(log only) | user_id, provider, isNewUser, merged | 行為分析 |
auth.binding.merged | 自動合併發生 | 是(Merge Event Log + Audit Log) | user_id, new_provider, old_providers | Toast 顯示、客服回溯、audit 信 |
auth.binding.merge_blocked | merge 因 email_verified=false 被擋(BR-026) | 是(security log) | attempted_provider, provider_email_masked, ip | 安全監控、攻擊偵測 |
auth.binding.created | 設定頁手動新增綁定 | 是(Audit Log) | user_id, provider | 稽核、audit 信 |
auth.binding.creation_blocked | 新增綁定因 cross-account email(BR-027) | 是(security log) | user_id, provider, masked_email | 安全監控 |
auth.binding.removed | 設定頁解綁 | 是(Audit Log) | user_id, provider, invalidated_session_count | 稽核、audit 信 |
auth.binding.removal_blocked | 解綁觸發 BR-025 | 否(log only) | user_id, attempted_provider | 行為分析 |
auth.stepup.required | sensitive action 缺 step-up token | 否(log only) | user_id, action | 行為分析 |
auth.stepup.success | step-up 驗證通過 | 是(log) | user_id, action, method (oauth/password) | 稽核 |
auth.ratelimit.triggered | BR-030 rate limit 觸發 | 是(security log) | dimension, key_masked, ip | 安全監控、調整閾值 |
auth.apple_s2s.received | Apple S2S webhook 收到(簽章已驗) | 是(Apple Provider Lifecycle Event Log) | transaction_id, event_type, apple_sub_hash | idempotency 去重、處理 lag 監控 |
auth.apple_s2s.processed | Apple S2S 事件處理完成 | 是(Apple Provider Lifecycle Event Log 狀態更新) | transaction_id, event_type, result (done / not_applicable / failed), user_id | end-to-end SLA 監控 |
auth.apple_s2s.signature_failed | Apple S2S 簽章 / iss / aud 驗證失敗 | 是(security log) | source_ip, claimed_iss, claimed_aud | 偽造攻擊偵測 |
auth.revocation.l1_failed | Provider 端 revoke 失敗 | 是(Revocation Propagation Job) | user_id, provider, trigger (unbind/delete/s2s), retry_count | DLQ 監控、合規舉證 |
auth.revocation.l2_failed | wport session invalidate 失敗 | 是(Revocation Propagation Job) | user_id, retry_count | DLQ 監控、內部一致性風險 |
auth.revocation.completed | 二層傳播全部完成 | 是(Revocation Propagation Job 終態) | user_id, provider, trigger, duration_ms | end-to-end 觀測 |
auth.account.deleted.cascade_completed | HINT-MUTATION-6 完整 cascade 完成 | 是(Auth Audit Notification Log) | user_id, providers, total_duration_ms | GDPR / 個資法 audit 舉證 |
auth.apple_first_email_write_retry | Apple 首次授權 email 寫入 binding 失敗,已排入 retry queue | 是(security / ops log) | user_id_or_none, attempt_count | 偵測 DB 故障或 schema 異常導致 email 持續寫不入 |
同一事件名不可有不同 payload;命名以 wport 既有事件命名 convention 為準(RD 對齊)。
14.4 術語字典(Terminology)
| 術語 | 定義 | 同義詞 / 不可混用 |
|---|---|---|
| Provider | 第三方登入服務商(Google / Apple / LINE / LinkedIn) | ✅ 可稱「登入方式」於 UI;❌ 不要稱「社交登入」(不準確) |
| Binding | 一個 wport 用戶與一個 provider 帳號的連結 | ✅ 同「綁定」 |
| Merge | 自動將新 provider 綁定到既有 wport 用戶 | ❌ 不要稱「整合」、「同步」 |
| Primary Email | wport 帳號的核心 email,唯一信箱原則的主鍵 | ❌ 不要稱「主信箱」於 UI(用「帳號 email」) |
| Relay Email | Apple Hide My Email 產生的匿名轉信 email | 僅技術術語;UI 用「Apple 匿名 email」 |
| Onboarding | 第一次登入後填寫個人資料的流程 | ✅ 同「新手引導」 |
15. 版本凍結表(Version Freeze)
| 項目 | v1 / v2 | Blocking? | 理由 | 拍板人 | 日期 |
|---|---|---|---|---|---|
| v1 範圍:Google + Apple 2 provider | v1 | ✅ | LINE / LinkedIn 延後 v2(理由見 §12.2 D4-defer) | Eric | 2026/05/18 |
| LINE / LinkedIn | v2 | ❌(v1 不 blocking) | LINE email scope 商業審核時程不可控;LinkedIn OIDC 需 spike + GCIP 付費 tier 評估 | Eric | 2026/05/18 |
| Email + Password 流程改動 | 不在本期 | ❌ | 沿用既有 | Eric | 2026/05/18 |
| 2FA / MFA | v2+ | ❌ | 屬於另一期帳號安全強化 | Eric | 2026/05/18 |
| 企業 SSO(SAML / OIDC) | v2+ | ❌ | 企業客製範圍 | Eric | 2026/05/18 |
| BR-024 / BR-025 回寫 | v1 | ✅ | 須與 PRD 同期生效 | Eric | 2026/05/18 |
| BR-026~030 回寫(auto-merge email 驗證 / cross-account 衝突 / step-up / session invalidation / rate limit) | v1 | ✅ | 安全 hard gate,須與 PRD 同期生效 | Eric | 2026/05/18 |
| BR-001 / BR-012 修訂(移除 Facebook 例舉) | v1 | ⚠️ 中 | 不影響 RD 開發,但須與 PRD 同期 release | Eric | 2026/05/18 |
| BR-018 修訂(補上帳號變更 audit 通知信) | v1 | ✅ | 與 BR-026~030 同期 | Eric | 2026/05/18 |
| v1 平台範圍:Web + iOS native app | v1 | ✅ | Android app 尚未開發,不在本 PRD 範圍。Apple Sign-In 無官方 Android native SDK;Android 第三方登入待 Android app 立項時另開 PRD | Eric | 2026/05/22 |
Apple ID Token aud 白名單雙值 | v1 | ✅ | Web aud = Apple Services ID;iOS native aud = iOS Bundle ID;BE 必須同時接受兩者,由 config 讀取不可寫死(§8.3 / §8.2 HINT-MUTATION-1) | Eric | 2026/05/22 |
| Apple / Google ID Token nonce 強制驗證 | v1 | ✅ | FE 產 nonce → hash → OAuth request → BE 驗章比對;iOS native ASAuthorizationAppleIDProvider 同等要求(§8.3) | Eric | 2026/05/22 |
| Rate limit 對驗章失敗請求計 IP quota | v1 | ✅ | 攻擊者送無效 token 仍消耗 per IP quota,否則 post-verify limit 可被繞過(§8.3) | Eric | 2026/05/22 |
| Step-up token 消耗時機:操作成功才作廢 | v1 | ✅ | 業務阻擋(client error)不消耗 token;server error / 驗章失敗才作廢(§8.3)。原「無論成功失敗皆作廢」修訂為此 | Eric | 2026/05/22 |
| Apple 採原生 Sign in with Apple(不經 Firebase) | v1 | ✅ | v1.2.0~v1.5.0 曾選擇 Firebase Auth,5/20 決策複審後確認改回原生:(1) Firebase 兩個原始理由(email 持久化、SPA redirect)已不再成立或言過其實;(2) 走原生反而省下 BR-029 L2 Firebase 層 + Account Deletion deleteUser 等工作量;(3) 個資法面少一個 processor(§12.2 D4) | Eric | 2026/05/22 |
| Google 沿用既有直連 OAuth 框架 | v1 | ✅ | 既有 Google 框架穩定且無新增功能訴求;不變更(§10.2 補充說明) | Eric | 2026/05/18 |
FE 互動鎖定:Web popup-only / iOS native 系統彈窗(v1 不引入 /auth/callback/ redirect) | v1 | ✅ | Web:避免 SPA 狀態被 redirect 打斷;Apple Sign in with Apple JS 已支援 usePopup: true。iOS native:ASAuthorizationAppleIDProvider / Google Sign-In SDK 原生系統彈窗,不使用 popup(§10.3) | Eric | 2026/05/22 |
| LINE / LinkedIn 採與 Apple 同類型原生 OIDC(不引入第三方託管層) | v2 | ❌(v1 不 blocking) | 架構決策保留至 v2;v2 啟動前由 RD spike 驗證 LinkedIn 直連 OIDC scope / claims | Eric | 2026/05/22 |
| 既有 Google 用戶 migration(雙寫 → 切換 → 清理) | v1 | ✅ | 本期完成,不延到下期。僅指 DB 內帳號模型 reshape(§12.4) | Eric | 2026/05/18 |
| Provider 按鈕排序(v1):Apple → Google | v1 | ✅ | iOS HIG 4.8 規範;v2 擴充為 Apple → Google → LinkedIn → LINE | Eric | 2026/05/18 |
Apple provider_uid = OIDC sub(取自 Apple JWKS 驗證後的 ID Token sub claim) | v1 | ✅ | 避免被 client 端傳入值或非 sub claim 取代;v2 LINE / LinkedIn 沿用同一原則(§5.1) | Eric | 2026/05/22 |
Step-up token 綁定 (user, action, target_provider) 三元組 + 一次性消耗 | v1 | ✅ | 防止 step-up token 跨 action / 跨 provider 被重用(BR-028 / §8.3) | Eric | 2026/05/20 |
Step-up OAuth 須驗 auth_time ≤ 5 分鐘 | v1 | ✅ | 防止 client-side SDK 從既有 session reissue 老 token 即被視為通過 step-up(§8.3) | Eric | 2026/05/22 |
| BR-029 session invalidate 採「同 provider 來源」最小範圍 + 已知 accepted risk | v1 | ✅ | UX vs 安全 trade-off 明示;殘餘風險靠 BR-028 step-up 攔截(§8.3) | Eric | 2026/05/20 |
firstBoundAt allow null + migrated flag 配對 | v1 | ✅ | 與 §12.4 migration 策略一致,禁止用 created_at 偷算 | Eric | 2026/05/20 |
| §10.5 錯誤碼 → UI 對應表 | v1 | ✅ | FE 對齊 single source of truth;BE 錯誤碼字串以 OpenAPI 為準 | Eric | 2026/05/20 |
| §12.5 帳號分裂客服 SOP(含 admin 合併能力) | v1 | ✅ | 不自助合併(守 BR-024/027 邊界),但須有客服救援路徑,否則履歷永久分裂 | Eric | 2026/05/20 |
| BR-030 fail-open 分層:pre-verify per IP → fail-open;post-verify per email/UID → fail-closed | v1 | ✅ | 一刀切 fail-open 會把 BR-030 安全價值架空(§4.2 / §8.3 / §10.4) | Eric | 2026/05/20 |
| BR-029 採二層 Revocation Propagation:L1 Provider / L2 wport | v1 | ✅ | v1.5.0 曾因 Apple via Firebase 規範三層;v1.6.0 Apple 改回原生,Firebase 中介層消失,自然收斂為二層(§12.7) | Eric | 2026/05/22 |
| BR-029 二層傳播失敗處理矩陣:unbind L1 best-effort / delete L1 Apple 必須成功 / delete L1 Google 必須成功(重試 N 次仍失敗則 fallback admin DLQ)/ L2 一律必須成功 | v1 | ✅ | App Store 5.1.1(v) 對 Apple delete 強制;GDPR 對 wport 端強制(§12.7) | Eric | 2026/05/22 |
| BR-031 Apple S2S Notification 接收能力 + 4 種 event_type 處理 | v1 | ✅ | 不訂閱會造成 wport 與 Apple 端永久 data drift(§12.6)。此為 Apple 平台機制,與是否經 Firebase 無關 | Eric | 2026/05/20 |
Apple S2S consent-revoked / account-delete 不適用 BR-025 / BR-028(外部來源 authoritative) | v1 | ✅ | 阻擋外部 authoritative 事件等同 wport 自行越權;用唯一登入方式情境改走 §12.5 客服恢復(§12.6.4) | Eric | 2026/05/20 |
Account Deletion Cascade:對外 cascade 含 Apple auth/revoke(必須成功)+ Google revoke(必須成功,重試 N 次仍失敗 fallback admin DLQ) | v1 | ✅ | App Store 5.1.1(v) + GDPR Article 17 + 個資法第 11 條(§12.8.2)。v1.5.0 曾規範額外的 Firebase deleteUser,v1.6.0 改回原生後此步驟消失 | Eric | 2026/05/22 |
| Apple 首次授權 email 寫入持久化保護:DB 寫入失敗必須排 retry queue / DLQ 至成功,不可僅標 binding 完成但 email 為 null | v1 | ✅ | 取代原 v1.2.0「以 Firebase 託管 email 解決」之保險;改回原生後此保護成為硬性需求(§5.2 / §8.3 / §10.2 補充說明) | Eric | 2026/05/22 |
Apple S2S webhook 安全性:JWT 簽章驗證 + iss + aud + transaction_id idempotency + event_time ≤ 7 天 | v1 | ✅ | 防偽造、防 replay、防重複處理(§12.6.2) | Eric | 2026/05/20 |
Apple S2S email-disabled 用戶 nudge 機制 | v1 | ✅ | 避免 wport 長期寄信到失效 relay(§12.6.3) | Eric | 2026/05/20 |
ACCOUNT_DELETION_IN_PROGRESS UI 狀態:cascade 未完成前禁止對用戶宣稱「已刪除」 | v1 | ✅ | 避免「wport 標示已刪但 Apple 端仍持有授權」此類合規誤導(§12.8.3) | Eric | 2026/05/20 |
| Apple relay 失效後通知信寄不到 | v1 | ⚠️ accepted risk | 用戶若關閉 Apple Hide My Email 轉信且 primary email 為 relay → 通知信會被退回;屬已知缺口,待 v1.x 補「強制設定可收信 email」此 UX 弱化此風險(§12.6.4) | Eric | 2026/05/20 |
無未解決的 blocking 項目。
16. Sync-fix 清單
| # | 對象 | 動作 | Owner | Blocking? |
|---|---|---|---|---|
| 1 | business-rules.md | 新增 BR-024 唯一信箱原則 | Eric(PM) | ✅ 是(v1 release 前完成) |
| 2 | business-rules.md | 新增 BR-025 防呆解綁機制 | Eric(PM) | ✅ 是 |
| 3 | business-rules.md BR-001 | 將「Facebook」例舉改為「Apple」(v2 再補 LINE/LinkedIn) | Eric(PM) | ⚠️ 中 |
| 4 | business-rules.md BR-012 | provider 綁定清除清單修訂(v1 補 Apple;v2 補 LINE/LinkedIn) | Eric(PM) | ⚠️ 中 |
| 5 | Coda i-R_41yO3CdG | 將 PRD 欄位設為 PM_39 | Claude / Eric | ✅ 是(Stage 9) |
| 6 | RD OpenAPI Spec | 依 §5、§7、§8 撰寫 endpoint 規格 | RD lead | ✅ 是(開發前) |
| 7 | Figma | 補上 SEC-4 / SEC-5 / SEC-6 設計稿 | Designer | ⚠️ 中 |
| 8 | QA Test Plan | E2E 涵蓋 §10.4 全部 Logical Inconsistency | QA | ⚠️ 中 |
| 9 | 客服 FAQ | 補上「解綁救援」、「Apple Hide My Email 是什麼」 | 客服 | ⚠️ 中 |
| 10 | business-rules.md | 新增 BR-026 auto-merge email 驗證 gate | Eric(PM) | ✅ 是(安全 hard gate) |
| 11 | business-rules.md | 新增 BR-027 cross-account email block | Eric(PM) | ✅ 是 |
| 12 | business-rules.md | 新增 BR-028 sensitive action step-up auth | Eric(PM) | ✅ 是 |
| 13 | business-rules.md | 新增 BR-029 解綁後 provider session invalidation | Eric(PM) | ✅ 是 |
| 14 | business-rules.md | 新增 BR-030 OAuth login rate limiting | Eric(PM) | ✅ 是 |
| 15 | business-rules.md BR-018 | 補上「帳號變更類事件須寄 audit 通知信」 | Eric(PM) | ✅ 是 |
| 16 | Apple Developer Portal(v1)— 一次完成 5 項(對應 spec Card 0 前置作業;原 #35、#48 已併入本條):① Apple Services ID 申請與配置(Web 路徑 aud);② Team ID / Key ID 配置 + Sign in with Apple Key 簽發;③ Service Domain & Return URL 填入;④ Server-to-Server Notification Endpoint 註冊(API-5 webhook 入口);⑤ App ID 啟用「Sign In with Apple」capability + 確認 iOS Bundle ID 並提供給 RD 寫入 config(iOS native 路徑 aud) | RD lead + iOS FE lead(⑤ 項需 iOS FE 配合確認 Bundle ID) | ✅ 是(開發前;Card C 開工前必完成) | |
| 16b | LinkedIn / LINE Developer 後台(v2 預備) | 各自開發者後台申請 OAuth client、配置 redirect URI、申請所需 scope(LINE email scope 商業審核見 #22) | RD lead | ❌ v1 不 blocking |
| 17 | RD migration script | 撰寫既有 Google 用戶 user.google_id → User Identity Binding 回填腳本;雙寫 → 切換 → 清理流程 | RD lead | ✅ 是(本期完成) |
| 18 | Email Template | 新增「自動合併」、「新增綁定」、「解綁」三種 audit 通知信模板 | PM + 客服 | ⚠️ 中 |
| 19 | Session 識別能力 | Session 必須能識別「來源 provider」以支援 BR-029 精準 invalidate;以及 §8.3「以特定 provider 來源 + 派生 child token」的能力。具體實作(session table 加欄位 / 獨立索引表 / claim 嵌入)由 RD 決定 | RD lead | ✅ 是 |
| 20 | 監控 dashboard | 新增 BR-030 rate limit 觸發、BR-026 merge_blocked 等安全 metric | RD + DevOps | ⚠️ 中 |
| 21 | Compliance(v1) | Apple App Store 4.8 規範審查 | PM + Legal | ✅ 是(v1 上線前) |
| 21b | Compliance(v2 預備) | LINE Login Terms / LinkedIn Developer Agreement 檢核(OIDC 用法是否符合條款) | PM + Legal | ❌ v1 不 blocking |
| 22 | LINE 商業審核 | 送件 LINE Login email permission 申請(v2 前置) | PM + Legal | ❌ v1 不 blocking,但建議 v1 上線後立即送件以縮短 v2 等待 |
| 23 | RD 實作(Apple provider_uid) | DB write path 強制以 Apple JWKS 驗章成功後的 ID Token sub claim 為 provider_uid 來源;不接受 client 端傳入的 sub;單元測試覆蓋 | RD lead | ✅ 是(§5.1) |
| 24 | RD 實作(Step-up token) | Token 必須綁 (user_id, action, target_provider);一次性消耗;TTL ≤ 5 min;OAuth re-auth 驗 auth_time ≤ 5 min | RD lead | ✅ 是(BR-028 / §8.3) |
| 25 | RD 實作(Idempotency 等價判定) | 並發 auto-merge 第二請求走 sign-in 分支,不重複寫 Merge Event Log / 不重複寄 audit 信;DB unique constraint + 鎖策略由 RD 決定,但 observable 行為須等價於 serializable | RD lead | ✅ 是(§5.2) |
| 26 | RD 實作(BR-030 分層 fail-open) | pre-verify per IP → fail-open;post-verify per email / UID → fail-closed(503);兩層 metric + alert | RD + DevOps | ✅ 是(§4.2 BR-030) |
| 27 | RD migration(firstBoundAt) | Schema 允許 firstBoundAt null;同時保留 migrated flag;查詢 / UI 以 flag 為準;禁止 COALESCE(firstBoundAt, created_at) 偷算 | RD lead | ✅ 是(§5.1 / §12.4) |
| 28 | FE 對接 | 依 §10.5 錯誤碼對應表實作 UI 行為;客服 CTA 直達 §12.5 工單入口;本地 retry 上限 3 次 | FE lead | ✅ 是(§10.5) |
| 29 | 客服系統 | 新增「帳號合併申請」工單表單,含 §12.5.2 必填欄位;FAQ 與內訓覆蓋 SOP | 客服 + PM | ✅ 是(v1 上線前;§12.5) |
| 30 | RD admin 工具 | 提供 §12.5.3 第 3 步全部能力(轉移 binding、歸戶履歷、軟刪除、audit log、雙端通知信);admin UI 可延後一週,但能力必須 v1 具備 | RD lead | ✅ 是(§12.5.5) |
| 31 | Admin 合併治理 | 雙人複核流程沿用既有 admin governance;本 PRD 不重寫 | Admin / Security | ⚠️ 中(§12.5.4) |
| 32 | business-rules.md | 新增 BR-031 Apple Sign in with Apple S2S Notification 處理 | Eric(PM) | ✅ 是(v1 release 前完成) |
| 33 | business-rules.md BR-029 | 收斂為二層 Revocation Propagation(L1 Provider / L2 wport)+ 失敗處理矩陣(v1.6.0 由 v1.5.0「三層含 Firebase」改回二層) | Eric(PM) | ✅ 是 |
| 34 | business-rules.md BR-012 | 補上「對外 cascade」條文(Apple auth/revoke + Google revoke);引用 App Store 5.1.1(v) 與 GDPR Article 17(v1.6.0 由 v1.5.0「三層含 Firebase deleteUser」改回二層) | Eric(PM) | ✅ 是 |
| 35 | (已併入 §16 #16) | ⚠️ 已併入 #16「Apple Developer Portal 一次完成 5 項」第 ④ 項(S2S Notification Endpoint)。本條保留條碼以維持既有交叉引用,不再單獨追蹤 | (見 #16) | (見 #16) |
| 36 | RD 實作(Apple S2S Webhook) | webhook handler 必須驗證 JWT 簽章(Apple JWKS)+ iss + aud + transaction_id idempotency + event_time ≤ 7 天;handler ≤ 5 秒回 200;重活動排 queue 異步處理(BR-031 / §12.6) | RD lead | ✅ 是 |
| 37 | RD 實作(二層 Revocation Propagation) | Revocation Propagation Job 表追蹤;L1 best-effort(unbind)/ 必須成功(delete:Apple App Store policy、Google 重試 N 次仍失敗則 fallback admin DLQ);L2 必須成功;DLQ + 重試 + oncall alert(BR-029 / §12.7) | RD lead | ✅ 是 |
| 38 | RD 實作(Account Deletion Cascade) | HINT-MUTATION-6 cascade:Apple auth/revoke 必須成功 + Google revoke 必須成功(重試 N 次仍失敗 fallback admin DLQ);ACCOUNT_DELETION_IN_PROGRESS 狀態下不可宣稱已刪;audit trail 留證 GDPR / 個資法(§12.8) | RD lead | ✅ 是 |
| 38b | RD 實作(Apple 首次 email 寫入持久化) | HINT-MUTATION-1 Apple 首次回傳 email 後寫入 binding 路徑必須含 retry queue + DLQ;寫入未成功不可標 binding 完成;observability metric:retry queue 深度(§5.2 / §8.3 / §10.2 / §10.4) | RD lead | ✅ 是(v1.6.0 新增) |
| 39 | Email Template | 新增「Apple 端撤銷/刪除觸發」與「帳號刪除 cascade 完成」兩種 audit 通知信模板(與 §16 #18 三種共五種) | PM + 客服 | ⚠️ 中 |
| 40 | 監控 dashboard | 補上 BR-029 L1/L2 失敗率、BR-031 Apple S2S 處理 lag、HINT-MUTATION-6 cascade DLQ size 等 metric(與 §16 #20 合併到同一 dashboard) | RD + DevOps | ⚠️ 中 |
| 41 | Compliance(v1) | Legal 確認 §12.8 cascade 步驟符合個資法第 11 條與 GDPR Article 17 舉證要求;audit trail 保留期由 Legal 給出建議值 | PM + Legal | ✅ 是(v1 上線前) |
| 42 | 客服 FAQ | 補上「Apple 端撤銷後如何恢復登入」、「帳號刪除處理中為何要等待」、「Apple email-disabled nudge 是什麼」 | 客服 | ⚠️ 中 |
| 43 | FE 對接 | 新增 ACCOUNT_DELETION_IN_PROGRESS 全頁狀態 + polling;BINDING_REVOKED_BY_PROVIDER Toast;ACCOUNT_NEEDS_CUSTOMER_SERVICE 全頁;Apple email-disabled banner nudge(§10.5 / §12.6) | FE lead | ✅ 是 |
| 44 | iOS FE 實作(Apple Sign-In) | 整合 ASAuthorizationAppleIDProvider;發起前產生 random nonce + SHA256 hash 帶入 request;取得 identityToken 後 POST 給 BE(帶 client_type=ios);沿用 §10.5 全部錯誤碼 UI 行為 | iOS FE lead | ✅ 是(v1 scope) |
| 45 | iOS FE 實作(Google Sign-In) | 整合 Google Sign-In iOS SDK(GIDSignIn);同上 nonce 機制;取得 ID Token 後 POST 給 BE(帶 client_type=ios);Google aud = iOS Google OAuth client_id(非 web client_id,需在 Google Cloud Console 另行建立) | iOS FE lead | ✅ 是(v1 scope) |
| 46 | RD 實作(Apple aud 白名單) | BE config 管理 Apple Services ID(Web)與 iOS Bundle ID(iOS native)兩個 aud 值;token 驗章時依 client_type 或 token aud 自動比對;兩值皆由 secret manager / env config 讀取,不可寫死(§8.3) | RD lead | ✅ 是 |
| 47 | RD 實作(nonce 驗證) | Apple / Google ID Token 驗章必須含 nonce 比對;FE 傳入 nonce(或 nonce hash)隨登入請求;BE 保存 nonce 並在驗章後比對消耗(一次性)。iOS native 路徑同等要求(§8.3) | RD lead | ✅ 是 |
| 48 | (已併入 §16 #16) | ⚠️ 已併入 #16「Apple Developer Portal 一次完成 5 項」第 ⑤ 項(iOS Bundle ID capability)。本條保留條碼以維持既有交叉引用,不再單獨追蹤 | (見 #16) | (見 #16) |
17. PRD 問題清單
| # | PRD 章節/規則 | 問題 | 建議修正 | 類別 | 影響 | SoT |
|---|---|---|---|---|---|---|
| 1 | §4.2 BR-001 既有條文 | 既有條文例舉「Facebook」,wport 不支援 Facebook | v1 改為「Google、Apple」;v2 再補 LINE/LinkedIn | 一致性 | 文件正確性 | business-rules.md |
| 2 | §4.2 BR-012 既有條文 | 同上,provider 綁定清除清單需修訂 | 同 #1 | 一致性 | 文件正確性 | business-rules.md |
| 3 | §5 LinkedIn email 唯一性(v2 才會遇到) | LinkedIn 用戶可能多人共用一個 work email(罕見但理論可能)→ 自動合併會誤將兩人合併 | v2 啟動時補上「分離帳號」客服工單流程;本 PRD 不展開設計 | 邊界情境 | 客服流程 | RD + 客服 |
| 4 | §6.2 SEC-4 顯示時間 | 「首次綁定時間」是否要顯示給用戶?或僅 internal 使用? | 建議顯示,便於用戶記憶;待 Figma 設計確認 | UX | UI 細節 | Designer |
| 5 | §10.4 Apple relay email 失效 | 用戶停用 Apple relay 後,wport 無法送信給該帳號;目前策略為「不主動處理」 | 提供 in-app 修改 primary email 的能力?本期暫不收,待用量資料佐證 | 後續優化 | 低 | 未來迭代 |
| 6 | §12.4 既有 Google 用戶 migration | firstBoundAt 若無專屬欄位可參考,不應用 user.created_at 推算 | 留 null + 標記 migrated=true,UI 顯示「綁定時間:未知(migration 紀錄)」 | 一致性 | 資料正確性 | RD |
17.1 RD Code Review 補充討論(2026/05/18 收斂)
以下為 PM 與資深後端在 PRD 進入 OpenAPI Spec 前的 review 紀錄。所有項目皆已收斂並回寫到本 PRD 對應章節。
| # | 類別 | 原始問題 | 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| Q1 | Architecture | Firebase 真的能託管 LINE / LinkedIn 嗎?原生 provider 名單不含這兩家 | v1 scope 收斂:LINE / LinkedIn 延後 v2;v1 僅 Google + Apple,皆為 Firebase 原生 provider 無爭議。v2 啟動時由 RD spike Firebase Identity Platform 通用 OIDC provider 是否能接 LINE/LinkedIn + GCIP 付費 tier 評估 | §3.2;§12.2 D4-defer;§15;§16 #16b |
| Q1b | Architecture | 為什麼 Apple 不直接接原生 SDK,要透過 Firebase? | ⚠️ 本格結論已於 v1.6.0(2026/05/22)撤回,改為直連 Apple 原生 OIDC。詳見 §17.6 U1~U12。原 5/18 review 紀錄保留於下方僅供歷史對照:「採用 Firebase Auth 的 Apple provider。理由:(1) Apple email 只給一次,Firebase 持久化保存,避免 wport BE 任一次 DB 寫入失敗導致該用戶帳號變廢;(2) Apple 原生 Web SDK 需 redirect + POST callback,徹底打斷 SPA 狀態;Firebase signInWithPopup 把跳轉地獄封裝起來」 | §12.2 D4;§10.2 補充說明;§17.6(v1.6.0 撤回紀錄) |
| Q2 | 安全層 | email_verified=false 時是否允許 auto-merge?(LINE 不強制驗證 email → 帳號接管漏洞) | Hard rule:auto-merge 必須 email_verified=true;否則回 EMAIL_NOT_VERIFIED 並要求走 email+password 流程 | BR-026;§8.3;§10.2;§10.4 |
| Q3 | Migration | 既有 Google 用戶 user.google_id 欄位如何 migrate?本期或下期? | v1 完成。採雙寫 → 切換 → 清理三階段策略,可 rollback。Owner=RD。firstBoundAt 若無專屬欄位則留 null + migrated=true,不用 user.created_at 推算 | §3.2 In Scope;§12.4;§15;§16 #17 |
| Q4 | 安全層 | 解綁 / 新增綁定要不要 step-up auth?目前 §8.3 僅要求 session 未過期 | 加 step-up:新增綁定為天然 step-up(必然重做 OAuth);解綁須擇一:(a) 重做 OAuth;(b) 重輸 email+password | BR-028;§8.3;§10.4 |
| Q5 | 資料語意 | ACT-7 新增綁定的 cross-account email 衝突(provider email 已是他人 primary email) | 阻擋,回 EMAIL_BELONGS_TO_OTHER_ACCOUNT。理由:這正是唯一信箱原則的反面情境 | BR-027;§8.3;§10.4 |
| Q6 | 資料語意 | Apple Hide My Email 第二次登入沒 email,§10.2 流程圖誤導 | §5 lookup 能力本身沒問題;§10.2 流程圖補上「Apple 第二次起 lookup 以 UID 為憑」備註 | §10.2 |
| Q7 | 資料語意 | Idempotency Key 構成?TTL? | RD 自行決定。PRD 僅要求「auto-merge 必須 idempotent」此能力,不指定 key 形式 | §5.2;§8.3 |
| Q8 | 體驗 / 維運 | 解綁後該 provider 的其他裝置 sessions 怎處理? | Invalidate。需 session table 加 provider 維度欄位 | BR-029;§5.1;§8.3;§16 #19 |
| Q9 | 維運 / 稽核 | 自動合併「不寄信」是否符合 wport audit 標準? | 改為寄送 audit email。新增綁定、解綁亦同。BR-018 修訂 | BR-018 修訂;§12.3;§16 #15、#18 |
| Q10 | 安全層 | Rate Limiting?OAuth 登入易被當帳號探測 | Per IP 60/min、per email 10/min、per provider UID 10/min。觸發 429 + Retry-After,異常 spike 觸發 oncall alert。降級策略 fail-open + metric | BR-030;§4.2;§8.3;§10.4;§14.2 |
| D1 | 文件層 | §6.2 Provider 按鈕排序 | v1 鎖死「Apple → Google」(Apple 須首位,iOS HIG 4.8);v2 擴充為 Apple → Google → LinkedIn → LINE | §14.2;§15 |
| D2 | 文件層 | §10.4「provider UID 全系統唯一」用詞容易誤解(跨 provider 不保證) | 改為「每個 provider 內 UID 唯一」 | §10.4 |
| D3 | 文件層 | §11 Mock Data Schema 寫 N/A,SEC-4 前端開發無 fixture | §11 補上 SEC-4 fixture JSON 範例 + 情境切換建議 | §11 |
17.2 v1.2.0 收斂紀錄(2026/05/18)
第二輪 PM × 資深後端 review。重點是 scope 收斂 + PRD/RD 邊界釐清。
| # | 類別 | 原始 review 問題 | 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| R1 | Scope | v1 範圍是否該包含 LINE / LinkedIn?LINE email scope 商業審核時程不可控、LinkedIn OIDC 需 RD spike | 延後 v2。v1 僅 Google + Apple,但資料模型、安全 gate(BR-024~030)一次到位 | §3.1;§3.2;§12.2 D4-defer;§15 |
| R2 | 架構 | Apple 為何不接原生 SDK? | (1) Apple email 只給一次;(2) Web SPA redirect 痛苦。詳細理由補進 §12.2 D4 與 §10.2 補充 | §12.2 D4;§10.2;§17.1 Q1b |
| R3 | Migration | firstBoundAt 用 user.created_at 推算是錯的(先 email 註冊、後綁 Google 的用戶會被誤標) | 改為「若無 user.google_linked_at 專屬欄位 → 留 null + 標記 migrated=true」 | §12.4 step 3;§17 #6 |
| R4 | PRD 邊界 | 「(provider, provider_uid) 必須 unique constraint + SELECT FOR UPDATE」是否該寫進 PRD? | 不寫。PRD 只表達能力與安全 gate(“auto-merge 必須 idempotent”、“BR-025 不可被併發繞過”),DB 約束、鎖策略屬 RD 在 OpenAPI Spec / 系統設計範圍 | §5.2;§8.3;§12.1 |
| R5 | PRD 邊界 | BR-025 併發漏洞處理是否寫進 PRD? | 不寫。PRD 已表達「BR-025 不可被併發繞過」此語意,交易隔離等級、鎖實作由 RD 決定 | §4.2 BR-025;§12.1 |
| R6 | PRD 邊界 | Rate limit pre-token-verify / post-token-verify 分層是否寫進 PRD? | 不寫。PRD 只列三維度(IP / email / UID)與閾值;實作層級由 RD 決定 | §4.2 BR-030;§10.2 補充 |
| R7 | PRD 邊界 | 60/min per IP 對企業 NAT 是否過低? | 不寫死 v1 數字。§14.2 仍列原值供 RD 起步,但加註「以 prod traffic 觀察後微調,討論交由 RD + DevOps」 | §14.2 |
| R8 | PRD 邊界 | Audit email 重試策略是否寫進 PRD? | 不寫。PRD 只表達「best-effort + 失敗不阻斷主流程」此語意,重試次數、DLQ、回收機制由 RD 決定 | §8.3 |
| R9 | 一致性 | email normalize 規則僅寫「lowercased」 | §5.2 補上「能力約束」:全系統共用同一 normalize 函式、記錄演算法版本、BR-024/026/027 比對皆以 normalized email 為基準 | §5.2 |
17.3 v1.3.0 收斂紀錄(2026/05/18 — 後端 P0 review)
第三輪 review:後端針對「架構假設不明、文件殘留誤導 RD」三項 P0 阻擋直接收斂。
| # | 類別 | 原始問題 | 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| P0-1 | 架構 | §10.2 原 sequence 圖把 Google 也畫成走 Firebase;§12.4 又寫現況是 user.google_id 單欄位(暗示直連 Google OAuth)。兩段互相矛盾,會讓 RD 誤以為 v1 要同時把 Google 也遷 Firebase | 架構雙軌定案:Google 沿用既有「直連 Google OAuth」框架(token 驗證走 Google JWKS、session 簽發不變、user table 模型不動);僅 Apple 走 Firebase。§10.2 sequence 拆為「Token 取得 / Token 驗證」兩段分軌;§12.4 加註「不遷 Firebase」釐清 migration 只是 DB 內帳號模型 reshape;§3.2、§6.2、§8.2 均同步 | §3.2;§6.2;§8.2;§10.2;§12.4 |
| P0-2 | 互動 | §6.2 寫「無頁面跳轉」signInWithPopup,但 §10.3 Navigation Flow 又畫了 /auth/callback/ redirect 路徑。RD 不知道要設計 popup token exchange 還是 callback POST | 鎖定 popup-only(移除 /auth/callback/)。理由:(1) 避免 SPA 狀態被 redirect 打斷;(2) Apple 走 Firebase 的核心理由就是吃掉 redirect 地獄,引入自家 callback 是浪費;(3) 與 §6.2 互動描述一致。若日後 Safari ITP / popup blocker 造成 Apple popup 失敗率過高,再評估 Apple-only 的 signInWithRedirect fallback(另開 PRD) | §10.3 |
| P0-3 | 一致性 | 雖 v1 scope 已收斂為 Google + Apple,但 ACT-5、SEC-5 Toast、§8.3 idempotency、§10.4 edge case、§11 fixture 仍大量以 LINE/LinkedIn 為範例敘述,QA / RD 只讀某一節時容易誤以為 v1 要做 LINE | 全文清掃 v1 作業性敘述中的 LINE/LinkedIn:ACT-5 LINE 未授權移除(改保留 ACT-5 為 EMAIL_NOT_VERIFIED)、SEC-5 Toast 改 Apple 範例、§8.3 idempotency 改 Apple 範例、§10.4 LINE 解綁 / LinkedIn 大小寫 / LinkedIn 衝突等列改 Apple、§11 fixture 移除 linkedin、§18 Format Mismatch(LINE)標為 v2 範疇。戰略性「為何延後 v2」說明保留(§3.2 v2 範圍、§12.2 D4-defer、§15 v2 rows、§17.1 Q1) | ACT 表;§6.2 SEC-5;§8.3;§10.4;§11;§18 |
17.4 v1.4.0 收斂紀錄(2026/05/20 — 資深後端 spec-readiness review)
第四輪 review:以「這份 PRD 能直接給 RD 寫 OpenAPI Spec 嗎?」為驗收標準,找出剩餘的 ambiguity 與隱性 trade-off 並明確化。
| # | 類別 | 原始問題 | 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| S1 | 資料模型 | §10.2 補充說明寫「Apple sub (UID)」,但 Firebase Admin SDK 驗 token 後有 decodedToken.uid(Firebase UID)與 decodedToken.firebase.identities['apple.com'](Apple sub)兩個值,RD 會猜錯哪一個作為 provider_uid | 鎖定 Apple OIDC sub;明確禁止存 Firebase UID。理由:避免被 Firebase project / 廠商綁死,v2 OIDC provider 沿用同一原則 | §5.1 新增「Provider UID 識別主鍵語意」子節;§15;§16 #23 |
| S2 | 安全層 | BR-028 step-up token 沒指定是否綁定「目標 provider」→ 用戶完成解綁 Apple 的 step-up 可拿同一 token 解綁任意 provider | Token 綁 (user_id, action, target_provider) 三元組 + 一次性消耗 + TTL ≤ 5 min。HINT-MUTATION-4 輸入新增 target_provider;HINT-MUTATION-3 驗 token 時必須比對三元組 | §5.2;§8.2 HINT-MUTATION-4;§8.3;§10.4;§10.5;§15;§16 #24 |
| S3 | 安全層 | BR-028 要求「重做該 provider OAuth」,但 Firebase signInWithPopup 可能直接從既有 session reissue 老 token,等於 step-up 被架空 | 加 auth_time ≤ 5 分鐘 gate(非 iat,因為 iat 為 token 簽發時間,不代表用戶實際 re-auth)。失敗回 STEP_UP_REQUIRED + sub_reason=STALE_AUTH,FE 觸發 signOut 後重打 popup | §5.2;§8.3;§10.4;§10.5;§15;§16 #24 |
| S4 | 安全層 | BR-029「invalidate 該 provider 來源 sessions」沒涵蓋「該 session 解綁前已派生的 child token(OAuth grant / API token)」;也沒明示「為何不全 invalidate」 | 採最小範圍(同 provider 來源 session + 其派生 child token) + 明示已知 accepted risk(殘餘風險靠 BR-028 step-up 攔截)。寫入凍結表 | §8.3;§15 |
| S5 | 並發 / 冪等 | §5.2「auto-merge 必須 idempotent」太弱:並發點兩次「以 Apple 登入」第二請求行為未定 | 明確等價判定基準為 (user_id, provider, provider_uid) 最終狀態,第二請求走 sign-in 分支、不重複寫 Merge Event Log / audit 信。鎖實作由 RD 決定,但 observable 行為須等價 serializable | §5.2;§16 #25 |
| S6 | 商業傷害 | 帳號分裂情境(Apple Hide My Email + Google 同人不同帳號、UID 轉手、provider sub 變更)只在 §17 #3 一句帶過,v1 上線就會發生 | 新增 §12.5 帳號分裂客服 SOP:v1 不自助(守 BR-024/027 邊界)但必須有客服救援;admin 合併工具能力 v1 上線前具備(UI 可延後一週) | §10.4;§10.5;§12.5;§15;§16 #29、#30、#31 |
| S7 | FE 對齊 | 錯誤碼散落各章節,FE 拿 PRD 開發要全文掃描 | 新增 §10.5 錯誤碼 → UI 對應表(含 HTTP、文案、CTA、可重試、客服路徑) | §10.5;§16 #28 |
| S8 | 資料一致性 | §5.1 寫「首次綁定時間」是必要追蹤資料,§12.4 又允許 null + migrated=true,RD 在做 DB 約束會卡住 | firstBoundAt allow null + migrated flag 配對;查詢 / UI 以 flag 為準;禁止 COALESCE 偷算 | §5.1;§12.4;§15;§16 #27 |
| S9 | 安全層降級 | BR-030 一刀切「Redis 不可用 fail-open」會讓所有 auth endpoint 在 Redis 故障時失去保護 | 分層降級:pre-verify per IP fail-open;post-verify per email / UID fail-closed(503)。寧可 post-verify fail-closed 也不要 pre-verify fail-closed | §4.2 BR-030;§8.3;§10.4;§15;§16 #26 |
| S10 | 邊界破洞 | §16 #19 直接寫「session table 加 provider 欄位」違反 §12.1「不指定 DB schema」邊界 | 改寫為「session 必須能識別來源 provider 之能力」,schema 實作由 RD 決定 | §16 #19 |
17.5 v1.5.0 收斂紀錄(2026/05/20 — 資深後端 P0 spec gap 補強)
第五輪 review:以「PRD 是否漏列 Apple 平台 / GDPR / OAuth 生命週期硬需求」為驗收標準。三項 P0 補強對應 RD 進入 OpenAPI Spec 前的最後一次缺口收斂。
| # | 類別 | 原始問題 | 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| T1 | 平台規範 | Apple Sign in with Apple 提供 Server-to-Server Notification(email-disabled / email-enabled / consent-revoked / account-delete),但 v1.4.0 PRD 完全沒提。後果:用戶在 Apple 端撤銷/刪除 → wport 無感 → binding 永久殘留;Hide My Email 失效 → wport 持續寄不到信。 | 新增 BR-031 + §12.6。webhook 必須驗章(Apple JWKS)+ iss + aud + transaction_id idempotency + event_time ≤ 7 天;handler ≤ 5 秒回 200;4 種 event_type 處理矩陣明列;唯一登入方式情境改走 §12.5 客服恢復(不適用 BR-025 / BR-028,因為事件源為 Apple authoritative) | §3.2;§4.1;§4.2 BR-031;§5.1;§7 ACT-10;§8.2 HINT-MUTATION-5;§8.3;§10.4;§10.5;§12.6;§14.3;§15;§16 #32、#35、#36 |
| T2 | OAuth 生命週期 | v1.4.0 BR-029 只規範 wport session invalidation(L3);OAuth 授權鏈有三層 cached state,僅清 L3 仍可被 Firebase / Provider 端 mint 新 token,且 Firebase / Provider 端 PII 殘留違反 GDPR / 個資法。 | BR-029 擴展為三層 Revocation Propagation + 失敗處理矩陣 + §12.7。L1(Provider 端 revoke)unbind 為 best-effort、delete 對 Apple 必須成功(App Store 5.1.1(v));L2(Firebase revokeRefreshTokens)必須成功(GDPR);L3(wport session invalidate)必須成功。Revocation Propagation Job 表追蹤;DLQ + oncall alert | §4.2 BR-029;§5.1;§5.2;§7 ACT-8/ACT-9;§8.2 HINT-MUTATION-3;§8.3;§10.4;§12.7;§14.3;§15;§16 #33、#37 |
| T3 | 合規層 | v1.4.0 BR-012「帳號刪除一併移除第三方綁定」僅清 wport DB;不呼叫 Firebase Admin deleteUser → Firebase 端 user record + PII 殘留違反 GDPR Article 17;不呼叫 Apple auth/revoke → 違反 App Store Review Guideline 5.1.1(v)。 | BR-012 補強 + 新增 §12.8 + HINT-MUTATION-6。cascade 步驟:對每個 binding 觸發 BR-029 三層傳播(所有層必須成功)+ Apple 路徑額外呼叫 Firebase deleteUser(不僅 revoke);L1/L2 未完成前禁止對用戶宣稱「已刪」,UI 顯示 ACCOUNT_DELETION_IN_PROGRESS polling;audit trail 留證以供 GDPR / 個資法舉證 | §4.1 BR-012;§4.3;§7 ACT-9;§8.2 HINT-MUTATION-6;§8.3;§10.4;§10.5;§12.8;§14.3;§15;§16 #34、#38、#41 |
| T4 | 監控 | 新增的三層傳播 / S2S / cascade 都需要觀測性,否則 DLQ 中累積的 stuck job 沒人發現 | 新增 metric:auth.apple_s2s.received/processed/signature_failed、auth.revocation.l1_failed/l2_failed/completed、auth.account.deleted.cascade_completed;合併到既有 dashboard | §13;§14.3;§16 #40 |
| T5 | 用戶體驗 | Apple email-disabled 後若不告知用戶,用戶會以為「wport 寄不到信 = wport 故障」 | 加 Apple Relay Email Status 標記與下次登入後 banner nudge | §5.1;§10.4;§12.6.3;§15 |
| T6 | 邊角 accepted risk | Apple relay 失效後通知信寄不到的場景無法在 v1 完美解決(用戶自己關閉了轉信) | 列為 accepted risk + v1.x 補「強制設定可收信 email」UX 弱化 | §12.6.4;§15 |
17.6 v1.6.0 收斂紀錄(2026/05/22 — Apple 接入方式由 Firebase 改回原生)
第六輪 review:依
pm_39-apple-auth-decision-brief-for-pm.md(2026/05/20)的決策複審,撤回 v1.2.0~v1.5.0 期間「Apple via Firebase」此一架構決定。本輪不涉及任何商業規則、UI、流程、文案的改動,純粹是後端架構選擇的修正。使用者完全無感。
| # | 類別 | 原始狀態(v1.5.0) | v1.6.0 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| U1 | 架構 | Apple 經 Firebase Auth 託管,後端以 Firebase Admin SDK 驗 Firebase ID Token | 改為直連 Apple OIDC:FE 用 Apple Sign in with Apple JS(usePopup: true)取得 Apple ID Token;BE 直接以 Apple JWKS(https://appleid.apple.com/auth/keys)驗章 + iss / aud / exp / sub | §3.2 In Scope;§5.1 Provider UID;§6.2 SEC-1;§8.2 HINT-MUTATION-1;§10.2.1 / 10.2.2;§12.2 D4 |
| U2 | D4 理由複審 | 原 PRD 用兩個理由支持 Firebase:(1) Apple email 只給一次;(2) Apple 原廠 Web 登入是 redirect | (1) 言過其實:只要後端寫 binding 路徑具備持久化保護(retry queue + DLQ),Apple email 持久化風險即可被 close。Firebase 不在這條路徑上幫忙——寫 DB 失敗仍須自己處理;(2) 過時:Apple Sign in with Apple JS 已正式支援 usePopup: true,可直接得到與 Google 同等的 popup UX | §12.2 D4 |
| U3 | Token Revocation 層數 | 三層:L1 Provider / L2 Firebase revokeRefreshTokens / L3 wport | 二層:L1 Provider / L2 wport。Firebase 中介層消失,最複雜易錯的「Apple via Firebase 特例層」直接砍掉 | §4.2 BR-029;§5.1 Revocation Propagation Job;§5.2;§7 ACT-8/9/10;§8.2 HINT-MUTATION-3;§8.3;§10.4;§12.7;§14.3 事件字典 |
| U4 | Account Deletion Cascade | wport 軟刪除 + L1 Provider revoke + L2 Firebase revoke + 額外 Firebase Admin deleteUser | 移除 Firebase Admin deleteUser(無 Firebase user record 需清除);其餘 cascade 不變(Apple auth/revoke 必須成功、Google revoke 必須成功 / 失敗 fallback admin DLQ) | §4.1 BR-012;§4.3;§8.2 HINT-MUTATION-6;§12.8 |
| U5 | Apple 首次 email 持久化保護 | 仰賴 Firebase 持久化 email | 改由 wport BE 自行負責:HINT-MUTATION-1 對 Apple 首次回傳 email 的寫入路徑必須有 retry queue + DLQ 至成功,否則不可標 binding 完成 | §3.2 In Scope;§5.2;§8.3;§10.2 補充說明;§10.4;§14.3;§16 #38b |
| U6 | Provider UID 來源 | Apple sub 從 Firebase Admin SDK 驗 token 後的 firebase.identities['apple.com'][0] 取出;明確禁存 Firebase UID(decodedToken.uid) | Apple sub 從 Apple JWKS 驗章成功後的 ID Token sub claim 直接取出;不再有 Firebase UID 議題 | §5.1 Provider UID 識別主鍵語意;§16 #23 |
| U7 | FE Apple 登入互動 | signInWithPopup(auth, new OAuthProvider('apple.com')) 由 Firebase SDK 包裝 | AppleID.auth.signIn({ usePopup: true })(或同等原生 API);popup UX 與 Firebase 路徑相同 | §6.2 SEC-1;§10.3 |
| U8 | Apple S2S Notification | 屬 Apple 平台機制,與是否經 Firebase 無關,v1.6.0 保留 BR-031 全部能力不變 | 不變(BR-031 / §12.6 全部保留);唯獨 §12.6 文字中提到「不經 Firebase Function 轉發」此 routing 偏好現已自然成立 | §4.2 BR-031;§7 ACT-10;§8.2 HINT-MUTATION-5;§12.6 |
| U9 | Sync-fix 任務 | 含 Firebase 後台設定(#16)、Firebase Identity Platform v2 預備(#16b)、Firebase Admin SDK 整合(多項) | 改為 Apple Developer Portal 設定(#16 統一 Apple Services ID + S2S endpoint);#16b 改為 LinkedIn / LINE Developer 後台預備;BR-029 / BR-012 / cascade 對應條文同步收斂為二層 | §16 #16、#16b、#33、#34、#37、#38、#38b |
| U10 | 商業規則 / UI / 流程 / 客服 SOP | — | 完全不變。BR-024 | — |
| U11 | 上架時程 | — | 預期較有利(BR-029 由三層減為二層、cascade 移除 Firebase deleteUser、不需 Firebase 後台設定學習成本);不額外算工時,但 sync-fix 任務減少 | §13 v1 Next Steps |
| U12 | LINE / LinkedIn v2 影響 | v1 共用 Firebase 後台(v2 預備 #16b 啟用 Identity Platform OIDC) | v2 沿用同類型「直連 provider OIDC + JWKS」架構;資料模型、安全 gate 一切沿用 | §3.2 延後範圍;§12.2 D4-defer;§13 v2 |
18. 邊界情境檢查(Edge Case Taxonomy Final Check)
依 references/edge_case_checklist.md 五大類,已覆蓋情境:
- ✅ Network & Performance:Timeout(30s)、Race(連點冪等)、Disconnection(popup 取得 token 後提交 BE 中斷)、High Latency(Apple OAuth 重試,僅影響 Apple)、Rate limit 觸發 + 分層降級(BR-030:pre-verify fail-open / post-verify fail-closed)、Apple S2S webhook ≤ 5s 回應限制(§12.6.5)、二層傳播 L1/L2 異步重試與 DLQ(§12.7)
- ✅ Data & Input:Null/Empty(Apple Hide My Email 為合法輸入)、Special Characters(大小寫 normalize)、Apple relay email 失效、Apple 首次 email 寫入持久化保護(retry queue / DLQ;§10.4 / §10.2 補充說明)、Migration 期間既有 Google 用戶雙寫、
firstBoundAtnull +migratedflag(§5.1 / §12.4)、Apple S2Semail-disabled標記與 user nudge(BR-031 / §12.6.3)、Apple S2Stransaction_ididempotency 去重(§12.6.2);Format Mismatch(LINE 無 email)屬 v2 範疇,v1 不涵蓋 - ✅ User Interruption:Navigation(OAuth popup 關閉)、Refresh(Onboarding 中途離開可恢復)、Onboarding 中途進設定頁解綁(BR-025 防呆觸發)、Account Deletion 處理中 polling 等待(§10.5
ACCOUNT_DELETION_IN_PROGRESS) - ✅ Auth & Lifecycle:Session Timeout(解綁前重新登入)、Concurrency(兩 tab 同時解綁)、Email 未驗證 auto-merge 攻擊面(BR-026)、Step-up auth 缺漏(BR-028)、Step-up token 不匹配 target_provider(§8.3)、Step-up OAuth 不新鮮 /
STALE_AUTH(§8.3)、解綁後其他裝置 session invalidate(含 child token)(BR-029)、Apple 端consent-revoked(BR-031)、Apple 端account-delete+ Apple 為唯一登入方式(BR-031 / §12.6.4)、二層傳播 L1 失敗於 unbind 後續(§12.7)、Account Deletion cascade L1 失敗禁止標記完成(§12.8.3) - ✅ Logical Inconsistency:Partial Success N/A(單一動作無 partial)、Step Skipping(未登入直接打 /settings)、新增綁定 cross-account email 衝突(BR-027)、Apple sub 取錯 claim 預防(§5.1)、跨帳號分裂(Apple relay + Google 同人)→ 客服 SOP(§12.5)、Apple S2S webhook payload 偽造 → 簽章驗證攔截(§12.6.2)、wport 帳號刪除後 Apple S2S 仍到達 →
not_applicable(§10.4 / HINT-MUTATION-5)
未涵蓋情境:無。
17.7 v1.7.0 收斂紀錄(2026/05/22 — iOS native app 範圍補入 + 安全細節收斂)
第七輪 review:確認 v1 同時含 Web + iOS native app(Android app 尚未開發)後補入對應影響,並同步收斂 nonce 強制驗證、rate limit 驗章失敗計數、step-up token 消耗時機三項安全細節。商業規則、BR-024~031、cascade 流程、UI、客服 SOP 完全不變。
| # | 類別 | 原始狀態(v1.6.0) | v1.7.0 結論 / 採用方案 | 對應 PRD 章節 |
|---|---|---|---|---|
| M1 | Scope | 未明示平台範圍,通篇預設 Web;§3.2 / §15 未提 iOS | 明示 v1 = Web + iOS native app;Android app 尚未開發,明確排除(§3.2 範圍)。詳見凍結表「v1 平台範圍」 | §3.2;§15;§16 #44、#45、#48 |
| M2 | 架構 | §8.3 hard gate 寫 aud = wport Apple Services ID(一個值);iOS native aud = Bundle ID 完全未提 → 若 RD 照 PRD 寫,iOS 登入會全部 401 | aud 白名單改雙值:Web = Apple Services ID;iOS native = iOS Bundle ID;兩值由 config 讀取不可寫死(§8.3 新增「Apple ID Token aud 白名單」列) | §3.2 In Scope;§8.2 HINT-MUTATION-1;§8.3;§15;§16 #46 |
| M3 | 架構 | §10.2.1 sequence 只有 Web popup 分支;iOS native ASAuthorizationAppleIDProvider 無描述 | §10.2.1 補 iOS native 分支(系統層彈窗、nonce、identityToken、aud = Bundle ID)(§10.2.1) | §10.2.1 |
| M4 | 架構 | §10.3 Navigation Flow 寫「popup-only」,iOS native 不用 popup → 說明不適用 | §10.3 改為「Web popup-only / iOS native 系統彈窗」,補 iOS FE 取 token 後 POST BE 的路徑 | §10.3;§15 |
| M5 | 安全 | §8.3 nonce 驗證寫「若使用」(可選) | 改為強制驗證。nonce 為 OIDC anti-replay 核心,可選等於不設防;iOS native 同等要求(§8.3 新增「Apple / Google ID Token nonce 驗證」列) | §8.3;§15;§16 #47 |
| M6 | 安全 | §8.3 rate limit 未提「驗章失敗的請求」是否計入 per IP quota | 補充:驗章失敗請求亦計 per IP quota,否則攻擊者送無效 token 永遠進不到 post-verify limit(§8.3 新增「Rate limiting — 驗章失敗計 IP quota」列) | §8.3;§15 |
| M7 | UX / 安全 | §8.3 step-up token 「無論操作成功失敗皆作廢」→ 用戶因業務阻擋(如 BR-025)被迫重做 OAuth step-up,體驗差 | 改為「操作成功才作廢;client error 不消耗 token」。server error / 驗章失敗仍作廢。client error 不洩漏額外資訊,trade-off 已評估(§8.3 新增「Step-up token 消耗時機」列,並修訂原「Step-up token 綁定範圍」) | §8.3;§15 |
文件結束