Files
CFDivePlatform/openspec/specs/oauth-state-validation/spec.md
T
a620906209 2a0e9255ae security(auth): P2 帳號鎖定 + OAuth state 驗證
- Account lockout:連續失敗 5 次鎖定 15 分鐘(Fixed Window,Cache)
  - Member / Provider 各自獨立 namespace
  - Email 正規化(strtolower+trim)
  - 不存在帳號不累計計數(防 Cache 污染)
  - companion key 記錄 locked_until(ISO 8601)
- OAuth CSRF 防護:移除 stateless(),手動 state 生成與驗證
  - session('oauth_state') + session('state') 雙寫確保 Socialite 內建驗證通過
  - state mismatch redirect 至 /login?error=oauth_failed
- throttle 從 5,1 調整為 10,1,避免 IP throttle 在 lockout 前觸發
- NormalizesEmail Trait、config/auth_lockout.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 01:20:25 +08:00

36 lines
2.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## ADDED Requirements
### Requirement: OAuth 授權流程帶 state 參數防 CSRF
後端 SHALL 在 OAuth redirect 前產生隨機 state 字串(至少 32 bytes hex),存入 `session('oauth_state')`,並附加於 OAuth redirect URL 的 `state` query parameter。`stateless()` 呼叫 SHALL 被移除;Socialite 在 `redirect()` 內會自行寫入 `session('state')`,實作 SHALL 在 `redirect()` 呼叫後以同一 state 值覆蓋 `session('state')`,確保 Socialite 內建驗證與手動驗證使用相同值。callback 時 SHALL 從 request 讀取 `state` 並與 `session('oauth_state')` 比對(`hash_equals`);比對成功後 SHALL 立即清除;不符或缺少時 SHALL redirect 至 `/login?error=oauth_failed`
#### Scenario: 正常 OAuth 登入流程
- **WHEN** 使用者點擊「Google 登入」,後端發出 redirect
- **THEN** redirect URL 包含 `state=<random>` 參數,且相同 random 值存入 session;使用者在 Google 完成授權後,callback 帶回相同 state,後端驗證通過,繼續正常登入流程
#### Scenario: state 缺失(直接訪問 callback
- **WHEN** 攻擊者或使用者直接訪問 `/auth/google/callback` 而不帶 `state` 參數
- **THEN** 後端 redirect 瀏覽器至 `{frontend_url}/login?error=oauth_failed`,不繼續登入流程
#### Scenario: state 不符(CSRF 攻擊)
- **WHEN** callback 帶的 `state``session('oauth_state')` 中的值不一致
- **THEN** 後端 redirect 瀏覽器至 `{frontend_url}/login?error=oauth_failed`,不繼續登入流程
#### Scenario: state 只能使用一次
- **WHEN** callback 驗證 state 成功後(`session()->pull('oauth_state')` 已清除),再次以相同 state 訪問 callback
- **THEN** session 中已無 `oauth_state`,視為 state 缺失,同樣 redirect 至 `/login?error=oauth_failed`
#### Scenario: 登入頁顯示 OAuth 失敗訊息
- **WHEN** 瀏覽器被 redirect 至 `/login?error=oauth_failed`
- **THEN** `LoginView.vue` 偵測到 `route.query.error === 'oauth_failed'`,顯示「OAuth 授權失敗,請重新嘗試」提示;`AuthCallbackView.vue` 不涉及此錯誤流程
#### Scenario: 多分頁並發 OAuth flow — 只有最後啟動的成功(預期行為)
- **WHEN** 使用者在同一瀏覽器開兩個 tabTab A 先啟動 OAuthsession 寫入 state_A),Tab B 後啟動 OAuthsession 覆寫為 state_B),Tab A 的 callback 先到達後端
- **THEN** Tab A 的 callback 帶 state_A,與 session 中的 state_B 不符,後端 redirect 至 `/login?error=oauth_failed`Tab B 的 callback 帶 state_B,比對成功,正常完成登入
### Requirement: OAuth session driver 正常運作
後端 SHALL 確保 session driver 在 Docker 環境中可寫入(`storage/framework/sessions` 目錄存在且有寫入權限),以支援 state 存取。
#### Scenario: Session 寫入成功
- **WHEN** OAuth redirect 觸發時
- **THEN** state 值成功寫入 session,無 500 錯誤