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>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
## 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 key(TTL 相同);若 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 key(failures + 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 正常
|
||||
Reference in New Issue
Block a user