Files
netrunner-backend/src/modules/auth/README.md
T
nineap 591643d364 Чинит .env.example: убирает мёртвый ADMIN_TG_ID, добавляет PROXY_INTERNAL_SECRET
ADMIN_TG_ID нигде не читается с тех пор, как admin_guard заменили на ролевую
систему (owner/staff/node_manage/partner_guard) — оставлять его в примерах
вводит в заблуждение. PROXY_INTERNAL_SECRET, наоборот, обязателен (main.rs
падает без него при старте), но отсутствовал во всех трёх .env*.example.

Заодно поправлен src/modules/auth/README.md — описывал admin_guard,
которого больше не существует.
2026-07-09 15:33:33 +07:00

5.5 KiB
Raw Blame History

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, не здесь) — вызывает тот же 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.