# `modules/auth` — авторизация Четыре способа попасть в систему сходятся в одной точке — `AuthService::issue_session`, которая выдаёт **пару** токенов, а не один JWT: - **access** — JWT (`Claims { sub, tg_id, exp }`, 15 минут), кладётся в cookie `nrxp_token` (`Path=/`). - **refresh** — непрозрачный случайный токен (30 дней), в БД хранится только его SHA-256-хеш (`refresh_sessions`); кладётся в cookie `nrxp_refresh`, строго ограниченную `Path=/api/v1/auth/refresh`, чтобы не улетать на остальные ручки. Обе cookie — `HttpOnly; Secure; SameSite=Lax`, в проде дополнительно `Domain` из `COOKIE_DOMAIN` (`.netrunner-vpn.com`) — одна сессия работает и на лендинге, и на ЛК на сабдомене. Никакой JS-читаемый токен нигде не хранится (ни cookie без `HttpOnly`, ни `localStorage`). `POST /auth/refresh` **ротирует** refresh-токен при каждом использовании: старая запись помечается `revoked_at`, выдаётся новая пара. Предъявление уже отозванного токена (признак кражи/повторного использования после ротации) отзывает **все** сессии юзера. `POST /auth/logout` отзывает текущую сессию и чистит cookie. Способы входа: 1. **Telegram Login Widget** (`POST /auth/telegram`, `controller::auth_telegram_api`). Фронтенд получает от виджета подписанный payload, бэкенд проверяет HMAC-SHA256 подпись ключом `sha256(bot_token)` в **постоянное время** (`mac.verify_slice`, не строковое `!=`) — это защита от timing-атаки на подбор подписи (см. `service::verify_tg_widget_auth`). 2. **Telegram-бот** (`/start [ref_code]`, обрабатывается в [`bot.rs`](../../bot.rs), не здесь) — вызывает тот же `AuthService::process_login`. Кнопки «Личный Кабинет»/«Тарифы»/ «Синдикат» открывают WebApp с короткоживущим (60с) bootstrap-JWT в URL-фрагменте; фронт меняет его на полноценную сессию через `POST /auth/exchange`. 3. **Ghost Protocol** (`POST /auth/seed/generate`, `POST /auth/seed/login`) — анонимный вход без привязки к Telegram. Сервер генерирует случайный 16-символьный seed, хеширует его Argon2 с **фиксированной** солью из `ARGON_SALT` (детерминированно — иначе логин никогда не совпадёт с хешем, сохранённым при регистрации) и хранит только хеш. Seed невосстановим: его теряет — теряет аккаунт. 4. **Bootstrap-обмен** (`POST /auth/exchange`) — принимает короткоживущий JWT из WebApp-ссылки бота, проверяет подпись/`exp` и выдаёт обычную сессию через `issue_session`. Это единственный способ входа, не создающий пользователя заново — он уже существует (создан через Telegram/Ghost). ## Гварды (`guard.rs`) - `auth_guard` — достаёт JWT из `Authorization: Bearer` **или** из cookie `nrxp_token`, валидирует подпись, подтягивает юзера из БД и кладёт его в `req.extensions()` для контроллеров. Для мутирующих запросов (`POST/PUT/PATCH/DELETE`), пришедших **через cookie**, дополнительно проверяет `Origin`/`Referer` против `CSRF_ALLOWED_ORIGINS` — это CSRF-защита. Bearer-запросы её не проходят и не должны: подделать `Authorization`-заголовок с чужого origin браузер не даст, поэтому CSRF для них неактуален. - `owner_guard` / `staff_guard` / `node_manage_guard` / `partner_guard` — ролевые гварды админки (Owner/Moderator/Partner), должны идти **после** `auth_guard` в цепочке слоёв (читают роль юзера из extensions, второй запрос к БД не нужен). Без завязки на Telegram — роль назначается через `staff`-модуль (создание Owner — CLI `--create-owner`, дальше Owner/Moderator создают друг друга через API). `node_manage_guard` — Owner всегда, либо Moderator с выданным правом `can_manage_nodes`. ## Sybil-защита Триал (`billing::activate_trial`) намеренно недоступен Ghost-аккаунтам — иначе генерация нового seed была бы бесплатным и безлимитным способом штамповать VPN-доступ. Подробности инцидента и фикса — в [`SECURITY_AUDIT.md`](../../../SECURITY_AUDIT.md).