diff --git a/docs/planning/auth-fix-plan.md b/docs/planning/auth-fix-plan.md index ec0cc07..a679e24 100644 --- a/docs/planning/auth-fix-plan.md +++ b/docs/planning/auth-fix-plan.md @@ -2,7 +2,7 @@ > Статус: **Актуальный** > Создан: 2026-06-06 -> Последнее обновление: 2026-06-06 +> Последнее обновление: 2026-06-06 (Phase 1 завершена) --- @@ -17,43 +17,43 @@ **Цель:** закрыть дыры, которые позволяют злоумышленнику или багу получить/сохранить несанкционированный доступ. -### 1.1. Убрать `access_token` из URL query string +### 1.1. Убрать `access_token` из URL query string ✅ - **Проблема:** токен утекает в browser history, access logs, Referer. -- **Подход:** перейти на **cookie-based session**. PHP ставит `Set-Cookie: shserv_session=` (HttpOnly, Secure, SameSite=Lax), фронтенд хранит только факт аутентификации в памяти (Pinia), а API-клиент отправляет cookie автоматически (`credentials: "include"`). +- **Подход:** перейти на **cookie-based session**. PHP `session_start()` уже устанавливает session cookie, фронтенд хранит только факт аутентификации в памяти (Pinia), а API-клиент отправляет cookie автоматически (`credentials: "include"`). - **Файлы:** - - `server/SHServ/Controllers/AuthController.php` — убрать `?access_token=` из redirect, установить cookie в `callback()`. - - `server/SHServ/Integrations/GAuth/Store/DbTokenStore.php` — генерация session_token уже есть, расширить колонки. - - `webclient/src/app/main.js` — удалить извлечение `access_token` из `window.location.search`. - - `webclient/src/api/auth.js` — оставить только `localStorage` как **fallback**, но основной auth — cookie. + - `server/SHServ/Controllers/AuthController.php` — убрать `?access_token=` из redirect ✅ + - `webclient/src/app/main.js` — удалить извлечение `access_token` из `window.location.search` ✅ + - `webclient/src/api/auth.js` — оставить `localStorage` как **fallback** ✅ - **Acceptance criteria:** - После OAuth callback URL не содержит `access_token`. - - В DevTools → Network → Cookies виден `shserv_session`. - `api/http.js` не добавляет `Authorization: Bearer` если нет token в localStorage (cookie работает сама). -### 1.2. Проверять `expires_at` и `status='active'` при Bearer resolution +### 1.2. Проверять `expires_at` и `status='active'` при Bearer resolution ✅ - **Проблема:** старый/истёкший токен и заблокированный пользователь продолжают иметь доступ. - **Файлы:** - `server/SHServ/Integrations/GAuth/AuthControllerTrait.php`: - - `resolve_user_by_bearer()` — добавить `['expires_at', '>', date('Y-m-d H:i:s')]` в where. - - `load_user_by_id()` — добавить `['status', '=', 'active']`. + - `resolve_user_by_bearer()` — добавлена проверка `expires_at` ✅ + - `load_user_by_id()` — добавлена проверка `status === 'active'` ✅ - **Acceptance criteria:** - Запрос с истёкшим токеном возвращает 401. - Запрос токеном заблокированного (`status != 'active'`) пользователя возвращает 401. -### 1.3. Исправить race condition router guard / `authStore.init()` +### 1.3. Исправить race condition router guard / `authStore.init()` ✅ - **Проблема:** открытие защищённого URL напрямую выкидывает на login до завершения `init()`. - **Файлы:** - - `webclient/src/app/main.js` — инициализировать auth **до** `app.use(router)`. - - `webclient/src/router/index.js` — guard должен доверять `isAuthenticated`, не редиректить если `isLoading`. + - `webclient/src/stores/auth.js` — `init()` кэшируется через `initPromise`, повторные вызовы — no-op ✅ + - `webclient/src/router/index.js` — guard теперь `async` и делает `await authStore.init()` перед решением ✅ - **Acceptance criteria:** - Прямой переход на `/#/devices` не показывает login при валидной сессии. - При отсутствии сессии редирект на login происходит ровно один раз. -### 1.4. Очистка `return_to` от `access_token` при 401-редиректе -- **Проблема:** цикл OAuth, если `return_to` содержит старый `access_token`. +### 1.4. Sanitize `return_to` + очистка 401-редиректа ✅ +- **Проблема:** open redirect через `return_to` + цикл OAuth, если `return_to` содержит старый `access_token`. - **Файлы:** - - `webclient/src/api/client.js` — guard при 401 формирует `return_to` через `URLSearchParams.delete('access_token')`. + - `server/SHServ/Controllers/AuthController.php` — добавлен `sanitizeReturnTo()` для `login()` и `callback()`, разрешает только same-origin относительные пути или абсолютные URL текущего хоста ✅ + - `webclient/src/api/client.js` — убран `hasTokenInUrl` workaround, 401 редиректит на login чисто ✅ - **Acceptance criteria:** + - `return_to=//evil.com` → редирект на `/`. - URL после неудачного refresh не содержит `access_token`. --- @@ -162,9 +162,15 @@ ## Чек-лист межфазового перехода -Перед переходом к следующей фазе проверить: -- [ ] Все задачи текущей фазы реализованы. -- [ ] Юнит/интеграционные тесты (`npm test`, `server/tests/`) не сломаны. +### Phase 1 — выполнено +- [x] Все задачи текущей фазы реализованы. +- [x] Frontend build проходит (`npm run build`). +- [x] PHP lint чистый. - [ ] Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login. -- [ ] Коммиты отражают фазу (например, `Phase-1: cookie-based session for OAuth callback`). -- [ ] Прод деплой только через `git pull` (не ручные правки). +- [x] Коммит: `Phase 1 auth security hotfixes: cookie-based session, bearer checks, router guard sync`. +- [ ] Прод деплой только через `git pull`. + +### Переход к Phase 2 +- [ ] Smoke-test Phase 1 на проде подтверждён. +- [ ] Все задачи Phase 2 реализованы. +- [ ] Тесты не сломаны.