Files
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

37 lines
3.6 KiB
Markdown
Raw Permalink 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.
## 1. 帳號鎖定設定檔與共用 Helper
- [x] 1.1 [後端] 建立 `config/auth_lockout.php`,定義 `max_attempts`env `LOCKOUT_MAX_ATTEMPTS`, 預設 5)與 `decay_minutes`env `LOCKOUT_DECAY_MINUTES`, 預設 15
- [x] 1.2 [後端] 建立 `app/Traits/NormalizesEmail.php` Trait,定義 `normalizeEmail(string $email): string``strtolower(trim($email))`),在 `AuthController``ProviderAuthController` 均 use 此 Trait
## 2. Member 帳號鎖定邏輯
- [x] 2.1 [後端] 修改 `app/Http/Controllers/API/AuthController.php``loginMember()`:對 request email 執行 `strtolower(trim())` 正規化後,先查 Cache key `login_failures:member:{email}`,若已達閾值讀取 `login_expires_at:member:{email}` 回傳 HTTP 423(含 `locked_until`
- [x] 2.2 [後端] DB 查無帳號時直接回傳 HTTP 401(訊息與密碼錯誤相同),**不**遞增計數
- [x] 2.3 [後端] 登入失敗(帳號存在但密碼錯)時:若 `login_failures` key 不存在則用 `Cache::put()` 設初始值 1 並同步寫入 `login_expires_at` companion keyTTL 相同);若 key 已存在則只 `Cache::increment()`(不重設 TTL);回傳 423 時讀取 `login_expires_at` key,若缺失則 fallback 為 `now()->addMinutes($decay_minutes)` 並寫 `Log::warning`
- [x] 2.4 [後端] 登入成功時 `Cache::forget('login_failures:member:{email}')``Cache::forget('login_expires_at:member:{email}')`
## 3. Provider 帳號鎖定邏輯
- [x] 3.1 [後端] 修改 `app/Http/Controllers/API/ProviderAuthController.php``loginProvider()`:同 Member 邏輯(email 正規化、不存在帳號不遞增、companion key、fixed window),Cache key 前綴改為 `login_failures:provider:` / `login_expires_at:provider:`
- [x] 3.2 [後端] 登入成功時清除兩個 Cache keyfailures + expires_at
## 4. OAuth State 驗證
- [x] 4.1 [後端] 修改 `app/Http/Controllers/API/SocialAuthController.php``redirectToProvider()`:移除 `stateless()`,產生 `bin2hex(random_bytes(32))``session('oauth_state')`,附加 `state` 於 redirect URL
- [x] 4.2 [後端] 修改 `handleProviderCallback()`:移除 `stateless()`,讀取 `request->state``session('oauth_state')` 比對;不符或缺失時回傳 HTTP 400;比對後立即 `session()->forget('oauth_state')`
## 5. 前端錯誤處理
- [x] 5.1 [前端] 修改 `frontend/src/views/LoginView.vue`axios catch 中判斷 `error.response?.status === 423`,顯示 `response.data.message`(帳號鎖定提示),而非泛用錯誤
- [x] 5.2 [前端] 修改 `frontend/src/views/CoachLoginView.vue`:同上,處理 423 狀態碼
- [x] 5.3 [前端] 修改 `frontend/src/views/AuthCallbackView.vue`:當後端 OAuth callback 回傳 400 時,redirect 至 `/login?error=oauth_failed`
- [x] 5.4 [前端] 修改 `frontend/src/views/LoginView.vue`:若 `route.query.error === 'oauth_failed'`,顯示「OAuth 授權失敗,請重新嘗試」提示
## 6. 整合驗證
- [x] 6.1 [整合測試] 手動測試 Member 登入:連續失敗 5 次後收到 423,等待 15 分鐘後或改正確密碼(需清除 Cache)後可登入
- [x] 6.2 [整合測試] 手動測試 Provider 登入:同上
- [x] 6.3 [整合測試] 手動測試 OAuth:正常流程可完成;直接訪問 callback URL(無 state)收到 400 並 redirect 至 `/login?error=oauth_failed`
- [x] 6.4 [整合測試] 確認 `storage/framework/sessions` 目錄在 Docker 容器中有寫入權限(session 存 state 不報錯)
- [x] 6.5 [部署] 確認生產環境 `.env` 設定 `CACHE_DRIVER=redis`;驗證方式:`php artisan tinker``Cache::put('test', 1, 10); Cache::get('test');` 回傳 1 即 Redis 正常