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, которого больше не существует.
5.5 KiB
modules/auth — авторизация
Четыре способа попасть в систему сходятся в одной точке — AuthService::issue_session,
которая выдаёт пару токенов, а не один JWT:
- access — JWT (
Claims { sub, tg_id, exp }, 15 минут), кладётся в cookienrxp_token(Path=/). - refresh — непрозрачный случайный токен (30 дней), в БД хранится только его
SHA-256-хеш (
refresh_sessions); кладётся в cookienrxp_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.
Способы входа:
- 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). - Telegram-бот (
/start [ref_code], обрабатывается вbot.rs, не здесь) — вызывает тот жеAuthService::process_login. Кнопки «Личный Кабинет»/«Тарифы»/ «Синдикат» открывают WebApp с короткоживущим (60с) bootstrap-JWT в URL-фрагменте; фронт меняет его на полноценную сессию черезPOST /auth/exchange. - Ghost Protocol (
POST /auth/seed/generate,POST /auth/seed/login) — анонимный вход без привязки к Telegram. Сервер генерирует случайный 16-символьный seed, хеширует его Argon2 с фиксированной солью изARGON_SALT(детерминированно — иначе логин никогда не совпадёт с хешем, сохранённым при регистрации) и хранит только хеш. Seed невосстановим: его теряет — теряет аккаунт. - Bootstrap-обмен (
POST /auth/exchange) — принимает короткоживущий JWT из WebApp-ссылки бота, проверяет подпись/expи выдаёт обычную сессию черезissue_session. Это единственный способ входа, не создающий пользователя заново — он уже существует (создан через Telegram/Ghost).
Гварды (guard.rs)
auth_guard— достаёт JWT изAuthorization: Bearerили из cookienrxp_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.