◆ wport | pm_39

第三方登入整合 2.0(Apple 原生;LINE / LinkedIn 延後 v2)Mockup PRD

第三方登入 2.0(Apple 等)

來源 doc/feature/pm_39/third-party-auth-mockup.md

第三方登入整合 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 Admin deleteUser 步驟(Apple auth/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 + Apple auth/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 Token auth_time 新鮮度 gate;(3) BR-029 session invalidate 範圍補強;(4) 新增 §10.5 錯誤碼 → UI 對應表;(5) 新增 §12.5 帳號分裂客服 SOP;(6) firstBoundAt nullable 與 §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 策略)
  • 對應 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 Rowi-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)
    1. 擴充第三方登入選項,從現有 Google 1 種擴充為 2 種(Google + Apple);LINE / LinkedIn 延後 v2
    2. 建立明確的「唯一信箱原則」帳號模型,避免同一人多個分裂帳號(多 provider 資料模型須一次到位,v2 可直接擴充)
    3. 實作「絲滑合併」(作法 B),讓跨 provider 使用相同 email 的用戶能無痛切換登入方式
    4. 提供「防呆解綁機制」,避免用戶不慎將自己鎖在帳號外(幽靈帳號)
    5. 將既有 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 欄位或 token aud 自動判斷)分派(見 §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:人力銀行場景中,履歷的求職者身份必須與企業端認知的個人身份一致,否則應徵歷史會分裂
  • 描述:使用者在設定頁解除第三方登入綁定時,系統必須確保使用者至少保留一種可用的登入方式(第三方綁定 ≥ 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:帳號接管會直接導致履歷、應徵歷史外洩,屬於最高等級安全風險

建議 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 皆採阻擋策略

建議 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) 強制要求)
    • L2 — wport 端 session invalidation(內部一致性層,沿用原 BR-029 語意)
      • Session Provider Index 找出該 provider 來源之所有 active sessions + 其派生 child token,全部 invalidate
      • 必須成功(原 BR-029 既有 hard gate)
  • 適用範圍:ACT-8 解綁、ACT-9 帳號刪除、ACT-10 處理 Apple S2S consent-revoked / account-delete 事件
  • 失敗處理矩陣
    解綁(unbind)帳號刪除(delete)
    L1best-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

建議 BR-031:Apple Sign in with Apple Server-to-Server Notification 處理

  • 描述:wport 必須提供 Apple S2S Notification webhook endpoint,接收並處理 Apple 端發起的 4 種 lifecycle event:
    event_typeApple 端動作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.comaud = wport 的 Apple Services ID
    • 必須以 transaction_id 做 idempotency check(Apple 會重試,重複收到同一通知不可重複執行 unbind / cascade)
    • webhook handler 必須在 ≤ 5 秒內回 200(否則 Apple 視為失敗會重試),重活動排程到後端 queue 異步處理
  • 設計依據
    • 不接收 S2S 的後果
      1. 用戶在 Apple 端撤銷授權 / 刪除 Apple ID → wport 完全無感 → binding 永久殘留 → 違反用戶意願與個資法
      2. 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 通知信」此類隱私事件

建議 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 同等
  • 設計依據(資深後端視角)
    • 攻擊情境
      1. 帳號列舉:攻擊者以同 email 大量試登入,藉錯誤碼差異推測「該 email 是否已註冊 wport」→ 洩漏求職者隱私
      2. Provider token 試錯:對 provider OIDC 端點(Google JWKS / Apple JWKS)暴力打 → 影響 wport 配額
      3. Credential stuffing:拿外洩 OAuth token 灌水
    • 選項對比:
      • A. 不設 limit → 帳號列舉攻擊面
      • B. 僅 IP 維度 → 攻擊者使用 IP 池 / 雲端 IP 即可繞過
      • C. IP + email + UID 三維度 ✅ 採用
    • 實作參考:建議使用 Redis-based sliding window(如 redis-cellrate-limit-flexible),key 為 auth:{dimension}:{value};error response 帶 Retry-After header
    • 降級策略(分層):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 Admin deleteUser 步驟。詳細 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 Tokensensitive action(解綁、新增綁定)通過 step-up 後的一次性短效 token;必須綁定 (user_id, action, target_provider) 三元組(見 §5.2 能力約束)對應 BR-028;避免「session 仍有效但 step-up 未完成」就執行操作;避免一份 token 跨 action / 跨 provider 重用
Apple Provider Lifecycle Event LogApple 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 決定)。

Providerwport DB provider_uid 採用值取得來源不採用值(明確排除)
Google(直連 OAuth)Google OIDC subGoogle JWKS 驗證後的 ID Token sub claim
Apple(直連 Sign in with Apple)Apple OIDC subApple 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 subprovider_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_time claim 距今 ≤ 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)、issaud,並以 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-3Onboarding 個人資料頁既有頁面無變動沿用 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 Bindingsign-up + 進入 Onboarding
ACT-4以 Apple Hide My Email 登入(全新 relay email)Write✅ 是User, User Identity Binding視為獨立新帳號(D1)
ACT-5新情境:merge 對象 email 未驗證(email_verified=falseRead✅ 是後端 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 NotificationWrite✅ 是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_typeweb / ios;iOS native app 須傳入,Web 預設;具體欄位命名由 RD 在 OpenAPI Spec 決定)
    • 期望行為:
      1. 後端依 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.comaud = wport Apple Services IDexp / nonce(強制驗證,見 §8.3)
        • provider=apple, client_type=ios → 同上 Apple JWKS 驗章路徑;差異:aud = wport iOS Bundle ID(由 RD 從 config 讀取,不寫死於 PRD);token 來源為 iOS native ASAuthorizationAppleIDProvider 回傳的 identityToken(JWT 格式與 Web 相同)
      2. 取得 provider UID + email + email_verified(兩條路徑匯流為共同格式)
      3. 判定情境(見 §10.2 System Flow)
      4. 若進入 merge 分支:先檢查 email_verified=true(BR-026);若為 false → 回 4xx EMAIL_NOT_VERIFIED,不建立 binding
      5. 回應「登入成功」+「是否觸發了合併」(後者用於 FE 決定要不要顯示 SEC-5 Toast)
      6. 若觸發合併或新增 binding → 寫入 Auth Audit Notification Log + 排程寄送 audit 信
      7. 建立 session 時記錄來源 provider(Session Provider Index
      8. Apple 首次授權 email 持久化保護:若該次 Apple ID Token 內含 email claim(即「Apple 首次回傳 email」),binding 寫入失敗時必須排入 retry queue 持續重試到成功(不可僅回 5xx 給 FE 後就放棄);FE 視為「登入失敗,請重試」由用戶自然觸發再次登入;後端在背景持續推進至 binding 建立成功
    • 邊界處理:
      • Apple Hide My Email relay email → 視為獨立 user,不執行合併(D1)
      • 任一 provider email_verified=false 且擊中 merge 分支 → EMAIL_NOT_VERIFIED(BR-026)
  • HINT-MUTATION-2:新增綁定(已登入狀態)

    • 輸入:provider 類型 + provider token
    • 期望行為:
      1. 驗證 token → 取得 provider UID + email
      2. 若該 UID 已綁在別的 wport 帳號 → 回 4xx PROVIDER_UID_TAKEN
      3. 若該 email 已是他人 primary email → 回 4xx EMAIL_BELONGS_TO_OTHER_ACCOUNT(BR-027)
      4. 加入綁定清單,寫入 audit log,排程寄送 audit 信
  • HINT-MUTATION-3:解除綁定

    • 輸入:要解除的 provider 類型 + step-up verification token(BR-028)
    • 期望行為:
      1. 驗證 step-up token;若無效、過期、已消耗、或 (user_id, action, target_provider) 不匹配 → 回 4xx STEP_UP_REQUIRED,FE 引導用戶完成 step-up 後重試
      2. 檢查 BR-025(這是唯一登入方式嗎?)→ 若是回 4xx LAST_LOGIN_METHOD
      3. 刪除該綁定
      4. 觸發 BR-029 二層 Revocation Propagation(同步回應 FE 「解綁完成」即可,二層傳播實際執行可異步,但 L2 失敗須重試到成功;詳見 §12.7):
        • L1(best-effort):呼叫 Provider 端 revoke endpoint(Google oauth2.googleapis.com/revoke / Apple appleid.apple.com/auth/revoke
        • L2(必須成功):依 Session Provider Index invalidate 該 provider 來源所有 active sessions + 派生 child token
      5. 寫入 Revocation Propagation Job 與 audit log,排程寄送 audit 信
  • HINT-MUTATION-4:簽發 step-up verification token(BR-028 配套)

    • 輸入:
      • sensitive action 類型(unbind / add-binding 等)
      • target_provider(要對哪一個 provider 解綁 / 新增綁定;必須明示)
      • step-up 證明(OAuth re-auth 結果 或 password 重輸結果)
    • 期望行為:
      1. 驗證證明:
        • 若為 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
      2. 簽發短效一次性 token:
        • 綁定 (user_id, action, target_provider) 三元組;換 action 或換 target 都不可重用
        • TTL ≤ 5 分鐘
        • 一次性消耗(驗證通過即作廢;無論操作成功或失敗皆不可二次使用,重試需重新 step-up)
      3. 回傳 token 給 FE 作為 HINT-MUTATION-3 的輸入
  • HINT-MUTATION-5:Apple Sign in with Apple S2S Notification 接收(外部 webhook,Apple → wport)

    • 輸入:Apple JWT payload(包含 iss / aud / events claim;events 為 JWT-encoded 內層 payload,含 typesubevent_timetransaction_id
    • 期望行為:
      1. 驗證外層 JWT 簽章(Apple JWKS https://appleid.apple.com/auth/keys);簽章不過 → 回 401,記 security log
      2. 驗證 iss = https://appleid.apple.comaud = wport Apple Services ID;不符 → 回 401
      3. 解析內層 events claim 取得 type + sub(Apple OIDC sub,作為 provider_uid 查 binding)+ transaction_id
      4. Idempotency check:若 transaction_id 已記錄於 Apple Provider Lifecycle Event Log 且狀態為 done → 直接回 200(不重做)
      5. 必須在 ≤ 5 秒內回 200:寫入 Apple Provider Lifecycle Event Log(狀態 pending)後立即回 200;重活動排程到後端 queue 異步處理
      6. 異步 handler 依 event_type 分派:
        • email-disabled → 更新 Apple Relay Email Status = disabled;標記 user nudge flag
        • email-enabled → 更新 Apple Relay Email Status = active;清除 nudge flag
        • consent-revoked → 移除 binding(不需 step-up)+ 觸發 BR-029 二層傳播 + 寄通知信
        • account-delete → 移除 binding + 觸發 BR-029 二層傳播 + 寄通知信;wport user 帳號保留(用戶可從其他 provider 取回);若 Apple 為唯一登入方式 → 將帳號標記為「needs customer service intervention」狀態(§12.5)
      7. handler 完成後更新 Apple Provider Lifecycle Event Log 狀態 done;失敗則 failed + 排程重試
    • 邊界處理:
      • sub 查無對應 binding(用戶已自行解綁 / 帳號已刪)→ 記錄事件後標記 not_applicable,回 200
      • 處理過程 BR-029 任一層失敗 → event log 狀態 failed,重試 queue 持續推進至成功
      • 同一用戶短時間內收到多個 event(例如先 email-disabledaccount-delete)→ 依時間序處理,後者覆蓋前者
  • HINT-MUTATION-6:刪除 wport 帳號(含對外 Cascade)

    • 輸入:當前用戶 session(沿用 BR-012 既有 owner check + 二次確認流程)
    • 期望行為(與既有 BR-012 流程並行 / 補強):
      1. 沿用 BR-012 既有 pre-check(Owner 檢查、進行中應徵、企業 owner 狀態等)
      2. 對該 user 的每一個 User Identity Binding 觸發 BR-029 二層 Revocation Propagation,但所有層皆設為「必須成功」(含 L1,因 Apple 路徑受 App Store 5.1.1(v) 規範;Google 路徑為 GDPR 資料極小化要求)
      3. 沿用 BR-012 既有 user 軟刪除 / 履歷處理 / 應徵歷史等步驟
      4. 寫入 Revocation Propagation Job + Auth Audit Notification Log;寄送帳號刪除完成通知信
      5. 完整 cascade 結果必須可供 GDPR / 個資法 audit 查詢(用戶若申訴「為何 Apple 端仍可看到 wport 授權?」可舉證已執行 revoke)
    • 失敗處理:
      • L1 Apple auth/revoke 失敗 → 對用戶端回成功,排程重試至成功才標記帳號刪除完成;用戶 UI 顯示「處理中」狀態
      • L1 Google revoke 失敗 → 重試 N 次後若仍失敗則 fallback 為 audit log + 進 admin DLQ 人工介入(接受外部一致性殘餘風險,但須在 GDPR audit trail 留證已盡力撤銷)

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 quotaper 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_METHODEMAIL_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 gateAuto-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 對外 Cascadewport 帳號刪除(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,無獨立路由

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/Apple email_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-onlyiOS 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)

  1. 避免 SPA 狀態被打斷:redirect callback 會中斷 React/Next.js 的記憶體狀態(route guard、deep link 目標、未送出表單),須額外處理 state rehydration、CSRF token、redirect URL whitelist,FE 工程量級顯著上升。
  2. Apple 原生 JS 已支援 popup 模式:早期 Apple Web 僅支援 redirect + form POST callback 是現已過時的限制;現行 Sign in with Apple JS 已正式支援 usePopup: true,行為與 Google popup 相同,不需中間託管層也能達到「無頁面跳轉」。
  3. 與 §6.2 SEC-1/SEC-2「無頁面跳轉」一致:避免互動描述與導航圖打架。
  4. 降級路徑(v1.x 視需要再開):若實測 Safari ITP / popup blocker 造成 Apple popup 失敗率過高,再評估 Apple 專用 的 redirect fallback(Apple Sign in with Apple JS usePopup: false,配合自家 /auth/callback/apple POST 接收端)。屆時為 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 & Performanceprovider OAuth 重導超時(> 30s)取消等待,回登入頁顯示「登入逾時,請重試」Yes未建立任何綁定/帳號沿用既有 Google 行為
Network & Performance用戶連點兩次 provider 按鈕第一次請求進行中時禁用按鈕;後端以 provider UID 為冪等鍵按鈕 disabled + spinnerYesMerge Event Log 不重複Idempotency hard gate
Network & PerformanceApple OIDC 服務中斷(Apple JWKS / 授權端點不可用;僅影響 Apple;Google 走既有路徑不受影響)後端對 Apple 流程回 5xx,FE 重試 1 次後放棄;Google 仍可正常登入Apple 按鈕:「Apple 登入服務暫時無法使用,請稍後再試或改用 Google」Yes不寫入任何資料v1 兩條路徑各自獨立:任一 provider 故障時另一個自動成為 fallback
Data & InputApple 首次授權回傳 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 & Performancepopup 取得 token 後,FE 提交 BE 過程中斷網路用戶可手動重試;後端以 (provider, provider_uid) 為自然冪等鍵顯示 retry 按鈕Yes以 provider UID 為自然冪等鍵popup-only 架構下無 callback 路徑(§10.3)
Data & InputApple 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/Aprimary email 為 wport 內部欄位,不隨綁定變動
Data & InputApple 回傳的 email 與 wport primary email 大小寫不同(JOHN@gmail.com vs john@gmail.com後端以 lowercased email 為比對基準視為相同 email,正常合併N/Anormalize 後比對沿用既有 Google 處理;§5.2 normalize 能力一致
User InterruptionOAuth 過程中關閉 popup / 切換 tab後端未收到 token,視為未登入回登入頁Yes無資料寫入
User InterruptionOnboarding 填寫到一半離開已填欄位保留(沿用 BR-002 既有行為);下次登入仍導向 Onboarding下次登入顯示「請完成個人資料」Yespartial state 保留
Auth & Lifecyclesession 過期,用戶在設定頁試圖解綁後端 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 既有 UIN/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,導向登入”請先登入”YesN/A沿用
Auth & LifecycleAuto-merge 對象 email 未驗證(v2 適用攻擊面,v1 dormant):攻擊者以 LINE 註冊新帳號,將 email 填為受害者 victim@gmail.com,但 LINE 未驗證此 email後端 block auto-merge,回 EMAIL_NOT_VERIFIED”此 email 尚未經 {provider} 驗證,無法自動連結。請先以原登入方式登入後到設定頁手動綁定”No(重試無用,須換流程)不建立 binding,不寫入 Merge Event LogBR-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/Asession table 須有 provider 維度BR-029
Network & PerformanceRate limit 觸發:同 IP / email / UID 超過 BR-030 閾值後端回 429 + Retry-After header”登入嘗試過於頻繁,請於 N 秒後再試”Yes(等待 window)不建立任何資料BR-030
Network & PerformanceRate limit 服務不可用(pre-verify 層):per IP 檢查 Redis 失聯Fail-open(不阻擋)+ alert用戶無感N/A接受短期暴露 IP 維度攻擊面,oncall 介入BR-030 分層降級
Network & PerformanceRate 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(需客服介入)不建立 bindingBR-027 唯一信箱反面
Data & InputMigration 期間既有 Google 用戶登入user.google_id 既有欄位與新 binding 表並存後端雙寫:既有欄位寫入 + binding 寫入;read 優先讀 binding用戶無感Yes雙寫策略,migration 完成後移除舊欄位§12.4
Auth & LifecycleStep-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 & LifecycleStep-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)再重打 popupYes不執行解綁;token 不簽發§8.3 step-up OAuth 新鮮度
Auth & LifecycleOnboarding 中途進設定頁解綁:用戶剛 sign-up 進 Onboarding(單一 binding、無 password),透過 deep link 進入 /settings/account 試圖解綁該唯一 provider沿用 BR-025:「唯一登入方式」防呆觸發,按鈕 disabled”請先完成個人資料後新增其他登入方式” + 導回 OnboardingN/A操作未執行BR-025;Onboarding flow 不阻擋進入設定頁但動作受限
Logical InconsistencyApple sub 取錯 claim:誤將其他 Apple claim 或 client 自填值存為 provider_uid → 後續 lookup 失準預防:DB write path 強制以 Apple JWKS 驗章成功後的 sub claim 為 provider_uid 來源;不接受 client 端傳入的 subN/A(預防性 invariant)N/Acode 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 & LifecycleApple S2S consent-revoked 到達:用戶在 Apple ID 設定撤銷對 wport 的授權webhook handler 異步觸發 unbind + BR-029 二層傳播;不需 step-up;不適用 BR-025 防呆用戶在 wport 端下次操作(其他 provider 登入)後設定頁顯示「Apple 已由 Apple 端撤銷」+ 收到 audit 通知信N/Abinding 刪除;二層傳播至完成BR-031;§12.6
Auth & LifecycleApple 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 & InputApple S2S email-disabled 到達:用戶在 Apple ID 關閉 Hide My Email 轉信更新 Apple Relay Email Status = disabled;user nudge flag 設定用戶下次登入後 banner 顯示「你的 Apple 匿名信箱已停用,請設定可收信 email 以接收重要通知」N/Abinding 不變;僅狀態標記BR-031;§12.6
Data & InputApple 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 監控未完成 jobN/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 InconsistencyApple S2S webhook payload 偽造:攻擊者偽造 webhook 試圖讓系統解綁他人 Apple binding簽章驗證失敗 → 回 401,記 security log + alertN/A(純後端)不影響任何 user stateBR-031;§12.6
Logical Inconsistencywport 帳號刪除後 Apple S2S 仍到達:用戶先刪 wport 帳號,幾分鐘後刪 Apple ID,Apple S2S 通知到達但 wport binding 已不存在handler lookup binding 失敗 → 標記 event log not_applicable 後回 200N/AN/A不報錯、不重複動作BR-031;HINT-MUTATION-5

10.5 錯誤碼 → UI 對應表(FE 對齊用)

本表為 FE 工程師對接 BE 錯誤碼的 single source of truth。錯誤碼字串以 RD OpenAPI Spec 為準(若與此表名稱有差異,以 OpenAPI 為主,PM 同步更新本表);FE 行為(訊息文案、CTA、是否可重試)為 PRD 規範範疇

錯誤碼觸發來源HTTPFE UI 行為主要文案(zh-TW)CTA可重試BR / 對應章節
EMAIL_NOT_VERIFIEDHINT-MUTATION-1 進 merge 分支但 email_verified=false422Modal「此 email 尚未經 {provider} 驗證,無法自動連結。請先以原登入方式登入後到設定頁手動綁定。」「我知道了」+ 導回登入頁否(須改流程)BR-026
EMAIL_BELONGS_TO_OTHER_ACCOUNTHINT-MUTATION-2 新增綁定,provider email 已是他人 primary email409Modal「此 email 已是另一個 wport 帳號的主信箱,請聯絡客服協助合併。」「聯絡客服」(直達 §12.5 合併工單入口)+「取消」否(須客服介入)BR-027;§12.5
PROVIDER_UID_TAKENHINT-MUTATION-2 新增綁定,該 provider UID 已綁在別的 wport 帳號409Modal「此 {provider} 帳號已綁定其他 wport 帳號。如為同一人,請聯絡客服協助轉移。」「聯絡客服」+「取消」§10.4;§12.5
STEP_UP_REQUIREDHINT-MUTATION-3 / 2 缺 step-up token、token 過期、已消耗、或 (action, target_provider) 不匹配403Modal + 觸發 step-up flow「為了你的帳號安全,請先重新驗證身份。」「重做 {provider} 登入」/「輸入密碼」二選一是(完成 step-up 後重試)BR-028
STEP_UP_REQUIRED + sub_reason=STALE_AUTHHINT-MUTATION-4 驗到 provider ID Token 的 auth_time > 5 分鐘403Modal「請重新登入 {provider} 帳號。」「重新登入 {provider}」(FE 觸發 provider sign-out 再重打 popup)§8.3 step-up OAuth 新鮮度
LAST_LOGIN_METHODHINT-MUTATION-3 解綁但會導致 0 登入方式409改為純資訊框(SEC-6 阻擋變體)「這是你唯一的登入方式。請先新增其他登入方式,再來解除此項。」「新增其他登入方式」(直達 SEC-4 add binding)BR-025
RATE_LIMITED第三方登入 / 新增綁定 endpoint 觸發 BR-030429Toast「登入嘗試過於頻繁,請於 {N} 秒後再試。」(N 由 Retry-After header 提供)「我知道了」是(等待 window)BR-030
OAUTH_FAILEDprovider OAuth 失敗、popup 被擋、用戶拒絕授權N/A(FE 端錯誤)Toast「{provider} 登入失敗,請重試或改用其他登入方式。」「重試」+「使用其他方式」§6.2 SEC-1/2 錯誤狀態
OAUTH_TIMEOUTOAuth 流程 > 30s 未完成N/AToast「登入逾時,請重試。」「重試」§10.4 Network & Performance
SESSION_EXPIRED任何受保護 endpoint 收到過期 session401自動導向 /login,登入後回原頁「登入已過期,請重新登入。」自動跳轉§10.4 Auth & Lifecycle
ACCOUNT_DELETION_IN_PROGRESSHINT-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 / 401Toast + 引導至「新增登入方式」「你已在 Apple 端取消對 wport 的授權,請改用其他登入方式或重新綁定 Apple。」Yes(改用其他方式)BR-031;§12.6
ACCOUNT_NEEDS_CUSTOMER_SERVICEApple 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)

scenariobindings 數hasPassword預期 UI
single-provider-no-password1false該 binding canUnlink=false,按鈕 disabled + hover tip
single-provider-with-password1truebinding 可解綁,因 email+password 為 fallback
multi-provider-no-password2+false全部可解綁
multi-provider-with-password2+true全部可解綁
apple-hide-my-email-only1 (Apple relay)falseproviderEmail 顯示遮罩 + 加註解釋

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-001018 對齊 + 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)

12.2 設計決策依據(Design Rationale 摘錄)

決策選項對比採用SaaS 對標wport context 理由
D1 Apple Hide My Email 處理A 拒絕 / B 視為獨立身份BLinear, SlackApple App Store 規範禁止強迫分享真實 email;強制拒絕會違反平台政策
D2 LINE email scopeA 強制 / B 可選 fallbackACakeResume, 104唯一信箱是 wport 帳號模型的核心 invariant,無 email 無法套用 BR-024
D3 自動合併是否顯示確認A 確認 modal / B 靜默 / C 自動執行 + ToastCGoogle Account, Stack Overflow兼顧「絲滑至上」與透明度,避免用戶日後困惑
唯一信箱原則A 鬆散 / B 嚴格BGoogle, Stack Overflow, Linear履歷身份必須與企業端認知一致,否則應徵歷史會分裂
防呆解綁機制A 開放 / B 至少保留 1 / C 強制改密碼BGitHub, Vercel用戶常因「換 LINE 帳號」想解綁,需避免幽靈帳號
Provider 排序按使用率 / 按 platform 規範平台規範優先iOS HIG 4.8Apple 按鈕必須位於 OAuth 群組首位
D4 Apple 接入方式A Apple 原生 Sign in with Apple JS / B Firebase Auth 的 Apple providerA(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 延後 v2B階段式上線策略詳見下方 D4-defer 補充:LINE email scope 商業審核時程不可控;LinkedIn 雖支援 OIDC 但仍需 RD spike 驗證 scope / claims 行為
D5 Auto-merge email 驗證A 接受未驗證 / B 強制 email_verified=trueBGoogle, Auth0, Apple Sign in with Apple接受未驗證 email 即為帳號接管漏洞,安全紅線不容妥協
D6 解綁 step-upA 僅 session 有效 / B 重做 OAuth 或重輸密碼BGitHub, Linear, Vercelsession 竊取即可接管帳號,sensitive action 須額外驗證
D7 解綁後 session 處理A 不處理 / B Invalidate 該 provider 來源 sessionsBGoogle, GitHub不 invalidate 會造成客服困擾,且邏輯與用戶心智不符
D8 自動合併稽核通知A 僅 in-app Toast / B 額外寄 audit emailBGoogle, AWS, GitHub帳號變更類事件須符合 wport 既有稽核標準(BR-018)
D9 Rate limit 維度A 不設 / B 僅 IP / C IP + email + UID 三維度CAuth0, 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/revokeApp 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 種事件BNotion, 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 採用原生的綜合理由

  1. 開發量更省:BR-029 由三層收斂為二層、BR-012 cascade 移除 Firebase Admin deleteUser,最複雜易錯的撤銷層次自然消失
  2. 架構一致:與既有 Google 路徑同類型(直連 OIDC + JWKS),不再有「Google 一套、Apple 另一套」的雙軌
  3. 個資法面更單純:少一個 Google 服務(Firebase)保管使用者個資
  4. 使用者體驗不變:popup UX 兩條路徑相同
  5. 對應決策複審文件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:

  1. LinkedIn issuer URL、JWKS 端點實際打通的 spike
  2. scope 行為(profileemailopenid)與 wport 實際需要的 profile 欄位對齊
  3. 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

  1. BR-024 唯一信箱原則 — 新增
  2. BR-025 防呆解綁機制 — 新增
  3. BR-026 Auto-merge 須 email_verified — 新增
  4. BR-027 新增綁定 cross-account email 衝突 — 新增
  5. BR-028 Sensitive action step-up auth — 新增
  6. BR-029 Token Revocation 二層傳播(v1.5.0 由「解綁後 provider session invalidation」擴展為三層;v1.6.0 因 Apple 改回原生收斂為二層)— 新增
  7. BR-030 OAuth login rate limiting — 新增
  8. BR-031 Apple Sign in with Apple Server-to-Server Notification 處理 — 新增(v1.5.0)
  9. BR-001 修訂 — 將 Facebook 改為 Apple/LINE/LinkedIn(移除 Facebook 例舉)
  10. 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 修訂:移除原 Firebase deleteUser 步驟,因 Apple 改回原生路徑無 Firebase user record)
  11. 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 步驟

  1. 準備階段(不影響 prod)

    • 新建 User Identity Binding 結構,與 user.google_id 並存
    • 撰寫 idempotent migration script(可重跑、可中斷)
  2. 雙寫階段(migration 期間)

    • 既有 sign-in 流程:同時寫入 user.google_id(既有)與 User Identity Binding(新)
    • 既有 lookup:優先讀 User Identity Binding,找不到時 fallback 讀 user.google_id
    • v1 新增的 Apple binding 只寫入新表(v2 LINE/LinkedIn 上線時亦同)
  3. 回填階段

    • 執行 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 紀錄)」
    • 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 紀錄
  4. 切換階段

    • lookup 邏輯改為「僅讀 binding 表」(移除 fallback)
    • 雙寫期暫時保留,以便 rollback
  5. 清理階段(本期 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 帳號,且系統設計刻意不允許自助合併(避免誤合併不同人):

  1. 同一人先以 Apple Hide My Email 註冊(relay email A)、後以 Google 註冊(真實 email B),兩者視為獨立身份
  2. 同一人在不同公司期間用不同 work email 註冊(B 改 C),多份履歷分散
  3. 同一 provider UID 被前任用戶轉手給新用戶(Google 帳號買賣 / 公司 SSO 帳號回收),新用戶試圖綁定但碰到 PROVIDER_UID_TAKEN
  4. 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 客服端處理流程

  1. 核驗身份:依 wport 客服 SOP 確認兩個帳號擁有者為同一人(沿用既有 KYC 流程)
  2. 預檢衝突:兩帳號是否有矛盾資料(同時為求職者 + 企業 owner?同時擁有不同進行中應徵?)→ 有則人工裁定保留方向
  3. 執行合併(後端提供 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
  4. 不可逆性說明:合併執行前客服必須出示確認頁;合併後 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 語意層必須達成)

  1. JWT 簽章驗證:以 Apple JWKS(https://appleid.apple.com/auth/keys)驗外層 JWT 簽章;簽章不過 → 401
  2. iss 驗證:必須 = https://appleid.apple.com
  3. aud 驗證:必須 = wport 的 Apple Services ID(PRD 不寫死值,RD 從 config 讀)
  4. 內層 events claim 解析:Apple 包了兩層 JWT;外層為 transport,內層 events 才是真正 payload
  5. Idempotency:以內層 transaction_id 為 key 去重(Apple Provider Lifecycle Event Log
  6. 時間視窗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 endpointGoogle:https://oauth2.googleapis.com/revoke;Apple:https://appleid.apple.com/auth/revoke
L2 — wport 端wport 自家 session / API token / refresh tokeninvalidate 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)
L1best-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_PROGRESS polling 狀態
  • 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 Admin deleteUser;v1.6.0 Apple 改回原生後,此步驟自然消失(無 Firebase user record 需清除),cascade 收斂為「Provider revoke + wport 軟刪除」二層。

12.8.1 完整 cascade 步驟(HINT-MUTATION-6 期望行為)

  1. 既有 BR-012 pre-check:Owner 檢查、進行中應徵、企業 owner 狀態(不變)
  2. 對每個 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 必須成功
  3. wport DB 處理:依 BR-012 既有流程處理 user 軟刪除、履歷、應徵歷史等
  4. audit log:寫入 Revocation Propagation Job(最終狀態)+ Auth Audit Notification Log(cascade 完成記錄)
  5. 通知信:寄送帳號刪除完成通知至 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 須在合理時間內通知所有持有該資料的 processorApple/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_PROGRESS UI 上線(FE)
  • ✅ Audit trail 可供 GDPR / 個資法舉證(具體查詢介面可後 v1 一週上線,但資料必須 v1 留存)

13. 下一步(Next Steps)

v1(本期)

  1. 本 PRD 簽核 — Eric 確認後進入 Stage 9(Coda 回寫 + git push)
  2. 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 等)
  3. 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」任務)
  4. RD Migration script — 撰寫既有 Google 用戶 user.google_id 回填腳本(§12.4,注意 firstBoundAt 推算規則)
  5. FE 設計圖 — Figma 補上 SEC-4 / SEC-5 / SEC-6 的設計稿(含 step-up modal)
  6. business-rules.md 回寫 — 新增 BR-024~031,修訂 BR-001 / BR-012 / BR-018
  7. QA 撰寫 E2E — 至少覆蓋 §10.4 中所有 Logical Inconsistency 情境,含 BR-026~031 新增安全 / 生命週期 gate(含 Apple S2S 偽造防禦、二層 revocation L1/L2 失敗、Apple 首次 email 寫入失敗持久化、帳號刪除 cascade 中斷)
  8. Compliance 檢核(v1) — Apple App Store 4.8 / 5.1.1(v) 規範審查
  9. 客服文件補充 — 設定頁登入方式如何使用、解綁後如何救援、Apple Hide My Email 是什麼
  10. Email Template — 新增「自動合併」、「新增綁定」、「解綁」、「Apple 端撤銷/刪除觸發」、「帳號刪除 cascade 完成」五種 audit 通知信模板
  11. 監控 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
  12. GDPR / 個資法合規 review — Legal 確認 §12.8 cascade 步驟符合個資法第 11 條與 GDPR Article 17 舉證要求;audit trail 保留期由 Legal 給出建議值

v2(下一期,建立獨立 PRD 接續,LINE / LinkedIn 各自一期,不一次做完

  1. LINE email scope 商業申請 — 先行送件,等審核期(不 block v1)
  2. LinkedIn OIDC spike — RD 直接以 LinkedIn issuer + JWKS 驗證 ID Token 在 dev 環境跑通;確認 profile / email / openid scope 行為符合 wport 需求
  3. 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 補上對應條文)
  4. v2 Compliance — LINE Login terms 對齊、LinkedIn Developer Agreement 對齊

14. 一致性表(Stage 6 必填)

14.1 欄位對照表(Field Mapping)

概念本 PRD 用詞Storybookbusiness-rules.mdSoT版本
第三方綁定User Identity BindingN/A”第三方登入綁定 / User Providers” (BR-012)PRDv1
唯一信箱Primary EmailN/A待 BR-024 新增PRDv1
自動合併紀錄Merge Event LogN/A待新增PRDv1
登入方式數Login Method DiversityN/A待 BR-025 新增PRDv1
Provider 來源 session 索引Session Provider IndexN/A待 BR-029 新增PRDv1
Audit 通知信紀錄Auth Audit Notification LogN/A待 BR-018 修訂PRDv1
Step-up 一次性 tokenStep-Up Verification TokenN/A待 BR-028 新增PRDv1
Apple S2S 事件紀錄Apple Provider Lifecycle Event LogN/A待 BR-031 新增PRDv1
二層撤銷追蹤Revocation Propagation JobN/A待 BR-029 新增(v1.5.0 曾擴展為三層、v1.6.0 收斂為二層)PRDv1
Apple relay 狀態Apple Relay Email StatusN/A待 BR-031 新增PRDv1

14.2 常數表(Constants)

常數數值來源備註
自動合併 Toast 顯示秒數5 秒PRD §6.2 SEC-5FE 常數
OAuth 超時30 秒PRD §10.4沿用既有 Google 設定
Apple OAuth 重試次數(FE 端 popup 失敗 / token 取得 5xx 時)1 次PRD §10.4RD 確認
支援 provider 數(v1)2 (Google + Apple)PRD §3.2v2 擴充至 4
Provider 按鈕排序(v1)Apple → GooglePRD §6.2 SEC-1/2Apple 須首位(iOS HIG 4.8);v2 擴充為 Apple → Google → LinkedIn → LINE
Apple relay email domain*@privaterelay.appleid.comApple 官方用於 D1 判定
Rate limit per IP60 次 / 分鐘PRD §4.2 BR-030sliding window;起步值,以 prod traffic 觀察後微調(討論交由 RD + DevOps,企業 NAT 共用 egress IP 可能需放寬)
Rate limit per email10 次 / 分鐘PRD §4.2 BR-030sliding window
Rate limit per provider UID10 次 / 分鐘PRD §4.2 BR-030sliding 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.6Apple 超時視為失敗會重試
Apple S2S end-to-end SLA≤ 24 小時(Apple 發送 → wport cascade 完成)PRD §12.6 / S8PRD 立場;實際數值由 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 DLQPRD §12.7DLQ 設計由 RD 決定
Account Deletion cascade UI polling 間隔由 FE 決定(建議 3–5 秒)PRD §10.5 ACCOUNT_DELETION_IN_PROGRESSFE 常數

14.3 事件字典(Event Dictionary)

事件名稱觸發來源後端寫入payload 概念用途
auth.signin.successprovider OAuth 完成 + token 驗證通過否(log only)user_id, provider, isNewUser, merged行為分析
auth.binding.merged自動合併發生是(Merge Event Log + Audit Log)user_id, new_provider, old_providersToast 顯示、客服回溯、audit 信
auth.binding.merge_blockedmerge 因 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.requiredsensitive action 缺 step-up token否(log only)user_id, action行為分析
auth.stepup.successstep-up 驗證通過是(log)user_id, action, method (oauth/password)稽核
auth.ratelimit.triggeredBR-030 rate limit 觸發是(security log)dimension, key_masked, ip安全監控、調整閾值
auth.apple_s2s.receivedApple S2S webhook 收到(簽章已驗)是(Apple Provider Lifecycle Event Log)transaction_id, event_type, apple_sub_hashidempotency 去重、處理 lag 監控
auth.apple_s2s.processedApple S2S 事件處理完成是(Apple Provider Lifecycle Event Log 狀態更新)transaction_id, event_type, result (done / not_applicable / failed), user_idend-to-end SLA 監控
auth.apple_s2s.signature_failedApple S2S 簽章 / iss / aud 驗證失敗是(security log)source_ip, claimed_iss, claimed_aud偽造攻擊偵測
auth.revocation.l1_failedProvider 端 revoke 失敗是(Revocation Propagation Job)user_id, provider, trigger (unbind/delete/s2s), retry_countDLQ 監控、合規舉證
auth.revocation.l2_failedwport session invalidate 失敗是(Revocation Propagation Job)user_id, retry_countDLQ 監控、內部一致性風險
auth.revocation.completed二層傳播全部完成是(Revocation Propagation Job 終態)user_id, provider, trigger, duration_msend-to-end 觀測
auth.account.deleted.cascade_completedHINT-MUTATION-6 完整 cascade 完成是(Auth Audit Notification Log)user_id, providers, total_duration_msGDPR / 個資法 audit 舉證
auth.apple_first_email_write_retryApple 首次授權 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 Emailwport 帳號的核心 email,唯一信箱原則的主鍵❌ 不要稱「主信箱」於 UI(用「帳號 email」)
Relay EmailApple Hide My Email 產生的匿名轉信 email僅技術術語;UI 用「Apple 匿名 email」
Onboarding第一次登入後填寫個人資料的流程✅ 同「新手引導」

15. 版本凍結表(Version Freeze)

項目v1 / v2Blocking?理由拍板人日期
v1 範圍:Google + Apple 2 providerv1LINE / LinkedIn 延後 v2(理由見 §12.2 D4-defer)Eric2026/05/18
LINE / LinkedInv2❌(v1 不 blocking)LINE email scope 商業審核時程不可控;LinkedIn OIDC 需 spike + GCIP 付費 tier 評估Eric2026/05/18
Email + Password 流程改動不在本期沿用既有Eric2026/05/18
2FA / MFAv2+屬於另一期帳號安全強化Eric2026/05/18
企業 SSO(SAML / OIDC)v2+企業客製範圍Eric2026/05/18
BR-024 / BR-025 回寫v1須與 PRD 同期生效Eric2026/05/18
BR-026~030 回寫(auto-merge email 驗證 / cross-account 衝突 / step-up / session invalidation / rate limit)v1安全 hard gate,須與 PRD 同期生效Eric2026/05/18
BR-001 / BR-012 修訂(移除 Facebook 例舉)v1⚠️ 中不影響 RD 開發,但須與 PRD 同期 releaseEric2026/05/18
BR-018 修訂(補上帳號變更 audit 通知信)v1與 BR-026~030 同期Eric2026/05/18
v1 平台範圍:Web + iOS native appv1Android app 尚未開發,不在本 PRD 範圍。Apple Sign-In 無官方 Android native SDK;Android 第三方登入待 Android app 立項時另開 PRDEric2026/05/22
Apple ID Token aud 白名單雙值v1Web aud = Apple Services ID;iOS native aud = iOS Bundle ID;BE 必須同時接受兩者,由 config 讀取不可寫死(§8.3 / §8.2 HINT-MUTATION-1)Eric2026/05/22
Apple / Google ID Token nonce 強制驗證v1FE 產 nonce → hash → OAuth request → BE 驗章比對;iOS native ASAuthorizationAppleIDProvider 同等要求(§8.3)Eric2026/05/22
Rate limit 對驗章失敗請求計 IP quotav1攻擊者送無效 token 仍消耗 per IP quota,否則 post-verify limit 可被繞過(§8.3)Eric2026/05/22
Step-up token 消耗時機:操作成功才作廢v1業務阻擋(client error)不消耗 token;server error / 驗章失敗才作廢(§8.3)。原「無論成功失敗皆作廢」修訂為此Eric2026/05/22
Apple 採原生 Sign in with Apple(不經 Firebase)v1v1.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)Eric2026/05/22
Google 沿用既有直連 OAuth 框架v1既有 Google 框架穩定且無新增功能訴求;不變更(§10.2 補充說明)Eric2026/05/18
FE 互動鎖定:Web popup-only / iOS native 系統彈窗(v1 不引入 /auth/callback/ redirect)v1Web:避免 SPA 狀態被 redirect 打斷;Apple Sign in with Apple JS 已支援 usePopup: true。iOS native:ASAuthorizationAppleIDProvider / Google Sign-In SDK 原生系統彈窗,不使用 popup(§10.3)Eric2026/05/22
LINE / LinkedIn 採與 Apple 同類型原生 OIDC(不引入第三方託管層)v2❌(v1 不 blocking)架構決策保留至 v2;v2 啟動前由 RD spike 驗證 LinkedIn 直連 OIDC scope / claimsEric2026/05/22
既有 Google 用戶 migration(雙寫 → 切換 → 清理)v1本期完成,不延到下期。僅指 DB 內帳號模型 reshape(§12.4)Eric2026/05/18
Provider 按鈕排序(v1):Apple → Googlev1iOS HIG 4.8 規範;v2 擴充為 Apple → Google → LinkedIn → LINEEric2026/05/18
Apple provider_uid = OIDC sub(取自 Apple JWKS 驗證後的 ID Token sub claim)v1避免被 client 端傳入值或非 sub claim 取代;v2 LINE / LinkedIn 沿用同一原則(§5.1)Eric2026/05/22
Step-up token 綁定 (user, action, target_provider) 三元組 + 一次性消耗v1防止 step-up token 跨 action / 跨 provider 被重用(BR-028 / §8.3)Eric2026/05/20
Step-up OAuth 須驗 auth_time ≤ 5 分鐘v1防止 client-side SDK 從既有 session reissue 老 token 即被視為通過 step-up(§8.3)Eric2026/05/22
BR-029 session invalidate 採「同 provider 來源」最小範圍 + 已知 accepted riskv1UX vs 安全 trade-off 明示;殘餘風險靠 BR-028 step-up 攔截(§8.3)Eric2026/05/20
firstBoundAt allow null + migrated flag 配對v1與 §12.4 migration 策略一致,禁止用 created_at 偷算Eric2026/05/20
§10.5 錯誤碼 → UI 對應表v1FE 對齊 single source of truth;BE 錯誤碼字串以 OpenAPI 為準Eric2026/05/20
§12.5 帳號分裂客服 SOP(含 admin 合併能力)v1不自助合併(守 BR-024/027 邊界),但須有客服救援路徑,否則履歷永久分裂Eric2026/05/20
BR-030 fail-open 分層:pre-verify per IP → fail-open;post-verify per email/UID → fail-closedv1一刀切 fail-open 會把 BR-030 安全價值架空(§4.2 / §8.3 / §10.4)Eric2026/05/20
BR-029 採二層 Revocation Propagation:L1 Provider / L2 wportv1v1.5.0 曾因 Apple via Firebase 規範三層;v1.6.0 Apple 改回原生,Firebase 中介層消失,自然收斂為二層(§12.7)Eric2026/05/22
BR-029 二層傳播失敗處理矩陣:unbind L1 best-effort / delete L1 Apple 必須成功 / delete L1 Google 必須成功(重試 N 次仍失敗則 fallback admin DLQ)/ L2 一律必須成功v1App Store 5.1.1(v) 對 Apple delete 強制;GDPR 對 wport 端強制(§12.7)Eric2026/05/22
BR-031 Apple S2S Notification 接收能力 + 4 種 event_type 處理v1不訂閱會造成 wport 與 Apple 端永久 data drift(§12.6)。此為 Apple 平台機制,與是否經 Firebase 無關Eric2026/05/20
Apple S2S consent-revoked / account-delete 不適用 BR-025 / BR-028(外部來源 authoritative)v1阻擋外部 authoritative 事件等同 wport 自行越權;用唯一登入方式情境改走 §12.5 客服恢復(§12.6.4)Eric2026/05/20
Account Deletion Cascade:對外 cascade 含 Apple auth/revoke(必須成功)+ Google revoke(必須成功,重試 N 次仍失敗 fallback admin DLQ)v1App Store 5.1.1(v) + GDPR Article 17 + 個資法第 11 條(§12.8.2)。v1.5.0 曾規範額外的 Firebase deleteUser,v1.6.0 改回原生後此步驟消失Eric2026/05/22
Apple 首次授權 email 寫入持久化保護:DB 寫入失敗必須排 retry queue / DLQ 至成功,不可僅標 binding 完成但 email 為 nullv1取代原 v1.2.0「以 Firebase 託管 email 解決」之保險;改回原生後此保護成為硬性需求(§5.2 / §8.3 / §10.2 補充說明)Eric2026/05/22
Apple S2S webhook 安全性:JWT 簽章驗證 + iss + aud + transaction_id idempotency + event_time ≤ 7 天v1防偽造、防 replay、防重複處理(§12.6.2)Eric2026/05/20
Apple S2S email-disabled 用戶 nudge 機制v1避免 wport 長期寄信到失效 relay(§12.6.3)Eric2026/05/20
ACCOUNT_DELETION_IN_PROGRESS UI 狀態:cascade 未完成前禁止對用戶宣稱「已刪除」v1避免「wport 標示已刪但 Apple 端仍持有授權」此類合規誤導(§12.8.3)Eric2026/05/20
Apple relay 失效後通知信寄不到v1⚠️ accepted risk用戶若關閉 Apple Hide My Email 轉信且 primary email 為 relay → 通知信會被退回;屬已知缺口,待 v1.x 補「強制設定可收信 email」此 UX 弱化此風險(§12.6.4)Eric2026/05/20

無未解決的 blocking 項目。


16. Sync-fix 清單

#對象動作OwnerBlocking?
1business-rules.md新增 BR-024 唯一信箱原則Eric(PM)✅ 是(v1 release 前完成)
2business-rules.md新增 BR-025 防呆解綁機制Eric(PM)✅ 是
3business-rules.md BR-001將「Facebook」例舉改為「Apple」(v2 再補 LINE/LinkedIn)Eric(PM)⚠️ 中
4business-rules.md BR-012provider 綁定清除清單修訂(v1 補 Apple;v2 補 LINE/LinkedIn)Eric(PM)⚠️ 中
5Coda i-R_41yO3CdGPRD 欄位設為 PM_39Claude / Eric✅ 是(Stage 9)
6RD OpenAPI Spec依 §5、§7、§8 撰寫 endpoint 規格RD lead✅ 是(開發前)
7Figma補上 SEC-4 / SEC-5 / SEC-6 設計稿Designer⚠️ 中
8QA Test PlanE2E 涵蓋 §10.4 全部 Logical InconsistencyQA⚠️ 中
9客服 FAQ補上「解綁救援」、「Apple Hide My Email 是什麼」客服⚠️ 中
10business-rules.md新增 BR-026 auto-merge email 驗證 gateEric(PM)✅ 是(安全 hard gate)
11business-rules.md新增 BR-027 cross-account email blockEric(PM)✅ 是
12business-rules.md新增 BR-028 sensitive action step-up authEric(PM)✅ 是
13business-rules.md新增 BR-029 解綁後 provider session invalidationEric(PM)✅ 是
14business-rules.md新增 BR-030 OAuth login rate limitingEric(PM)✅ 是
15business-rules.md BR-018補上「帳號變更類事件須寄 audit 通知信」Eric(PM)✅ 是
16Apple 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 路徑 audRD lead + iOS FE lead(⑤ 項需 iOS FE 配合確認 Bundle ID)✅ 是(開發前;Card C 開工前必完成)
16bLinkedIn / LINE Developer 後台(v2 預備)各自開發者後台申請 OAuth client、配置 redirect URI、申請所需 scope(LINE email scope 商業審核見 #22)RD lead❌ v1 不 blocking
17RD migration script撰寫既有 Google 用戶 user.google_idUser Identity Binding 回填腳本;雙寫 → 切換 → 清理流程RD lead✅ 是(本期完成)
18Email Template新增「自動合併」、「新增綁定」、「解綁」三種 audit 通知信模板PM + 客服⚠️ 中
19Session 識別能力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 等安全 metricRD + DevOps⚠️ 中
21Compliance(v1)Apple App Store 4.8 規範審查PM + Legal✅ 是(v1 上線前)
21bCompliance(v2 預備)LINE Login Terms / LinkedIn Developer Agreement 檢核(OIDC 用法是否符合條款)PM + Legal❌ v1 不 blocking
22LINE 商業審核送件 LINE Login email permission 申請(v2 前置)PM + Legal❌ v1 不 blocking,但建議 v1 上線後立即送件以縮短 v2 等待
23RD 實作(Apple provider_uidDB write path 強制以 Apple JWKS 驗章成功後的 ID Token sub claim 為 provider_uid 來源;不接受 client 端傳入的 sub;單元測試覆蓋RD lead✅ 是(§5.1)
24RD 實作(Step-up token)Token 必須綁 (user_id, action, target_provider);一次性消耗;TTL ≤ 5 min;OAuth re-auth 驗 auth_time ≤ 5 minRD lead✅ 是(BR-028 / §8.3)
25RD 實作(Idempotency 等價判定)並發 auto-merge 第二請求走 sign-in 分支,不重複寫 Merge Event Log / 不重複寄 audit 信;DB unique constraint + 鎖策略由 RD 決定,但 observable 行為須等價於 serializableRD lead✅ 是(§5.2)
26RD 實作(BR-030 分層 fail-open)pre-verify per IP → fail-open;post-verify per email / UID → fail-closed(503);兩層 metric + alertRD + DevOps✅ 是(§4.2 BR-030)
27RD migration(firstBoundAtSchema 允許 firstBoundAt null;同時保留 migrated flag;查詢 / UI 以 flag 為準;禁止 COALESCE(firstBoundAt, created_at) 偷算RD lead✅ 是(§5.1 / §12.4)
28FE 對接依 §10.5 錯誤碼對應表實作 UI 行為;客服 CTA 直達 §12.5 工單入口;本地 retry 上限 3 次FE lead✅ 是(§10.5)
29客服系統新增「帳號合併申請」工單表單,含 §12.5.2 必填欄位;FAQ 與內訓覆蓋 SOP客服 + PM✅ 是(v1 上線前;§12.5)
30RD admin 工具提供 §12.5.3 第 3 步全部能力(轉移 binding、歸戶履歷、軟刪除、audit log、雙端通知信);admin UI 可延後一週,但能力必須 v1 具備RD lead✅ 是(§12.5.5)
31Admin 合併治理雙人複核流程沿用既有 admin governance;本 PRD 不重寫Admin / Security⚠️ 中(§12.5.4)
32business-rules.md新增 BR-031 Apple Sign in with Apple S2S Notification 處理Eric(PM)✅ 是(v1 release 前完成)
33business-rules.md BR-029收斂為二層 Revocation Propagation(L1 Provider / L2 wport)+ 失敗處理矩陣(v1.6.0 由 v1.5.0「三層含 Firebase」改回二層)Eric(PM)✅ 是
34business-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)
36RD 實作(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✅ 是
37RD 實作(二層 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✅ 是
38RD 實作(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✅ 是
38bRD 實作(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 新增)
39Email 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⚠️ 中
41Compliance(v1)Legal 確認 §12.8 cascade 步驟符合個資法第 11 條與 GDPR Article 17 舉證要求;audit trail 保留期由 Legal 給出建議值PM + Legal✅ 是(v1 上線前)
42客服 FAQ補上「Apple 端撤銷後如何恢復登入」、「帳號刪除處理中為何要等待」、「Apple email-disabled nudge 是什麼」客服⚠️ 中
43FE 對接新增 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✅ 是
44iOS FE 實作(Apple Sign-In)整合 ASAuthorizationAppleIDProvider;發起前產生 random nonce + SHA256 hash 帶入 request;取得 identityToken 後 POST 給 BE(帶 client_type=ios);沿用 §10.5 全部錯誤碼 UI 行為iOS FE lead✅ 是(v1 scope)
45iOS 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)
46RD 實作(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✅ 是
47RD 實作(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 不支援 Facebookv1 改為「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 設計確認UXUI 細節Designer
5§10.4 Apple relay email 失效用戶停用 Apple relay 後,wport 無法送信給該帳號;目前策略為「不主動處理」提供 in-app 修改 primary email 的能力?本期暫不收,待用量資料佐證後續優化未來迭代
6§12.4 既有 Google 用戶 migrationfirstBoundAt 若無專屬欄位可參考,不應用 user.created_at 推算留 null + 標記 migrated=true,UI 顯示「綁定時間:未知(migration 紀錄)」一致性資料正確性RD

17.1 RD Code Review 補充討論(2026/05/18 收斂)

以下為 PM 與資深後端在 PRD 進入 OpenAPI Spec 前的 review 紀錄。所有項目皆已收斂並回寫到本 PRD 對應章節。

#類別原始問題結論 / 採用方案對應 PRD 章節
Q1ArchitectureFirebase 真的能託管 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
Q1bArchitecture為什麼 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
Q3Migration既有 Google 用戶 user.google_id 欄位如何 migrate?本期或下期?v1 完成。採雙寫 → 切換 → 清理三階段策略,可 rollback。Owner=RD。firstBoundAt 若無專屬欄位則留 null + migrated=trueuser.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+passwordBR-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 + metricBR-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 章節
R1Scopev1 範圍是否該包含 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
R3MigrationfirstBoundAtuser.created_at 推算是錯的(先 email 註冊、後綁 Google 的用戶會被誤標)改為「若無 user.google_linked_at 專屬欄位 → 留 null + 標記 migrated=true§12.4 step 3;§17 #6
R4PRD 邊界(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
R5PRD 邊界BR-025 併發漏洞處理是否寫進 PRD?不寫。PRD 已表達「BR-025 不可被併發繞過」此語意,交易隔離等級、鎖實作由 RD 決定§4.2 BR-025;§12.1
R6PRD 邊界Rate limit pre-token-verify / post-token-verify 分層是否寫進 PRD?不寫。PRD 只列三維度(IP / email / UID)與閾值;實作層級由 RD 決定§4.2 BR-030;§10.2 補充
R7PRD 邊界60/min per IP 對企業 NAT 是否過低?不寫死 v1 數字。§14.2 仍列原值供 RD 起步,但加註「以 prod traffic 觀察後微調,討論交由 RD + DevOps」§14.2
R8PRD 邊界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-onlysignInWithRedirect 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 解綁任意 providerToken 綁 (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
S7FE 對齊錯誤碼散落各章節,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
T2OAuth 生命週期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_failedauth.revocation.l1_failed/l2_failed/completedauth.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 riskApple 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
U2D4 理由複審原 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
U3Token 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 事件字典
U4Account Deletion Cascadewport 軟刪除 + 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
U5Apple 首次 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
U6Provider UID 來源Apple sub 從 Firebase Admin SDK 驗 token 後的 firebase.identities['apple.com'][0] 取出;明確禁存 Firebase UID(decodedToken.uidApple sub 從 Apple JWKS 驗章成功後的 ID Token sub claim 直接取出;不再有 Firebase UID 議題§5.1 Provider UID 識別主鍵語意;§16 #23
U7FE Apple 登入互動signInWithPopup(auth, new OAuthProvider('apple.com')) 由 Firebase SDK 包裝AppleID.auth.signIn({ usePopup: true })(或同等原生 API);popup UX 與 Firebase 路徑相同§6.2 SEC-1;§10.3
U8Apple 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
U9Sync-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-024031 全部保留、SEC-1SEC-6 UI 不變、§10.1 User Flow 不變、§12.5 客服 SOP 不變
U11上架時程預期較有利(BR-029 由三層減為二層、cascade 移除 Firebase deleteUser、不需 Firebase 後台設定學習成本);不額外算工時,但 sync-fix 任務減少§13 v1 Next Steps
U12LINE / 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 用戶雙寫firstBoundAt null + migrated flag(§5.1 / §12.4)、Apple S2S email-disabled 標記與 user nudge(BR-031 / §12.6.3)、Apple S2S transaction_id idempotency 去重(§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 章節
M1Scope未明示平台範圍,通篇預設 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 登入會全部 401aud 白名單改雙值: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
M7UX / 安全§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

文件結束