Аутентификация и авторизация — архитектура безопасности
[DRAFT]
Проблема: Как спроектировать систему доступа, которая не сломается под нагрузкой, не пропустит злоумышленника и не заставит пользователей ненавидеть вас за постоянные перелогины?
#Введение
Представьте, что вы проектируете офисное здание. У вас есть:
- входная дверь (кто может войти)
- замки на кабинетах (кто куда может зайти)
- система пропусков (как долго действует доступ)
- охрана (кто следит за подозрительной активностью)
- эвакуационные выходы (что делать, если всё пошло не так)
Аутентификация (authentication, authn) — это проверка «кто вы». Авторизация (authorization, authz) — это «что вам можно делать». Звучит как две простые вещи. На практике это одна из самых недооценённых по сложности областей веб-архитектуры.
Почему? Потому что auth пронизывает всё:
- Каждый API-запрос надо проверить
- Каждый сервис должен знать, кто к нему обращается
- Каждый пользователь должен иметь ровно тот доступ, который нужен для работы
- Система должна работать быстро, даже когда миллион пользователей одновременно логинятся
Я видел проекты, где auth-система падала быстрее, чем бизнес-логика. И проекты, где «простая» JWT-аутентификация превращалась в кошмар безопасности, потому что никто не подумал про revocation.
Эта глава — про то, как не наступить на те же грабли.
#Session-based vs Token-based
Вечный вопрос: хранить сессию на сервере или отдавать токен клиенту? У каждого подхода — своя цена.
#Session-based аутентификация
Классика. Пользователь логинится, сервер создаёт сессию, сохраняет её в Redis/Postgres/Memcached, отдаёт клиенту session ID в cookie. Клиент прикладывает cookie с каждым запросом, сервер ищет сессию в хранилище.
Плюсы:
- Revocation — мгновенный. Удалили сессию из БД — пользователь больше не авторизован.
- Контроль — полный. Вы знаете, сколько активных сессий, когда последний раз пользователь что-то делал, с какого устройства.
- Размер — минимальный. В cookie — только session ID, вся логика на сервере.
Минусы:
- Stateful-сервер. Каждый запрос требует обращения к storage. Это latency, это нагрузка, это ещё одна точка отказа.
- Масштабирование — нужно централизованное хранилище сессий (обычно Redis). Redis упал — все пользователи разлогинились.
- Mobile-клиенты — с cookies не всегда удобно работать. Токами проще.
#Token-based аутентификация (JWT и друзья)
Пользователь логинится, сервер выдаёт подписанный токен (обычно JWT). Клиент хранит токен (localStorage, Secure Cookie, Memory) и прикладывает к каждому запросу. Сервер проверяет подпись — без обращения к БД.
Плюсы:
- Stateless. Серверу не нужно хранить состояние. Каждый запрос самодостаточен.
- Масштабируется горизонтально. Новый инстанс не знает о сессиях — и не надо.
- Mobile-friendly. Токен можно передать в заголовке, в теле запроса, в query — как удобно.
Минусы:
- Revocation — проблема. Токен живёт, пока не истёк. Нельзя «отозвать» токен, который уже на руках у пользователя. Только чёрные списки (blacklist) — но это возвращает нас к stateful-архитектуре.
- Размер. JWT содержит claims — и они могут быть большими. Каждый запрос носит этот вес.
- Security. Если токен украли — злоумышленник может им пользоваться до истечения. Нужны короткие access token + refresh token механизм.
#Когда что выбирать
Короткий ответ: используйте session-based, если вам нужен жёсткий контроль над сессиями (банковские системы, админки, compliance-heavy проекты). Используйте token-based, если вам нужно масштабироваться и вы готовы мириться с нагрузкой на revocation.
Но правда в том, что в современных системах вы почти всегда используете оба подхода одновременно. Session-based для веб-интерфейса, token-based для API и mobile. Или JWT для access token + server-side refresh token.
#Практическая архитектура: гибрид
В реальности вы редко используете что-то в чистом виде. Вот типичная архитектура, которую я видел в дюжине production-систем:
Браузер/Mobile ←session cookie→ Backend ←JWT→ MicroservicesПользователь логинится через браузер — backend создаёт HTTP-only session cookie. Backend-сервис (или API Gateway) преобразует сессию в JWT для внутренних вызовов. Сессия живёт на сервере, JWT — внутри кластера.
Почему это работает:
- Пользователя можно разлогинить мгновенно (удалили сессию из Redis)
- Браузер хранит только session ID (нельзя украсть JWT через XSS)
- Внутренние сервисы получают stateless JWT (никакого Redis на каждый запрос)
- JWT содержит информацию о пользователе (id, роли) — сервисам не нужно дёргать auth-сервис
#Decision Matrix: Session vs JWT
| Критерий | Session-based | JWT-based |
|---|---|---|
| Revocation | Мгновенный | Сложный (blacklist/короткий TTL) |
| Latency на запрос | +1 RTT к storage | 0 (локальная проверка) |
| Stateful storage | Redis/Postgres (+cost, +complexity) | Не нужен |
| Размер токена | Минимальный (session ID) | Растёт с claims |
| Security (XSS) | Secure httpOnly cookie | Зависит от хранения |
| Mobile compatibility | Сложно (cookies) | Естественно (Bearer header) |
| Audit сессий | Полный контроль | Ограниченный |
| Horizontal scaling | Нужен shared storage | Из коробки |
#JWT: структура, подпись, ротация, revocation
JWT (JSON Web Token) — это не «просто строка». Это три base64-закодированные секции, разделённые точками:
header.payload.signatureHeader — метаданные: алгоритм подписи (alg), тип токена (typ).
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-1"
}Payload — claims: утверждения о пользователе и токене. Стандартные (registered) — iss (issuer), sub (subject), exp (expiration), iat (issued at), jti (JWT ID). Публичные (public) — любые кастомные поля. Приватные (private) — между конкретными сторонами.
{
"sub": "user_12345",
"iss": "https://auth.myapp.com",
"iat": 1715000000,
"exp": 1715003600,
"roles": ["admin", "editor"],
"org_id": "org_42"
}Подпись (signature) — самое важное. Это HMAC (симметричная подпись) или RSA/ECDSA (асимметричная). Подпись гарантирует, что токен не был подменён.
#Алгоритмы подписи
- HS256 (HMAC + SHA256) — один секрет для подписи и верификации. Просто, быстро. Но если секрет украли — всё пропало. Не подходит для распределённых систем.
- RS256 (RSA + SHA256) — пара ключей: приватный для подписи (только у issuer), публичный для верификации (у всех сервисов). Безопаснее, но медленнее.
- ES256 (ECDSA + SHA256) — как RS256, но на эллиптических кривых. Те же гарантии, меньший размер ключа, быстрее.
Практический совет: Всегда используйте асимметричную подпись (RS256 или ES256), если у вас больше одного сервиса. Только issuer знает приватный ключ, все остальные сервисы верифицируют публичным. Это изолирует поверхность атаки.
#Проклятие JWT: revocation
Вот главная проблема JWT: токен нельзя отозвать.
Ситуация: вы уволили сотрудника, но у него есть JWT, который действителен ещё 30 минут. Что делать?
Варианты решения (все — компромисс):
-
Короткие access token + refresh token — access token живёт 5-15 минут, refresh token — дольше (часы/дни). Если надо отозвать доступ — удаляете refresh token на сервере. Access token истечёт сам через 15 минут. На практике — работает хорошо. Проблема — refresh token снова требует stateful-хранилища.
-
Чёрные списки (blacklist) — храните ID отозванных токенов (jti claim) в Redis до их истечения. Каждый запрос проверяет: «этот jti не в чёрном списке?». Минус — каждый запрос снова требует обращения к Redis. Вы вернулись к сессиям, но с бóльшими токенами.
-
Graceful degradation — принимаем, что в худшем случае доступ может быть продлён на время жизни токена. Для многих систем — приемлемо. Для финансовых — нет.
-
Token rotation + family — каждый раз при использовании refresh token вы выдаёте новый access + refresh, а старый refresh token инвалидируете. Если кто-то попытается использовать старый refresh token — вы знаете, что токен украден, и можете отозвать всю семью.
#Пример архитектуры с JWT + refresh tokens
Клиент Auth Service API Gateway / Сервисы
│ │ │
│──── POST /auth/login ───────▶│ │
│◀── { access_token (15m), │ │
│ refresh_token (7d) } │ │
│ │ │
│──── GET /api/orders ──────────────────────────────────────▶│
│ Authorization: Bearer <access_token> │
│ │ Проверка подписи │
│◀──────────────────────────────────── { orders: [...] } ───│
│ │ │
│──── POST /auth/refresh ────▶│ │
│ { refresh_token } │ Проверка: refresh в БД? │
│ │ Не отозван? │
│◀── { access_token (15m), │ Генерируем новый │
│ refresh_token (7d) } │ │Важно: Refresh token должен быть одноразовым (rotation). Если кто-то украл refresh token — после его использования оригинальный клиент получит ошибку и узнает о компрометации.
#OAuth 2.1 и OpenID Connect
OAuth 2.1 — это не новая версия, а уточнение OAuth 2.0. Он запрещает некоторые опасные практики из RFC, которые раньше были разрешены (Implicit Grant, Resource Owner Password Credentials Grant). Если вы проектируете auth сегодня — используйте OAuth 2.1 как базовый стандарт.
#Зачем это вообще нужно?
Представьте: пользователь хочет дать приложению «Прочитать мои контакты» доступ к своей адресной книге в Google. Без OAuth ему пришлось бы отдать свой пароль этому приложению. С OAuth — он даёт ограниченный токен с конкретными правами (scopes) без раскрытия пароля.
OAuth — это про делегирование доступа (delegation). Не про аутентификацию.
#OpenID Connect (OIDC)
OIDC — это слой поверх OAuth, который добавляет аутентификацию. OAuth сам по себе не знает, «кто пользователь». OIDC добавляет ID token (JWT с профилем пользователя) и UserInfo endpoint.
Flow на практике (Authorization Code + PKCE):
1. User нажимает «Войти через Google» в вашем приложении
2. Браузер редиректит на auth.google.com с параметрами:
- client_id = ваш ID приложения
- redirect_uri = куда вернуть пользователя
- response_type = code
- scope = openid profile email
- code_challenge = SHA256(code_verifier) ← PKCE
3. Google спрашивает пользователя: «Разрешаете?»
4. Google редиректит обратно на redirect_uri с временным code
5. Ваш сервер отправляет code + code_verifier на Google token endpoint
6. Google возвращает: access_token + id_token (JWT) + refresh_token
7. Ваш сервер верифицирует id_token — теперь вы знаете, кто пользовательПочему важно: Шаг 5 выполняется на сервере, а не в браузере. Никто не перехватит code. А PKCE (Proof Key for Code Exchange) защищает от атаки, когда злоумышленник перехватывает code до того, как сервер его обменяет.
#Scopes и Consent
Scopes — это то, что разрешает приложение. В OAuth 2.1 конфигурация scopes становится центральной частью архитектуры:
- Минимальные права (least privilege) — запрашиваете только то, что нужно
- Consequent consent — пользователь видит, что вы запрашиваете, и может отказать
- Scopes как границы между сервисами — на уровне gateway вы проверяете scope, а не роль
#SSO: SAML, CAS — и почему это сложно
SSO (Single Sign-On) — это когда пользователь входит один раз и получает доступ ко всем сервисам организации. Звучит прекрасно. На практике — ад интеграции.
#SAML 2.0
SAML (Security Assertion Markup Language) — старый, но живучий стандарт для enterprise SSO. Использует XML (что уже диагноз). Работает через редиректы браузера.
Как работает:
- Пользователь заходит в Service Provider (SP) — например, в Salesforce
- SP не видит сессии, редиректит на Identity Provider (IdP) — например, Okta
- Пользователь логинится на IdP (если ещё не залогинен)
- IdP генерирует SAML Assertion (XML с подписью) и отправляет SP через браузер
- SP верифицирует подпись XML — пользователь авторизован
Проблемы SAML:
- XML — тяжёлый, сложный для парсинга, подвержен XML signature wrapping атакам
- SP-initiated flow (когда пользователь заходит в сервис первым) сложен
- IdP может упасть — и тогда никто никуда не зайдёт
- Настройка — копирование XML-метаданных между системами
#CAS (Central Authentication Service)
CAS — протокол, разработанный в Yale. Проще SAML, но менее гибкий. Используется в университетах и legacy-системах.
Основная идея: центральный сервер проверяет билет (ticket), который выдаётся после успешной аутентификации.
#Современная альтернатива: OIDC + SSO
Сегодня OIDC вытесняет SAML и CAS. Google, Microsoft, Okta, Auth0 — все поддерживают OIDC как основной протокол.
Архитектура SSO на OIDC:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Service C │
│ (App A) │ │ (App B) │ │ (App C) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌──────▼──────┐
│ IdP │
│ (Auth0, │
│ Keycloak, │
│ Azure AD) │
└─────────────┘User session хранится на IdP. Каждый сервис использует OIDC — редиректит на IdP, получает ID token, создаёт локальную сессию. Если пользователь уже залогинен на IdP — редиректа нет, сразу получает токен.
#Почему SSO — это сложно
- Единая точка отказа. IdP упал — никто никуда не заходит. Нужна избыточность, кэширование, grace period для токенов.
- Logout. Single Logout (SLO) — это кошмар. Надо разлогинить пользователя на всех сервисах. SAML SLO — бомба замедленного действия. OIDC SLO — чуть проще, но всё ещё нетривиально.
- Session management. Если пользователь меняет пароль — надо завершить все сессии на всех сервисах.
- Latency. Каждый редирект на IdP — это дополнительный round trip. Если IdP в другом регионе — время входа растёт.
- Отладка. Попробуйте отладить цепочку редиректов через 3 сервиса и 2 IdP. Спойлер: это ад.
#MFA и 2FA — архитектура второго фактора
Пароль — это что-то, что вы знаете. Второй фактор (2FA) — это что-то, что вы имеете (телефон, TOTP-код) или что-то, чем вы являетесь (биометрия). Multi-Factor Authentication (MFA) — это требование двух или более факторов.
Архитектурные подходы к MFA:
TOTP (Time-based One-Time Password) Пользователь сканирует QR-код в приложении-аутентификаторе (Google Authenticator, Authy, 1Password). Приложение генерирует 6-значный код, меняющийся каждые 30 секунд.
# Упрощённая проверка TOTP
import pyotp
totp = pyotp.TOTP(secret) # секрет хранится на сервере (зашифрован)
if totp.verify(code, valid_window=1):
# код корректен (с допуском ±30 секунд из-за clock skew)Архитектурно важно:
- Секрет хранится зашифрованным в БД. Утечка БД — утечка всех TOTP-ключей
- Clock skew — часы на сервере и телефоне могут расходиться.
valid_window=1даёт допуск - Backup codes — пользователь должен иметь одноразовые коды для восстановления доступа
SMS/Email OTP Надежность ниже, чем TOTP (SMS могут перехватывать, email — компрометироваться). NIST больше не рекомендует SMS как второй фактор.
- Проблема: операторская задержка, спам-фильтры, cost для компании
- Если используете — ограничьте частоту отправки (rate limit на отправку — 3 SMS в час на номер)
WebAuthn / Passkeys Современный стандарт (FIDO2/WebAuthn). Пользователь регистрирует устройство (или passkey), и аутентификация происходит через криптографическую подпись.
Браузер ←→ Сервер
│ │
│── challenge ─▶│ (случайная строка)
│◀── challenge ─│
│ │
│── signature ──▶│ (подписано приватным ключом устройства)
│ │
│◀── verified ──│ (проверка публичным ключом на сервере)Архитектура хранения публичных ключей:
CREATE TABLE webauthn_credentials (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
public_key TEXT NOT NULL, -- PEM-encoded public key
credential_id TEXT NOT NULL UNIQUE, -- ID от устройства
device_name TEXT, -- "iPhone 15 Pro"
sign_count BIGINT DEFAULT 0, -- защита от cloned key
created_at TIMESTAMP DEFAULT NOW()
);WebAuthn — самый безопасный второй фактор. Никто не может украсть приватный ключ с сервера, потому что его там нет. Passkeys добавляют синхронизацию между устройствами (iCloud Keychain, Google Password Manager).
Step-up authentication Вместо того чтобы требовать MFA при каждом входе, запрашивайте его только для чувствительных операций:
- Смена пароля — TOTP
- Перевод денег — TOTP
- Просмотр данных — достаточно пароля
- Удаление аккаунта — TOTP + email confirmation
Архитектурно: step-up authentication требует, чтобы auth-сервис умел различать «сессию с MFA» и «сессию без MFA». В JWT — добавляете claim amr (Authentication Methods Reference). На уровне middleware проверяете: если endpoint требует MFA — проверяйте, есть ли mfa в amr.
#RBAC vs ABAC vs ReBAC — модели авторизации
Аутентификация ответила на вопрос «кто вы?». Авторизация отвечает на вопрос «что вам можно делать?». И вот тут начинается самое интересное.
#RBAC (Role-Based Access Control)
Самая распространённая модель. Пользователь имеет роли, роли имеют разрешения. Всё.
User ──► Role ──► PermissionПример:
user_alice ──► role_admin ──► permission:delete_user, permission:edit_settings
user_bob ──► role_editor ──► permission:create_post, permission:edit_postКогда работает: Большинство бизнес-приложений. Если ваша модель доступа укладывается в «админ, редактор, пользователь» — RBAC достаточно.
Когда не работает:
- Иерархические структуры (организация → отдел → команда → проект)
- Контекстно-зависимые разрешения («редактор может редактировать только свои статьи, если статус черновик»)
- Многомерные политики (регион, время, отдел, проект)
#ABAC (Attribute-Based Access Control)
ABAC — это RBAC на стероидах. Вместо ролей — политики, которые проверяют атрибуты: субъекта (должность, отдел), объекта (тип документа, владелец), окружения (время, IP, устройство).
Пример политики:
GRANT access TO document
WHERE user.department = document.department
AND user.clearance >= document.classification
AND time.now BETWEEN 09:00 AND 18:00
AND device.type IN ['corporate_laptop', 'corporate_phone']Плюсы:
- Гибкость — можно выразить почти любую политику
- Масштабируется на сложные организации
Минусы:
- Сложность — политики тяжело отлаживать
- Performance — каждый запрос может требовать вычисления сложных условий
- Audit — понять, почему пользователю что-то разрешено/запрещено, может быть сложно
Когда использовать: Enterprise-системы с complicated access policies, регуляторные требования (HIPAA, SOX), multi-tenant системы с разными политиками для разных клиентов.
#ReBAC (Relationship-Based Access Control)
ReBAC — это модный подход (спасибо Google Zanzibar). Разрешения вычисляются на основе отношений между сущностями.
Модель: объект — отношение — субъект.
document:42#viewer@user:alice
document:42#owner@group:engineering
group:engineering#member@user:bob
→ bob может просматривать document:42, потому что он member group:engineering, которая owner document:42Где используется:
- Google Drive (файлы, папки, общие диски)
- GitHub (репозиторий — команда — пользователь)
- Figma (файлы — проекты — команды)
Плюсы:
- Идеально для иерархических и сетевых структур доступа
- Масштабируется до миллиардов объектов (Zanzibar — proof)
- Разделение графа отношений и бизнес-логики
Минусы:
- Overkill для простых систем
- Требует инфраструктуры (Zanzibar не так просто поднять)
- Не все политики можно выразить через отношения
#Decision Matrix: RBAC vs ABAC vs ReBAC
| Критерий | RBAC | ABAC | ReBAC |
|---|---|---|---|
| Простота реализации | Высокая | Низкая | Средняя |
| Гибкость политик | Низкая | Высокая | Высокая (для иерархий) |
| Performance | Высокий | Средний-Низкий | Высокий (с кэшированием) |
| Auditability | Простая | Сложная | Средняя |
| Multi-tenant | Сложно | Хорошо | Хорошо |
| Иерархический доступ | Нет | Да | Да (из коробки) |
| Инфраструктурные затраты | Низкие | Средние | Высокие |
Когда что выбирать:
- RBAC — стартап, B2B SaaS с простыми ролями, внутренние админки
- ABAC — enterprise, регулируемые индустрии, сложные политики доступа
- ReBAC — collaborative-инструменты, content sharing, документооборот
#Практический пример: как ломается RBAC
Представьте систему документооборота с RBAC. Есть роли: Admin, Manager, Employee.
Employee может создавать документы. Manager может утверждать. Admin — всё.
Простая модель — да. А теперь:
- Employee из отдела A не должен видеть документы отдела B
- Manager может утверждать документы, только если сумма меньше $10,000
- Внешний контрактор может редактировать документ, но только если его пригласили
- Внутренний аудитор может читать любые документы, но не создавать и не редактировать
С RBAC вам придётся создать роли: Employee_DeptA, Employee_DeptB, Manager_DeptA_Under10K, Manager_DeptA_Over10K, Manager_DeptB, External_Collaborator, Readonly_Auditor. И это только начало. Через полгода количество ролей взрывается, и понять, что какая роль делает — невозможно. Это называется role explosion — классическая проблема RBAC в сложных системах.
ABAC решает это политиками, а RBAC — бесконечным размножением ролей.
#Практический пример: ReBAC на Google Drive
Google Drive использует ReBAC. Файл имеет владельца. Владелец может дать доступ:
- Конкретному пользователю (user:alice#viewer@file:doc1)
- Группе (group:engineering#editor@file:doc1)
- Домейну (domain:example.com#commenter@file:doc1)
- Всем (public#viewer@file:doc1)
Папка наследует права на вложенные файлы. Пользователь может быть в нескольких группах с разными правами.
Если вы пишете collaborative-инструмент (типа Notion, Figma, Miro) — ваша модель доступа почти наверняка ReBAC, даже если вы называете это RBAC.
#API keys vs PAT vs M2M — для сервис-сервис коммуникации
Люди подключаются через браузер. Сервисы — через API. И для сервисов нужна своя аутентификация.
#API keys
Самый простой способ: сгенерировали ключ — отдали сервису. Сервис прикладывает его в каждом запросе.
GET /api/v1/data HTTP/1.1
X-API-Key: sk_live_3f8a2b1c...Плюсы: Простота. Сервер проверяет key против хеша в БД. Минусы: Кто использует ключ — непонятно. Ключ нельзя отозвать частично. Секрет в каждом запросе — перехватили запрос, получили ключ.
Где использовать: Публичные API с низкими требованиями безопасности (погода, публичные данные), запросы из надёжной сети (VPC).
#PAT (Personal Access Token)
PAT — это API key, привязанный к конкретному пользователю. GitHub, GitLab, npm — все используют PAT.
Формат: ghp_XXXXXXXXXXXX
Привязан к: пользователю
Scopes: repo, admin:org, delete_repo (выбираются при создании)Отличие от API key: PAT можно отозвать персонально, его можно привязать к конкретной операции. Если сотрудник ушёл — отзываете его PAT, остальные работают.
Best practices для PAT:
- Всегда ограничивать scopes (минимальные права)
- Устанавливать expiration (если поддерживается)
- Не хранить в коде
- Использовать для CI/CD отдельные токены (machine user)
#M2M (Machine-to-Machine) — Client Credentials
Когда два сервиса общаются без участия пользователя — используется OAuth 2.0 Client Credentials Grant.
Service A Auth Service Service B
│ │ │
│──── POST /oauth/token ────▶│ │
│ client_id=svc_a │ │
│ client_secret=xxx │ │
│ grant_type= │ │
│ client_credentials │ │
│ │ │
│◀── { access_token: jwt, │ │
│ expires_in: 3600 }│ │
│ │ │
│──── GET /api/b/resource ──────────────────────────────▶│
│ Authorization: Bearer <jwt> │
│ │ │
│◀────────────────────────── { result } ───────────────│JWT содержит claim client_id вместо sub (нет пользователя). Можно добавить кастомные claims: service_role, permissions.
Когда использовать: Микросервисная архитектура, server-to-server интеграции, webhooks, background jobs.
#Best practices для M2M:
- Используйте асимметричную подпись — каждый сервис может проверить токен без обращения к auth
- mTLS (mutual TLS) — для критических внутренних коммуникаций (service mesh, Istio/Linkerd делают это прозрачно)
- Короткие токены (15-60 минут) + автоматический refresh
- Каждый сервис — отдельный client_id с минимальными scopes
#Rate limiting, throttling, quota management
Аутентификация — это дорого. Проверка пароля (bcrypt/argon2), генерация токена, запрос к IdP — всё это занимает ресурсы. Если не ограничивать нагрузку — злоумышленник может положить auth-систему обычным перебором.
#Rate limiting
Ограничение по количеству запросов за единицу времени.
Где ставить:
- На уровне Gateway (все запросы)
- На уровне Auth Service (только логин)
- На уровне API-сервисов (работа с ресурсами)
Алгоритмы: | Алгоритм | Как работает | Плюсы | Минусы | |----------|-------------|-------|--------| | Token Bucket | Токены добавляются с фиксированной скоростью | Сглаживает burst | Нужна настройка размера корзины | | Leaky Bucket | Запросы обрабатываются с фиксированной скоростью | Предсказуемая нагрузка | Отбрасывает burst | | Fixed Window | Сброс счётчика каждую минуту | Простота | Проблема с burst на границе окна | | Sliding Window | Скользящее окно с весами | Точность | Сложнее в реализации |
Практический совет: Используйте Token Bucket для API Gateway и Fixed Window для auth endpoint. Token Bucket хорошо сглаживает — пользователь может сделать burst, не превышая среднюю скорость.
#Что лимитировать при auth
- Login attempts — 5 попыток в минуту на IP. Заблокировать на 15 минут после превышения.
- Password reset — 3 попытки в час на email.
- Token refresh — 10 раз в минуту на пользователя (защита от refresh token rotation abuse).
- Registration — 1 аккаунт в час на IP (защита от создания ботов).
- API calls — разумно в зависимости от плана.
#Throttling vs Quota
- Rate limiting (throttling) — временное ограничение. «Не больше 100 запросов в секунду». Превысил — 429 Too Many Requests, через секунду снова можно.
- Quota — долгосрочное ограничение. «Не больше 10 000 запросов в день». Превысил — 429 до конца дня или до upgrade плана.
#Ответ на превышение
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715007600Важно: Не показывайте злоумышленнику, сколько попыток осталось до валидного логина. На auth endpoint — всегда отвечайте 401, не показывайте разницу между «пользователь не найден» и «неверный пароль».
#Security architecture: least privilege, defense in depth, zero trust
Это не про библиотеки и протоколы. Это про принципы, по которым вы строите систему.
#Least Privilege (минимальные привилегии)
Каждый субъект (пользователь, сервис, процесс) должен иметь ровно те права, которые нужны для выполнения его задачи. И ни одним больше.
На практике:
- Сервис A, которому нужно читать очередь Redis — не должен иметь права на запись в эту очередь
- CI/CD токен для деплоя stage — не должен иметь доступ к production
- Пользователь может удалять документы, только если он admin — даже если он создал эти документы
Почему это важно: Если злоумышленник получит доступ к учётке с минимальными правами — ущерб минимален.
#Defense in Depth (эшелонированная защита)
Никогда не полагайтесь на один уровень защиты. Каждый уровень должен быть независимым и перекрывать слабые места других уровней.
┌────────────────────────────────────────────────────┐
│ Network │
│ ┌────────────────────────────────────────────┐ │
│ │ Transport (mTLS, HTTPS) │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Application (OAuth, JWT) │ │ │
│ │ │ ┌────────────────────────────────┐ │ │ │
│ │ │ │ Service Mesh (mTLS, RBAC) │ │ │ │
│ │ │ │ ┌──────────────────────────┐ │ │ │ │
│ │ │ │ │ Data (encryption at │ │ │ │ │
│ │ │ │ │ rest, column-level) │ │ │ │ │
│ │ │ │ └──────────────────────────┘ │ │ │ │
│ │ │ └────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘Пример: Утечка JWT — плохо. Но если у вас:
- HTTPS (не перехватят трафик)
- Короткий access token (15 минут — потом бесполезен)
- Refresh token rotation (узнают о краже)
- Device fingerprinting (подозрительный логин — дополнительный фактор)
- Rate limiting (не смогут перебрать)
...то даже утечка одного токена не приведёт к катастрофе.
#Zero Trust
«Никогда не доверяй, всегда проверяй». В архитектуре zero trust нет «внутренней сети» и «внешней сети». Каждый запрос — независимо от источника — должен быть аутентифицирован, авторизован и зашифрован.
Принципы Zero Trust для auth:
- Все запросы аутентифицируются — даже внутри VPC, даже между сервисами в одном Kubernetes namespace
- Каждый сервис решает авторизацию самостоятельно — не полагается на gateway
- Непрерывная верификация — не только на входе. Подозрительное поведение во время сессии — дополнительный вызов auth
- Микросегментация — каждый сервис имеет изолированную сеть, доступ — только по необходимости
Zero Trust — это не продукт, который вы покупаете. Это подход к архитектуре. Istio/Linkerd (service mesh) реализуют zero trust на уровне сети. OAuth + OIDC — на уровне приложения.
#Token Introspection и JWKS endpoint
Разберём, как сервисы на практике проверяют токены, не обращаясь к auth-сервису на каждый запрос.
JWKS (JSON Web Key Set) — endpoint, который отдаёт публичные ключи для верификации подписи JWT.
GET /.well-known/jwks.json
Response:
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-1",
"use": "sig",
"n": "0vx7ago...",
"e": "AQAB"
}
]
}Архитектурный паттерн:
- Auth-сервис публикует JWKS endpoint
- Все сервисы кэшируют JWKS (TTL — обычно 24 часа)
- Каждый сервис получает JWT, находит ключ по
kidв header, верифицирует подпись - Если подпись валидна — токен корректен, пароль/сессию не проверяем
Проблема: Что делать, если приватный ключ скомпрометирован? Надо:
- Сгенерировать новую пару ключей
- Добавить новый ключ в JWKS (с новым
kid) - Удалить старый ключ (но не сразу — дать время всем сервисам обновить кэш)
- Все токены, подписанные старым ключом, станут невалидны
На практике ротация ключей — редкая операция, но когда она нужна — она должна быть отработана. У Netflix есть автоматическая ротация ключей каждые N дней. У большинства — ручная процедура.
Token Introspection (RFC 7662) — альтернатива JWKS для случаев, когда простой проверки подписи недостаточно:
POST /introspect HTTP/1.1
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Response:
{
"active": true,
"sub": "user_12345",
"scope": "read write",
"client_id": "my-client",
"token_type": "access_token",
"exp": 1715003600
}Introspection возвращает не просто «токен валиден/не валиден», а полную информацию о токене. Используется, когда:
- Токен — opaque (не JWT, а просто случайная строка)
- Нужно проверить статус revocation в реальном времени
- Нужна дополнительная информация, которой нет в JWT claims
Практический совет: Используйте JWKS для большинства случаев. Introspection — только когда вам действительно нужно real-time revocation. Introspection на каждый запрос убивает преимущество stateless-токенов.
#Когда это применимо
Пройдёмся по сценариям и посмотрим, какая комбинация подходов работает в каждом.
#Стартап / MVP (1-10 разработчиков)
Что у вас есть: Один backend (монолит), один фронтенд, 100-1000 пользователей. Что вам нужно: Просто, безопасно, не отвлекаться на инфраструктуру.
Рекомендация: Используйте session-based auth + готовый auth-провайдер (Supabase Auth, Clerk, Firebase Auth). Session-based — потому что revocation делается тривиально, а масштабирование пока не проблема. Готовый провайдер — чтобы не писать свою систему регистрации, сброса пароля, email confirmation.
Frontend ←session cookie→ Monolith ←SQL→ Database
│
(session в той же БД)Если хотите JWT — берите короткий access token (15 минут) + refresh token в httpOnly cookie. Сделайте middleware, которое автоматически обновляет токен.
И главное: ничего не пишите сами. Auth-libraries (NextAuth.js, Lucia, passport.js) — да. Своя реализация регистрации с нуля — нет.
#Growing SaaS (10-50 разработчиков)
Что у вас есть: Несколько сервисов, мобильное приложение, B2B-клиенты, возможно multi-tenant.
Что вам нужно: SSO для enterprise-клиентов, API для интеграций, разделение доступа по ролям.
Рекомендация: Внедрите готовый IdP (Auth0, Keycloak, Cognito). Переведите все сервисы на OIDC. JWT access token + refresh token. RBAC для базовых ролей.
Mobile App ──┐
Browser ────┤──► Auth0/Keycloak ──► JWT ──► API Gateway ──► Services
CI/CD ────┘ │
(verification via JWKS)Архитектура ролей: Храните роли в JWT claims. Не больше 3-5 ролей. Если нужно больше — переходите на ABAC.
#Enterprise / High-scale (50+ разработчиков)
Что у вас есть: Множество сервисов, команды, тысячи пользователей, строгие требования безопасности, multi-region.
Что вам нужно: Zero trust, детальная авторизация, audit, compliance, федерация с существующими IdP клиентов.
Рекомендация: Self-hosted Keycloak (или managed — если бюджет позволяет). ABAC или ReBAC для авторизации. mTLS между сервисами. Token exchange для сервис-сервис коммуникаций. Step-up MFA для чувствительных операций.
Client ──► Gateway (OAuth proxy) ──► Service Mesh (mTLS) ──► Services
│ │
│ (авторизация: │ (авторизация:
│ JWT + ABAC) │ in-app проверки)
│ │
└─────────────────── IdP ──────────────────────────┘
│
(Keycloak/Azure AD)Каждый сервис сам проверяет права — не полагается на gateway. Gateway — только первый рубеж защиты, а не единственный.
#Кейс: Архитектура Auth0 / Keycloak
#Что внутри популярных auth-систем?
Auth0 и Keycloak — два разных подхода к одной проблеме. Auth0 — SaaS, Keycloak — self-hosted. Но архитектурные принципы у них схожи.
#Auth0
Auth0 — это OAuth 2.0 / OIDC провайдер как сервис. Его архитектура построена для insane масштаба: Amazon Cognito обслуживает сотни миллионов пользователей, Auth0 — тысячи enterprise-клиентов.
Внутренняя архитектура (упрощённо):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tenant │ │ Tenant │ │ Tenant │
│ Config │ │ Config │ │ Config │
│ (Redis) │ │ (Redis) │ │ (Redis) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌─────▼───────────────▼───────────────▼─────┐
│ Auth Pipeline │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Rules │ │ Hooks │ │ Actions │ │
│ │(Node.js)│ │(Node.js)│ │(Node.js)│ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ (Кастомизация auth flow — Webtask) │
└─────────────────┬────────────────────────┘
│
┌────────────┴────────────┐
│ │
┌────▼─────┐ ┌──────▼──────┐
│ Token │ │ Connections │
│ Service │ │ (DB, LDAP, │
│ (JWT, │ │ Social, SAML│
│ signing) │ │ OIDC) │
└──────────┘ └─────────────┘Ключевые архитектурные решения Auth0:
-
Multi-tenant изоляция. Каждый tenant — отдельное пространство конфигурации. Но shared инфраструктура для масштабирования. Изоляция на уровне данных, не на уровне сервисов.
-
Auth Pipeline с изоляцией. Правила (Rules / Actions) — это пользовательский код, который выполняется при логине. Auth0 запускает его в изолированном sandbox (Webtask — их собственная serverless-платформа). Если кастомное правило упало — не падает весь auth flow, только для этого tenant.
-
Token Service — stateless. JWT подписываются асимметрично. Каждый сервис может проверить токен без обращения к Auth0 (через JWKS endpoint). Это ключевое для производительности.
-
Connections — плагинная архитектура. Базы данных, социальные логины (Google, Facebook), enterprise-IdP (SAML, OIDC, LDAP) — каждый реализован как плагин. Добавление нового connection не требует изменения auth pipeline.
-
Graceful degradation. Если Redis упал — Auth0 продолжает отвечать на запросы с закэшированными конфигурациями tenant. Если token service упал — уже выданные токены продолжают работать (проверка через JWKS, не через token service).
#Keycloak
Keycloak — open-source, self-hosted, Java (WildFly / Quarkus). Подход — дать enterprise функциональность без vendor lock-in.
Ключевые архитектурные решения Keycloak:
-
Infrastructure sessions. Keycloak использует инфраструктурные сессии — не user session для UI, а session для отслеживания auth flow. Каждый login request — это сессия, которая может быть восстановлена при падении ноды.
-
User Federation. Keycloak не только хранит пользователей сам, но и умеет подключаться к внешним хранилищам (LDAP, Active Directory) через federation providers. Пользователь «живёт» в LDAP, а Keycloak даёт к нему OIDC-доступ.
-
SPI (Service Provider Interface). Keycloak — это не продукт, а платформа. Почти всё можно переопределить через SPI: аутентификацию, авторизацию, синхронизацию пользователей, email-уведомления. И это одновременно плюс (гибкость) и минус (можно прострелить ногу).
-
Events. Keycloak генерирует события на каждый auth action: login, logout, token refresh, consent, error. Это основа для audit log, аномалий-детекции, интеграции с SIEM.
-
Admin console vs Account console. Разделение административных интерфейсов. Admin — для настройки realm, клиентов, ролей. Account — для пользователей (изменить пароль, настроить MFA, посмотреть активные сессии).
#Что мы можем извлечь
Не нужно писать свой auth-сервер (спойлер: это седьмое «Не делай так»). Но понимание архитектуры под капотом помогает:
- Проектировать интеграцию с auth-провайдером, учитывая его слабые места
- Выбирать между managed (Auth0) и self-hosted (Keycloak) — это trade-off между operational cost и контролем
- Строить свои сервисы схожим образом — плагинная архитектура, изоляция tenant, stateless-токены
#Чек-лист для самопроверки
- Аутентификация и авторизация — разные вещи. У вас раздельные системы?
- Токены подписываются асимметрично (RS256/ES256), а не одним секретом?
- Для OAuth — используете Authorization Code + PKCE, а не Implicit Grant?
- Access token живёт не больше 15 минут?
- Refresh token — одноразовый, с rotation?
- Revocation — продумана? Что делать при увольнении сотрудника?
- Rate limiting — на всех auth endpoint?
- Пароли хранятся с bcrypt/argon2 (не MD5, не SHA, не в plaintext)?
- HTTPS — везде, включая внутренние сервисы?
- Модель авторизации — RBAC/ABAC/ReBAC — выбрана осознанно под контекст?
- API keys — не используются для пользовательской аутентификации?
- M2M коммуникации — отдельные токены, а не общие секреты?
- Audit log — каждое значимое действие логируется?
- Secrets — не в коде. Используется vault/secret manager?
- Zero trust — хотя бы на уровне «каждый запрос проверяется, независимо от источника»?
#«Не делай так» — анти-паттерны
#1. JWT как session token без revocation
Самая распространённая ошибка. «Поставили JWT, всё работает, мы stateless, мы крутые». Через месяц: «Как отозвать доступ сотруднику, которого мы уволили?».
Почему так делают: Потому что в туториалах JWT выглядит как простое решение. И оно простое — пока не понадобится отозвать токен.
Как надо: JWT как access token — нормально. Но только в паре с refresh token, который хранится на сервере и может быть отозван.
#2. Собственный OAuth-сервер
«Напишем свой OAuth-сервер. Ну что там сложного? Редкий код токенов, пара endpoint'ов».
Я видел шесть разных реализаций OAuth-серверов своими руками. Шесть. Ни одна не была безопасной. Ни одна не поддерживала все необходимые flow. Каждая стоила компании месяцев разработки и лет поддержки.
OAuth 2.0 (RFC 6749) — это 57 страниц спецификации. Плюс:
- OAuth 2.1 (дополнение)
- OIDC Core (30 страниц)
- OIDC Discovery
- JWKS (RFC 7517)
- Token Revocation (RFC 7009)
- Token Introspection (RFC 7662)
- PAR (RFC 9126)
- FAPI (для финансов)
Как надо: Используйте готовые решения. Auth0, Keycloak, Cognito, Okta, Supabase Auth, Logto. Open-source или managed — не пишите сами. Если вам кажется, что ваш случай «особенный» — перечитайте этот пункт ещё раз.
#3. Хранение секретов в коде
# config/auth.py
JWT_SECRET = "super_secret_key_123"Это не шутка. Я находил production-ключи в GitHub репозиториях компаний из Fortune 500. И не один раз.
Как надо:
- Всегда используйте vault / secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager)
- Для локальной разработки — .env файлы, которые не попадают в git
- Для секретов CI/CD — secrets от CI-системы
- Регулярная ротация секретов
- Никогда — никогда — не коммитьте секреты
#4. Игнорирование rate limiting
«У нас всего 100 пользователей, зачем нам rate limiting?». Через месяц — сайт лёг под DDoS на страницу логина. Или: злоумышленник перебирает пароли со скоростью 1000 попыток в секунду.
Rate limiting — это не фича для больших. Это базовая защита.
#5. Единый токен для всего
Один JWT, который даёт доступ ко всему. Сервис А — не должен иметь доступ к данным сервиса Б. Пользователь-админ не должен иметь доступ к пользовательским данным только потому, что «он же админ».
Как надо: Scopes, permissions, service-specific tokens. Каждый токен — минимально необходимый набор прав.
#6. Игнорирование Cross-Origin безопасности
Сценарий: вы сделали SPA на React, JWT хранится в localStorage. На страницу внедрили XSS-скрипт. Всё — токен украден, аккаунт угнан.
Проблемы:
- localStorage доступен через JavaScript (XSS → кража токена)
- Нет CSRF-защиты (запросы от другого origin обрабатываются)
- CORS настроен разрешительно (Access-Control-Allow-Origin: *)
Как надо:
- JWT — не в localStorage. Храните access token в JavaScript-переменной (в памяти приложения), refresh token — в httpOnly Secure SameSite=Strict cookie
- Cookies с флагом HttpOnly — недоступны из JS, не украдёт XSS
- SameSite=Strict — не отправляются с cross-site запросов (защита от CSRF)
- CSRF-токены — для legacy-систем, где SameSite не поддерживается
- CORS — белый список конкретных origins, не wildcard
- Content Security Policy (CSP) — блокирует инлайн-скрипты, снижает риск XSS
Set-Cookie: refresh_token=xxx; HttpOnly; Secure; SameSite=Strict; Path=/auth; Max-Age=604800Архитектурный паттерн BFF (Backend for Frontend): SPA не общается с auth-сервисом напрямую. Всё идёт через BFF — backend-прокси, который:
- Хранит refresh token в server-side session
- Выдаёт access token в memory (не localStorage)
- Автоматически обновляет токены
- Может форсировать logout
Browser ←cookies→ BFF ←JWT→ API Services
│
(BFF сам управляет
refresh token rotation)Это решает проблему безопасности JWT на клиенте. Цена — дополнительный сервис в архитектуре и latency на каждый запрос.
#7. Хранение паролей в plaintext или с быстрым хешированием
«Мы используем MD5, это же быстро!». MD5 — это миллиарды хешей в секунду на современном GPU. Если утёк файл с паролями — все пароли восстановят за минуты.
Как надо:
- bcrypt с cost factor >= 10 (замедляет перебор)
- argon2id — современная альтернатива (ещё более устойчива к GPU-перебору)
- scrypt — если нужно контролировать потребление памяти
- Соль (salt) — уникальная для каждого пароля
- Pepper — секрет, известный только приложению (хранится в vault)
# bcrypt — просто и безопасно
import bcrypt
salt = bcrypt.gensalt(rounds=12) # ~250ms на современном CPU
hash = bcrypt.hashpw(password.encode(), salt)
# argon2 — современная альтернатива
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hash = ph.hash(password)Важно: если пользователь забыл пароль — вы не можете его восстановить. Только сброс. Если ваша система «восстанавливает пароль» — значит, вы храните пароли в plaintext или reversible encryption.
#Ключевые выводы
-
Authn и authz — разные системы. Аутентификация проверяет «кто вы?». Авторизация — «что вам можно?». Не смешивайте их.
-
Stateless — это компромисс. JWT даёт масштабирование без stateful-хранилища. Но revocation — это плата за stateless. Всегда продумывайте: «что делать при утечке токена?».
-
OAuth 2.1 + OIDC — стандарт де-факто. Не SAML (если вы не enterprise в 2010-м). Не собственный протокол. Authorization Code + PKCE — единственный правильный flow.
-
Модель авторизации — под контекст. RBAC — для простого, ABAC — для сложного, ReBAC — для hierarchical/collaborative. Выбирайте осознанно.
-
Rate limiting — это обязанность, а не опция. Особенно на auth endpoint. Без rate limiting ваш auth сервер — открытая дверь.
-
Не пишите auth-систему сами. Используйте готовые решения. Это не про «not invented here syndrome». Это про то, что безопасность auth — это десятки человеко-лет опыта, которые вы не повторите за спринт.
-
Zero Trust — это подход, а не продукт. Каждый запрос проверяется. Каждый сервис авторизует самостоятельно. Доверие — это не то, что вы даёте один раз.
Следующая глава: Глава 10 — Observability: мониторинг, логирование, трейсинг Cross-reference: Глава 30 — Security at Scale (расширение моделей безопасности на enterprise-масштаб) Forward reference: AI-агенты создают новые векторы угроз — от slopsquatting до agent-to-agent атак (Глава 37: «Анти-паттерны AI-assisted разработки»).