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

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

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


Контекст

После аудита авторизации (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 ставит Set-Cookie: shserv_session=<session_token> (HttpOnly, Secure, SameSite=Lax), фронтенд хранит только факт аутентификации в памяти (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.
  • 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

  • Проблема: старый/истёкший токен и заблокированный пользователь продолжают иметь доступ.
  • Файлы:
    • 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'].
  • Acceptance criteria:
    • Запрос с истёкшим токеном возвращает 401.
    • Запрос токеном заблокированного (status != 'active') пользователя возвращает 401.

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.
  • Acceptance criteria:
    • Прямой переход на /#/devices не показывает login при валидной сессии.
    • При отсутствии сессии редирект на login происходит ровно один раз.

1.4. Очистка return_to от access_token при 401-редиректе

  • Проблема: цикл OAuth, если return_to содержит старый access_token.
  • Файлы:
    • webclient/src/api/client.js — guard при 401 формирует return_to через URLSearchParams.delete('access_token').
  • Acceptance criteria:
    • 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 по нему в 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.jssetInterval или setTimeout на обновление за 60 секунд до expiration.
  • Acceptance criteria:
    • Токен обновляется до истечения, 401 от API не происходит при нормальной работе.

2.4. Убрать дублирование извлечения токена из URL

  • Проблема: main.js и LoginPage.vue оба пытаются обработать callback → лишний me.
  • Файлы:
    • webclient/src/features/auth/pages/LoginPage.vue — удалить onMounted логику с route.query.access_token.
    • 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.
  • Файлы:
    • /etc/nginx/sites-enabled/default (prod) — limit_req_zone + limit_req для /auth/.
    • Либо PHP middleware для fallback.
  • Acceptance criteria:
    • 10+ запросов /auth/login или /auth/callback в минуту с одного IP → 429.

3.4. Cache permissions

  • Проблема: PermissionResolver делает 3 запроса к БД на каждый API call.
  • Файлы:
    • server/SHServ/Integrations/GAuth/PermissionResolver.php — кешировать результат в $_SESSION на время запроса или с TTL.
  • 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:
    • Один источник правды для секретов на окружении.

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

Перед переходом к следующей фазе проверить:

  • Все задачи текущей фазы реализованы.
  • Юнит/интеграционные тесты (npm test, server/tests/) не сломаны.
  • Ручной smoke-test на локальном сервере: login → dashboard → reload → logout → login.
  • Коммиты отражают фазу (например, Phase-1: cookie-based session for OAuth callback).
  • Прод деплой только через git pull (не ручные правки).