Часть II: Строительные блоки
Глава 9

Аутентификация и авторизация — архитектура безопасности

[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.signature

Header — метаданные: алгоритм подписи (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 минут. Что делать?

Варианты решения (все — компромисс):

  1. Короткие access token + refresh token — access token живёт 5-15 минут, refresh token — дольше (часы/дни). Если надо отозвать доступ — удаляете refresh token на сервере. Access token истечёт сам через 15 минут. На практике — работает хорошо. Проблема — refresh token снова требует stateful-хранилища.

  2. Чёрные списки (blacklist) — храните ID отозванных токенов (jti claim) в Redis до их истечения. Каждый запрос проверяет: «этот jti не в чёрном списке?». Минус — каждый запрос снова требует обращения к Redis. Вы вернулись к сессиям, но с бóльшими токенами.

  3. Graceful degradation — принимаем, что в худшем случае доступ может быть продлён на время жизни токена. Для многих систем — приемлемо. Для финансовых — нет.

  4. 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 — это то, что разрешает приложение. В 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 (что уже диагноз). Работает через редиректы браузера.

Как работает:

  1. Пользователь заходит в Service Provider (SP) — например, в Salesforce
  2. SP не видит сессии, редиректит на Identity Provider (IdP) — например, Okta
  3. Пользователь логинится на IdP (если ещё не залогинен)
  4. IdP генерирует SAML Assertion (XML с подписью) и отправляет SP через браузер
  5. 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 — это сложно

  1. Единая точка отказа. IdP упал — никто никуда не заходит. Нужна избыточность, кэширование, grace period для токенов.
  2. Logout. Single Logout (SLO) — это кошмар. Надо разлогинить пользователя на всех сервисах. SAML SLO — бомба замедленного действия. OIDC SLO — чуть проще, но всё ещё нетривиально.
  3. Session management. Если пользователь меняет пароль — надо завершить все сессии на всех сервисах.
  4. Latency. Каждый редирект на IdP — это дополнительный round trip. Если IdP в другом регионе — время входа растёт.
  5. Отладка. Попробуйте отладить цепочку редиректов через 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 — плохо. Но если у вас:

  1. HTTPS (не перехватят трафик)
  2. Короткий access token (15 минут — потом бесполезен)
  3. Refresh token rotation (узнают о краже)
  4. Device fingerprinting (подозрительный логин — дополнительный фактор)
  5. 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"
    }
  ]
}

Архитектурный паттерн:

  1. Auth-сервис публикует JWKS endpoint
  2. Все сервисы кэшируют JWKS (TTL — обычно 24 часа)
  3. Каждый сервис получает JWT, находит ключ по kid в header, верифицирует подпись
  4. Если подпись валидна — токен корректен, пароль/сессию не проверяем

Проблема: Что делать, если приватный ключ скомпрометирован? Надо:

  • Сгенерировать новую пару ключей
  • Добавить новый ключ в 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:

  1. Multi-tenant изоляция. Каждый tenant — отдельное пространство конфигурации. Но shared инфраструктура для масштабирования. Изоляция на уровне данных, не на уровне сервисов.

  2. Auth Pipeline с изоляцией. Правила (Rules / Actions) — это пользовательский код, который выполняется при логине. Auth0 запускает его в изолированном sandbox (Webtask — их собственная serverless-платформа). Если кастомное правило упало — не падает весь auth flow, только для этого tenant.

  3. Token Service — stateless. JWT подписываются асимметрично. Каждый сервис может проверить токен без обращения к Auth0 (через JWKS endpoint). Это ключевое для производительности.

  4. Connections — плагинная архитектура. Базы данных, социальные логины (Google, Facebook), enterprise-IdP (SAML, OIDC, LDAP) — каждый реализован как плагин. Добавление нового connection не требует изменения auth pipeline.

  5. Graceful degradation. Если Redis упал — Auth0 продолжает отвечать на запросы с закэшированными конфигурациями tenant. Если token service упал — уже выданные токены продолжают работать (проверка через JWKS, не через token service).

#Keycloak

Keycloak — open-source, self-hosted, Java (WildFly / Quarkus). Подход — дать enterprise функциональность без vendor lock-in.

Ключевые архитектурные решения Keycloak:

  1. Infrastructure sessions. Keycloak использует инфраструктурные сессии — не user session для UI, а session для отслеживания auth flow. Каждый login request — это сессия, которая может быть восстановлена при падении ноды.

  2. User Federation. Keycloak не только хранит пользователей сам, но и умеет подключаться к внешним хранилищам (LDAP, Active Directory) через federation providers. Пользователь «живёт» в LDAP, а Keycloak даёт к нему OIDC-доступ.

  3. SPI (Service Provider Interface). Keycloak — это не продукт, а платформа. Почти всё можно переопределить через SPI: аутентификацию, авторизацию, синхронизацию пользователей, email-уведомления. И это одновременно плюс (гибкость) и минус (можно прострелить ногу).

  4. Events. Keycloak генерирует события на каждый auth action: login, logout, token refresh, consent, error. Это основа для audit log, аномалий-детекции, интеграции с SIEM.

  5. 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.


#Ключевые выводы

  1. Authn и authz — разные системы. Аутентификация проверяет «кто вы?». Авторизация — «что вам можно?». Не смешивайте их.

  2. Stateless — это компромисс. JWT даёт масштабирование без stateful-хранилища. Но revocation — это плата за stateless. Всегда продумывайте: «что делать при утечке токена?».

  3. OAuth 2.1 + OIDC — стандарт де-факто. Не SAML (если вы не enterprise в 2010-м). Не собственный протокол. Authorization Code + PKCE — единственный правильный flow.

  4. Модель авторизации — под контекст. RBAC — для простого, ABAC — для сложного, ReBAC — для hierarchical/collaborative. Выбирайте осознанно.

  5. Rate limiting — это обязанность, а не опция. Особенно на auth endpoint. Без rate limiting ваш auth сервер — открытая дверь.

  6. Не пишите auth-систему сами. Используйте готовые решения. Это не про «not invented here syndrome». Это про то, что безопасность auth — это десятки человеко-лет опыта, которые вы не повторите за спринт.

  7. Zero Trust — это подход, а не продукт. Каждый запрос проверяется. Каждый сервис авторизует самостоятельно. Доверие — это не то, что вы даёте один раз.


Следующая глава: Глава 10 — Observability: мониторинг, логирование, трейсинг Cross-reference: Глава 30 — Security at Scale (расширение моделей безопасности на enterprise-масштаб) Forward reference: AI-агенты создают новые векторы угроз — от slopsquatting до agent-to-agent атак (Глава 37: «Анти-паттерны AI-assisted разработки»).