# План устранения проблем авторизации

> Статус: **Актуальный**  
> Создан: 2026-06-06  
> Последнее обновление: 2026-06-08 (Phase 4 завершена)

---

## Контекст

После аудита авторизации (OAuth + SPA same-domain) выявлено 17 проблем разной критичности.  
Этот документ — единственный источник правды по порядку исправлений. Каждая фаза должна быть завершена полностью перед переходом к следующей.

---

## Phase 1 — Security Hotfixes (P0)

**Цель:** закрыть дыры, которые позволяют злоумышленнику или багу получить/сохранить несанкционированный доступ.

### 1.1. Убрать `access_token` из URL query string ✅
- **Проблема:** токен утекает в browser history, access logs, Referer.
- **Подход:** перейти на **cookie-based session**. PHP `session_start()` уже устанавливает session cookie, фронтенд хранит только факт аутентификации в памяти (Pinia), а API-клиент отправляет cookie автоматически (`credentials: "include"`).
- **Файлы:**
  - `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`.
  - `api/http.js` не добавляет `Authorization: Bearer` если нет token в localStorage (cookie работает сама).

### 1.2. Проверять `expires_at` и `status='active'` при Bearer resolution ✅
- **Проблема:** старый/истёкший токен и заблокированный пользователь продолжают иметь доступ.
- **Файлы:**
  - `server/SHServ/Integrations/GAuth/AuthControllerTrait.php`:
    - `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()` ✅
- **Проблема:** открытие защищённого URL напрямую выкидывает на login до завершения `init()`.
- **Файлы:**
  - `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. Sanitize `return_to` + очистка 401-редиректа ✅
- **Проблема:** open redirect через `return_to` + цикл OAuth, если `return_to` содержит старый `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`.

---

## Phase 2 — Session Stability (P1)

**Цель:** пользователь остаётся залогиненым дни и недели без ручных повторных входов.

### 2.1. Автоматический refresh токена при 401 ✅
- **Проблема:** истечение access_token мгновенно разлогинивает.
- **Файлы:**
  - `webclient/src/stores/auth.js` — в `init()` при 401 сначала вызывает `refreshToken()`, потом повторяет `apiGet("/auth/me")` ✅
  - `webclient/src/api/client.js` — при 401 ответа делает **один** вызов `/auth/refresh` (с queue, чтобы не делать N параллельных), затем retry исходного запроса ✅
- **Acceptance criteria:**
  - Токен истёк → frontend автоматически обновляет и retry запроса без видимого разлогина.
  - Если refresh тоже 401 → только тогда редирект на login.

### 2.2. Поддержать Bearer-token fallback в `refresh()` и `logout()` ✅
- **Проблема:** эти endpoint'ы читают только `$_SESSION`, но SPA может работать без session cookie.
- **Файлы:**
  - `server/SHServ/Controllers/AuthController.php` — `refresh()` и `logout()` теперь извлекают access_token из `Authorization` header, ищут session по `access_token` в `shserv_sessions`, и работают от найденной записи ✅
- **Acceptance criteria:**
  - Вызов `POST /auth/refresh` с `Authorization: Bearer <token>` возвращает новый токен даже без PHP session cookie.
  - `POST /auth/logout` с валидным Bearer отзывает сессию.

### 2.3. Использовать `expires_in` на фронтенде (proactive refresh) ✅
- **Проблема:** фронтенд не знает, когда токен истечёт, и ждёт 401.
- **Файлы:**
  - `webclient/src/api/auth.js` — сохраняет `expires_at` (timestamp) рядом с token в localStorage ✅
  - `webclient/src/stores/auth.js` — `setTimeout` на обновление за 60 секунд до expiration, с cancel при logout ✅
- **Acceptance criteria:**
  - Токен обновляется **до** истечения, 401 от API не происходит при нормальной работе.

### 2.4. Убрать дублирование извлечения токена из URL ✅
- **Проблема:** `main.js` и `LoginPage.vue` оба пытались обработать callback → лишний `me`.
- **Файлы:**
  - `webclient/src/features/auth/pages/LoginPage.vue` — очищен, не содержит логики OAuth callback ✅
  - `webclient/src/app/main.js` — единственная точка обработки ✅
- **Acceptance criteria:**
  - `LoginPage.vue` не содержит логики OAuth callback.

---

## Phase 3 — Infrastructure Hardening (P2)

**Цель:** защитить от stolen tokens, несовместимостей окружения и abuse.

### 3.1. Записывать IP/UA в сессии и проверять при авторизации ✅
- **Проблема:** stolen token полностью работает с любого устройства.
- **Файлы:**
  - `server/SHServ/Integrations/GAuth/Store/DbTokenStore.php` — `put()` добавляет `ip_address`, `user_agent` ✅
  - `server/SHServ/Integrations/GAuth/AuthControllerTrait.php` — `resolve_user_by_bearer()` проверяет совпадение ✅
- **Acceptance criteria:**
  - Запрос с валидным токеном, но другого IP/UA → 401 с кодом `session_suspicious`.

### 3.2. Fix `getallheaders()` для nginx ✅
- **Проблема:** функция может отсутствовать в nginx + php-fpm.
- **Файлы:**
  - `server/SHServ/Controllers/WebhookController.php` — заменить `getallheaders()` на fallback из `$_SERVER` ✅
- **Acceptance criteria:**
  - Webhook успешно принимается на проде (nginx).

### 3.3. Rate limiting на auth endpoints ✅
- **Проблема:** brute force state, спам OAuth flow.
- **Файлы:**
  - `server/SHServ/Integrations/GAuth/RateLimiter.php` — file-based sliding-window rate limiter ✅
  - `server/SHServ/Controllers/AuthController.php` — `checkAuthRateLimit()` вызывается в `login()` и `callback()` ✅
- **Acceptance criteria:**
  - 10+ запросов `/auth/login` или `/auth/callback` в минуту с одного IP → 429.

### 3.4. Cache permissions ✅
- **Проблема:** `PermissionResolver` делает 3 запроса к БД на каждый API call.
- **Файлы:**
  - `server/SHServ/Integrations/GAuth/PermissionResolver.php` — static cache на время запроса ✅
- **Acceptance criteria:**
  - Повторные `require_permission()` в рамках одного запроса не бьют в БД.

---

## Phase 4 — Cleanup & Polish (P3)

**Цель:** убрать технический долг, привести код к стандартам проекта.

### 4.1. Исправить миграцию — убрать raw string interpolation ✅
- **Проблема:** seed-данные вставляются через строковую интерполяция в SQL (потенциальная SQLi).
- **Файлы:**
  - `server/database/migrations/2026_06_06_000001_gauth_integration.php` — заменить raw `INSERT IGNORE` на `ThinBuilder::insert()` или параметризованные запросы.
- **Acceptance criteria:**
  - Нет конкатенации переменных в SQL строках миграции.

### 4.2. LoginPage.vue стили — перейти на gnexus-ui-kit ✅
- **Проблема:** используются CSS custom properties вместо utility-классов kit'а.
- **Файлы:**
  - `webclient/src/features/auth/pages/LoginPage.vue` — заменить `var(--color-*)` на `.text-primary`, `.text-muted`, `.border-subtle` и т.д.
- **Acceptance criteria:**
  - В `<style scoped>` блоке нет `var(--color-*)`.

### 4.3. Дедупликация .env файлов ✅
- **Проблема:** `server/.env` и `server/SHServ/.env` дублируют GAuth секреты. Поддерживать два файла рискованно.
- **Файлы:**
  - `server/SHServ/config.php` — уже ищет `.env` в текущей и родительской директории, так что `server/SHServ/.env` можно удалить если он дублирует.
- **Acceptance criteria:**
  - Один источник правды для секретов на окружении.

---

## Чек-лист межфазового перехода

### Phase 1 — выполнено
- [x] Все задачи текущей фазы реализованы.
- [x] Frontend build проходит (`npm run build`).
- [x] PHP lint чистый.
- [x] Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login.
- [x] Коммит: `Phase 1 auth security hotfixes: cookie-based session, bearer checks, router guard sync`.
- [x] Прод деплой только через `git pull`.

### Phase 2 — выполнено
- [x] Все задачи текущей фазы реализованы.
- [x] Frontend build проходит (`npm run build`).
- [x] PHP lint чистый.
- [x] Тесты не сломаны (13 pre-existing failures).
- [x] Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login.
- [x] Прод деплой только через `git pull`.

### Phase 3 — выполнено
- [x] Все задачи текущей фазы реализованы.
- [x] Frontend build проходит (`npm run build`).
- [x] PHP lint чистый.
- [x] Тесты не сломаны (13 pre-existing failures).
- [x] Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login.
- [x] Прод деплой только через `git pull`.

### Phase 4 — выполнено
- [x] Все задачи текущей фазы реализованы.
- [x] Frontend build проходит (`npm run build`).
- [x] PHP lint чистый.
- [x] Тесты не сломаны (13 pre-existing failures).
- [x] Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login.
- [x] Прод деплой только через `git pull`.

### Переход к следующим задачам
- [x] Smoke-test Phase 3 на проде подтверждён.
- [x] Все задачи Phase 4 реализованы.
