Newer
Older
smart-home-server / docs / planning / auth-fix-plan.md

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

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


Контекст

После аудита авторизации (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.jsinit() кэшируется через 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.phprefresh() и 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.jssetTimeout на обновление за 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.phpput() добавляет ip_address, user_agent
    • server/SHServ/Integrations/GAuth/AuthControllerTrait.phpresolve_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.phpcheckAuthRateLimit() вызывается в 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 — выполнено

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

Phase 2 — выполнено

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

Phase 3 — выполнено

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

Phase 4 — выполнено

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

Переход к следующим задачам

  • Smoke-test Phase 3 на проде подтверждён.
  • Все задачи Phase 4 реализованы.