Перейти к содержанию

AUTH

Идентификация, аутентификация, авторизация

Идентификация - заявление, кто ты, без проверки: ввод логина, email или другого уникального идентификатора.

Аутентификация - проверка, что ты действительно тот, за кого себя выдаёшь: пароль, биометрия, двухфакторная проверка.

Авторизация - проверка прав после успешной аутентификации. Отвечает на вопрос «что этому пользователю можно делать в системе?»: какие страницы, функции и данные ему доступны.

Note

Не прошёл аутентификацию - 401 Unauthorized. Вошёл, но прав нет - 403 Forbidden.


Способы аутентификации

  • Логин и пароль
  • Email и код - пользователь переходит по ссылке из письма или вводит код из него, почта служит идентификатором
  • SMS-код, OTP (One-Time Password) - одноразовый код
  • Биометрия - отпечаток пальца, лицо
  • Push-уведомление - вход подтверждают на другом устройстве
  • QR-код - его сканируют телефоном, на котором уже выполнен вход
  • USB-ключи и физические токены - YubiKey и другие ключи FIDO2

Двухфакторная аутентификация (2FA)

2FA - вход по двум факторам: к паролю добавляется одноразовый код на телефон или из приложения-аутентификатора.

Кроме входа, 2FA просят на чувствительных операциях: смена пароля, доступ к личным данным.


Хранение паролей

В БД лежит не пароль, а его хэш. При входе сервер хэширует введённый пароль и сравнивает с сохранённым.

  • Не годятся: MD5, SHA-1, SHA-256. Это быстрые хэши: GPU перебирает миллиарды вариантов в секунду, и соль от перебора не спасает.
  • Безопасные: bcrypt, Argon2. Они медленные специально, их стоимость (число раундов, память) настраивается.

Соль - уникальная случайная строка, которую добавляют к каждому паролю перед хэшированием.

  • одинаковые пароли дают разные хэши, заранее посчитанные таблицы (rainbow tables) не помогают
  • хранится рядом с хэшем: bcrypt и Argon2 кладут её прямо в строку хэша
  • формат bcrypt: $2b$12$ + 22 символа соли + 31 символ хэша, где 12 - cost, 2^12 раундов

Перец - общий секрет для всех паролей. Добавляется отдельно и хранится вне БД: в конфиге или хранилище секретов. Утекла одна БД - без перца хэши не перебрать.


Basic Auth


Cookie

Cookie - пара имя=значение, которую сервер ставит заголовком Set-Cookie. Браузер хранит её и сам отправляет в заголовке Cookie с каждым запросом на тот же домен и путь.

HTTP/1.1 200 OK
Set-Cookie: sessionid=abc123xyz; Path=/; HttpOnly; Secure; Max-Age=3600
GET /dashboard HTTP/1.1
Host: example.com
Cookie: sessionid=abc123xyz

Атрибуты:

Атрибут Что делает
Domain Для какого домена отправлять. Без него - только хосту, который поставил cookie, с ним - ещё и поддоменам
Path Для каких путей. Path=/ - для всего сайта
Expires / Max-Age Время жизни. Без них это session cookie: живёт до закрытия браузера
HttpOnly Cookie недоступна из JavaScript, XSS её не прочитает
Secure Отправляется только по HTTPS
SameSite Слать ли cookie с запросов, начатых на другом сайте: Strict - никогда, Lax - только при переходе по ссылке, None - всегда, нужен Secure

Удаление - та же cookie с пустым значением и датой в прошлом или Max-Age=0. Path и Domain должны совпадать с исходными:

Set-Cookie: sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT

Cookie с HttpOnly из JavaScript не удалить, её стирает только сервер.

Диагностика: DevTools -> Application -> Cookies, или curl -i <url> покажет Set-Cookie в ответе.


Сессии (Session Authentication)

После аутентификации сервер должен узнавать пользователя на каждом следующем запросе. Способов два: сессия и токен.

Сессия - состояние входа, которое хранит сервер. Клиенту уходит только session ID в cookie, это stateful-подход.

Жизненный цикл сессии

  1. Создание. При логине сервер генерирует session ID и заводит запись в хранилище сессий. Гостевую сессию, например для корзины, создают уже на первом запросе.
  2. ID уходит клиенту в cookie: Set-Cookie: JSESSIONID=abc123xyz456; Path=/; HttpOnly; Secure; SameSite=Lax.
  3. Каждый запрос. Браузер хранит cookie и сам прикладывает её ко всем запросам на этот сайт.
  4. Проверка. Сервер ищет сессию по ID в хранилище:
    • не нашёл - 401 или редирект на страницу входа
    • нашёл - проверяет, что сессия не истекла, достаёт userId и права и решает, пускать ли к ресурсу
  5. Завершение.
    • По таймауту. Сессию закрывает таймаут на сервере, а не срок cookie: истёкшая cookie только перестаёт отправляться.
    • При выходе. Сервер удаляет сессию и стирает cookie.
    • Программно. Сервер сам закрывает сессию при угрозе безопасности или смене прав.

Session ID

Session ID - уникальный идентификатор сессии, его генерирует сервер. Сам по себе ничего не значит: это ключ к записи в хранилище, где лежат данные пользователя.

  • случайная строка из букв и цифр, например abc123xyz456 или UUID d9f9b76d-315b-487d-a456-8308dfb87f2e
  • берётся из криптостойкого генератора, чтобы ID нельзя было подобрать
  • имя cookie зависит от платформы: JSESSIONID в Java, PHPSESSID в PHP, или своё, например sessionid

Украли ID - получили чужую сессию (session hijacking), см. Защита сессии.


Что хранить в сессии

  • userId - если пользователь вошёл
  • Данные сеанса - время последнего запроса, текущее состояние: корзина, фильтры
  • Права - роли и разрешения, чтобы решать о доступе без похода в БД
  • Служебные данные входа - одноразовые коды, токены других механизмов аутентификации

Где хранить сессии

Где Скорость После перезапуска Несколько серверов
Память процесса Самая быстрая Сессии теряются У каждого сервера свои, балансировщик держит пользователя на одном сервере (sticky sessions)
Redis, Memcached Быстро, данные в RAM Redis восстановит из RDB-снапшота или AOF, если они включены. Memcached теряет всё Общее хранилище
БД, таблица сессий Медленнее Сохраняются Общее хранилище
Файлы на диске, по умолчанию в PHP Медленнее Сохраняются У каждого сервера свои

В БД часто держат только саму сессию: ID, пользователя, время последнего доступа. Данные пользователя лежат в своих таблицах.

Бывает и сессия на клиенте: Flask по умолчанию кладёт данные сессии в подписанную cookie. По сути это тот же подход, что у JWT.


Защита сессии

Украденный session ID - вход в чужой аккаунт. Исключить кражу полностью нельзя, но риск сильно снижают:

  1. HTTPS везде и cookie с Secure и HttpOnly. ID не перехватят по сети (MITM) и не прочитают скриптом (см. Cookie, TLS).
  2. Таймауты и инвалидация. Сессия истекает после простоя, например через 15 минут. При выходе и по истечении ID удаляется на сервере, чтобы его нельзя было использовать повторно.
  3. Новый ID после входа. Сервер выдаёт новый session ID при логине и смене прав. Иначе работает session fixation: атакующий заранее подсовывает жертве свой ID и ждёт, пока она войдёт. Менять ID можно и периодически, но не на каждом запросе: параллельные запросы со старым ID начнут падать.
  4. Привязка к IP и User-Agent. Если они сменились, сессию, возможно, украли или открыли на другом устройстве. IP у мобильных клиентов меняется сам, поэтому такую проверку делают мягкой.
  5. CSRF-токен на изменяющих запросах (см. CSRF).
  6. 2FA на чувствительных операциях (см. 2FA).
  7. Мониторинг. Логи и алерты на подозрительную активность: входы с разных IP за короткое время, серии неудачных попыток входа.
  8. Шифрование данных сессии на сервере. Утечка хранилища не раскроет данные пользователя и его действия.
  9. Обновления. Сервер, фреймворки и библиотеки сессий обновляют, чтобы не ловить известные уязвимости.

Итого: главное - HTTPS, защищённые cookie, таймауты и инвалидация сессии на сервере.


Токены (Token Based Authentication)

Токен - строка, которая подтверждает, что пользователь прошёл аутентификацию. Клиент передаёт её с каждым запросом к защищённым ресурсам.

  • Stateless - все данные внутри токена: ID пользователя, время жизни, подпись от подделки. Сервер ничего не хранит, поэтому такие токены удобны при масштабировании. Так устроен JWT.
  • Stateful (opaque) - токен лишь ссылка на данные, которые сервер хранит в БД или Redis. По сути это тот же session ID, зато доступ легко отозвать: удалил запись.

См. Сессии или JWT и Stateful и Stateless в Spring Security.


JWT (JSON Web Token)

JWT - stateless-токен: подписанный JSON с данными пользователя. Сервер проверяет подпись и верит содержимому, не обращаясь к хранилищу.

Структура

Три части через точку, каждая закодирована в base64url: HEADER.PAYLOAD.SIGNATURE.

  • Header - тип токена и алгоритм подписи: HS256, HS512, RS256
  • Payload - claims, данные о пользователе и сроке: sub, exp, роли
  • Signature - подпись header и payload, без ключа её не подделать
header:               payload:                          signature:
{                     {                                 HMACSHA512(
  "alg": "HS512",       "sub": "1",                       base64url(header) + "." + base64url(payload),
  "typ": "JWT"          "username": "username",           secret
}                       "role": ["admin", "manager"],   )
                        "exp": 1767225600
                      }

Секрета в токене нет. Подпись считается из первых двух частей и секрета, который знает только сервер. Поменял хоть символ в payload - подпись не сойдётся.

Header и payload - это кодирование, а не шифрование: прочитать их может любой, у кого есть токен, а подделать без ключа нельзя. Если содержимое надо скрыть, токен шифруют (JWE).

Стандартные claims: sub - кто, exp - до какого момента действует (Unix time), iat - когда выдан, iss - кто выдал, aud - для кого.

Диагностика - прочитать payload без ключа:

jq -R 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' <<< "$TOKEN"

Подпись: HMAC и RSA

  • HMAC (HS256, HS512) - один общий секрет: им подписывают и им же проверяют. Кто может проверить токен, тот может и выпустить свой. Проверка самая дешёвая.
  • RSA (RS256) - подписывают приватным ключом, он есть только у сервера аутентификации. Проверяют публичным, его можно раздать всем сервисам.

Что класть в токен

  • Положить можно любые claims, но кладут минимум для аутентификации и авторизации: userId, username, роли.
  • Никаких паролей и персональных данных: payload прочитает любой, у кого есть токен.
  • Роли в токене экономят поход в БД, но устаревают: роль сменили, а токен со старой ролью живёт до exp. Если в токене только userId, права приходится читать из БД на каждом запросе, а сессия держит их под рукой.
  • Каждый claim увеличивает токен, а он едет в каждом запросе.

Проверка JWT на сервере

Сервер разбирает и проверяет токен на каждом запросе:

  1. Достаёт токен из заголовка Authorization: Bearer <token> или из cookie.
  2. Проверяет подпись: секретом для HMAC или публичным ключом для RSA.
  3. Проверяет exp: не истёк ли токен.
  4. Берёт из payload userId, роли, scope и решает, можно ли выполнить запрос.
  5. Всё в порядке - обрабатывает запрос. Подпись не сошлась или токен истёк - 401 Unauthorized с описанием ошибки.
GET /protected/resource HTTP/1.1
Host: example.com
Authorization: Bearer <JWT>

Разбор и проверка подписи на каждом запросе - лишняя работа для сервера, но с HMAC она минимальна.


Access и refresh token

Пара токенов: утёкший access живёт недолго, а вход можно отозвать через refresh.

  • Access token - короткоживущий, 15-30 минут. Ходит в каждом запросе к API.
  • Refresh token - долгоживущий, 7-60 дней. Нужен только для получения нового access. Сервер хранит его в БД, поэтому может отозвать.

Где хранить на клиенте

Токен Где Почему
Access В памяти приложения: state, переменная localStorage и sessionStorage читает любой JS на странице, XSS украдёт токен
Refresh Cookie с HttpOnly, Secure, SameSite JS её не прочитает, браузер сам приложит её к запросу на обновление

Часто access кладут в localStorage: так проще, и токен переживает перезагрузку страницы. Цена - токен доступен XSS. Access из памяти теряется при перезагрузке, и клиент просто получает новый через refresh.

Полный цикл

  1. Логин. Клиент отправляет POST /api/login с {email, password}, сервер сверяет их с БД. Если всё верно, генерирует access и refresh, refresh сохраняет в БД. Access уходит в теле ответа, refresh - в cookie: Set-Cookie: refreshToken=...; HttpOnly; Secure.
  2. Запросы к API. Клиент кладёт access в заголовок Authorization: Bearer <access>. Сервер проверяет токен (см. Проверка JWT на сервере): валиден - 200 и тело ответа, нет - 401.
  3. Обновление. Обработчик на 401 вызывает POST /api/refresh, браузер сам прикладывает cookie с refresh. Сервер проверяет refresh: подпись и exp, если это JWT, и что он есть в БД, ведь отозванного там уже нет. Выдаёт новый access и новый refresh: на каждом обновлении (rotation, старый удаляется из БД) или когда старый скоро истечёт. Клиент заменяет access в памяти, cookie с refresh браузер обновит сам.
  4. Выход. Клиент вызывает POST /api/logout, refresh приходит в cookie. Сервер удаляет его из БД и стирает cookie. Access отозвать нельзя, он доживает до exp. Клиент удаляет access из памяти.

Итого: короткий access ограничивает ущерб от утечки, refresh в БД даёт отзыв, и пользователь при этом не вводит пароль каждые 15 минут.


Отзыв и утечка токена

JWT нельзя отозвать до exp: пользователь вышел или у него сменились права, а токен всё ещё валиден. Refresh этого не решает. Старый access он не отменяет, а только позволяет держать access коротким.

Способы отзыва:

  • Короткий access и refresh в БД. Удалил refresh - через 15-30 минут доступ пропадёт сам.
  • Blacklist. Список отозванных токенов (по jti) в Redis, сервер сверяется с ним на каждом запросе. Это возвращает на сервер состояние.
  • Версия токенов. У пользователя в БД счётчик или хэш, в токене - его копия. Сменил значение в БД - все старые токены пользователя недействительны.
  • Opaque-токен с проверкой через introspection (см. Безопасность микросервисов).

Утечка:

  • Украденный access работает до exp, украденный refresh - пока его не отзовут.
  • Refresh скомпрометирован: отозвать все токены пользователя (сменить версию или хэш в БД) и потребовать вход заново.
  • Токены крадут через XSS из localStorage и перехватом без HTTPS (см. Атаки).

Сессии или JWT

Защищают их одинаково: HTTPS, флаги cookie, короткий срок жизни. Разница - где живёт состояние входа: у сессии на сервере, у JWT в самом токене.

Сессия, stateful JWT, stateless
Где состояние На сервере: память, Redis, БД В токене у клиента
Что у клиента Только session ID в cookie Весь токен: в памяти или в cookie
Как сервер проверяет Ищет сессию по ID в хранилище Проверяет подпись и exp, хранилище не нужно
Несколько серверов Нужно общее хранилище сессий, это сложнее Каждый сервер проверяет токен сам
Данные пользователя Не уходят клиенту Payload читает любой, если токен не зашифрован
Сменились данные или роль Сервер обновляет сессию сразу Токен устарел, нужен новый через refresh
Выход и отзыв Удалил сессию - доступа нет Токен живёт до exp, нужен механизм отзыва
Истечение Таймаут на сервере Поле exp в токене
Размер Короткий ID Больше, растёт с каждым claim
Работа на запрос Поиск сессии, данные и права уже в ней Проверка подписи, права из токена или из БД
Память сервера Растёт с числом пользователей Не тратится

Когда что:

  • Сессии - приложение с централизованным хранением и сильным контролем на сервере, где важны мгновенный выход и отзыв доступа
  • JWT - распределённые системы: много серверов и сервисов, каждый проверяет токен сам, без общего хранилища

OAuth

OAuth 2.0 - протокол делегированного доступа: приложение получает от провайдера (Google, GitHub) access token и действует от имени пользователя, не зная его пароля. «Войти через Google» - это OpenID Connect поверх OAuth 2.0 (см. Безопасность микросервисов).

Как подключить вход через провайдера:

  1. Завести проект в консоли разработчика провайдера, у Google это проект в Google Cloud Console. Service account для этого не нужен: он для доступа сервера к API без пользователя.
  2. Создать OAuth-клиента и указать redirect URI: адрес, куда провайдер вернёт пользователя после входа.
  3. Получить Client ID (публичный) и Client Secret (приватный, его показывают один раз при создании).
  4. Добавить тестовых пользователей или опубликовать приложение. Пока приложение в тестовом режиме, войти могут только добавленные пользователи.

Note

Шаги - из видео на YouTube.


SSO (Single Sign-On)

SSO - пользователь один раз проходит аутентификацию и потом получает доступ сразу к нескольким связанным системам без повторного ввода логина и пароля.

Вход выполняет один Identity Provider (Keycloak, Okta, Google), остальные системы доверяют его токенам. Протоколы - OpenID Connect поверх OAuth или SAML.


Модели авторизации

Права доступа после входа описывают одной из моделей:

  • RBAC (Role-Based Access Control) - права привязаны к ролям, роли к пользователю
  • ABAC (Attribute-Based Access Control) - решение по атрибутам пользователя, ресурса и среды
  • ACL (Access Control List) - у каждого ресурса свой список: кто и что с ним может делать

См. Безопасность микросервисов.


Атаки

SQL-инъекции


XSS

XSS (Cross-Site Scripting) - атакующий внедряет свой скрипт в страницу сайта. Скрипт работает с правами страницы и читает всё, что доступно JavaScript: localStorage, sessionStorage, cookie без HttpOnly. Так крадут токены и session ID.

Защита от кражи токенов:

  • session ID и refresh - в cookie с HttpOnly
  • access - в памяти приложения, а не в localStorage

Память - не полная защита: пока страница открыта, скрипт может слать запросы от её имени, но унести токен с собой ему сложнее. Саму XSS закрывают экранированием вывода и заголовком Content-Security-Policy.


CSRF

CSRF (Cross-Site Request Forgery) - чужой сайт заставляет браузер жертвы отправить запрос на твой сайт. Браузер сам прикладывает cookie, и запрос выполняется от имени вошедшего пользователя.

Защита:

  • CSRF-токен - случайное значение в форме или заголовке запроса. Сервер сверяет его перед действием, чужой сайт его не знает.
  • SameSite=Lax или Strict у cookie. Браузер не шлёт её с запросов чужих сайтов, с Lax - кроме перехода по ссылке.
  • Запрос на refresh, если refresh лежит в cookie, тоже защищают CSRF-токеном в заголовке.

Токен в заголовке Authorization от CSRF не страдает: браузер не подставляет его сам.


Brute force