Аутентификация и авторизация
Сначала доказать, кто пользователь, затем решить, что ему разрешено.
За 30 секунд
Аутентификация отвечает на вопрос «кто вы?», а авторизация — «что вам можно?». Пользователи подтверждают личность паролями, одноразовыми кодами, входом через соцсети или ключами, а сервер запоминает их с помощью сессии или токена. Затем роли и права определяют доступ. Большинство инцидентов безопасности начинается с мелких ошибок именно здесь, поэтому паттерны стоит знать хорошо.
Почему я это использую
- Я делаю авторизацию в каждом продукте: вход для клиентов в вебе и мобильном приложении, вход для персонала в админку и машинный доступ для партнёров.
- Я использую SMS OTP, вход через соцсети и 2FA, потому что каждой аудитории нужен свой баланс удобства и безопасности.
- Я тщательно проектирую роли и права, потому что в мультитенантном SaaS одна пропущенная проверка может раскрыть данные другой компании.
- Я выделяю авторизацию в отдельный модуль с тестами, потому что именно эту часть атакующие пробуют первой.
Как это работает
01Сессии или токены
После входа сервер должен узнать вас при следующем запросе. С сессиями сервер хранит состояние входа (в Redis или базе данных) и выдаёт браузеру случайный id сессии в cookie — как номерок в гардеробе. Сам по себе номерок ничего не значит; сервер ищет по нему запись. Выход из системы — это просто: удалить сессию. С токенами сервер выдаёт клиенту подписанный токен, который сам содержит claims — например, id пользователя и срок действия, — и сервер проверяет подпись вместо поиска. Сессии — лучший вариант по умолчанию для сайтов на вашем собственном домене. Токены подходят мобильным приложениям, сторонним клиентам и вызовам между сервисами. Многие продукты используют и то, и другое.
- Сессионная cookie: HttpOnly, Secure, SameSite=Lax (или Strict), короткий таймаут бездействия.
- Перегенерируйте id сессии при входе, чтобы предотвратить session fixation.
- Cookies требуют защиты от CSRF; bearer-токены в заголовках требуют защиты от XSS.
02JWT: access-токены, refresh-токены и отзыв
JWT (JSON Web Token) состоит из трёх частей: заголовка, payload с claims и подписи. Payload может прочитать кто угодно, поэтому никогда не кладите туда секреты; подпись лишь доказывает, что токен не изменяли. Для вызовов API используйте короткоживущий access-токен (5–15 минут), а для получения новых access-токенов — долгоживущий refresh-токен, который хранится безопасно. Ротируйте refresh-токены: каждое использование выдаёт новый и аннулирует старый. Если старый refresh-токен когда-нибудь используют снова, считайте, что его украли, и отзывайте всё семейство токенов. Поскольку JWT действителен до истечения срока, отзыв требует дополнительной работы: denylist id токенов (jti) в Redis, версия токена у пользователя или повторная проверка пользователя на каждом запросе. Когда токены проверяют несколько сервисов, предпочитайте асимметричную подпись (RS256 или EdDSA) и всегда проверяйте alg, iss, aud и exp.
// Verify an access token (jose library)
import { jwtVerify, createRemoteJWKSet } from 'jose';
const jwks = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
export async function verifyAccessToken(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer: 'https://auth.example.com',
audience: 'api.example.com',
algorithms: ['RS256'],
});
if (await denylist.has(payload.jti as string)) throw new Error('Token revoked');
return payload;
}
03OAuth2, OpenID Connect и вход через соцсети
OAuth2 — стандарт делегированного доступа: пользователь позволяет приложению действовать от его имени, не отдавая ему пароль. OpenID Connect (OIDC) — слой поверх него, который добавляет идентичность: ID-токен, сообщающий, кто пользователь. «Войти через Google» или «Войти через Apple» — это OIDC. Для веб- и мобильных приложений используйте Authorization Code flow с PKCE (Proof Key for Code Exchange), который защищает код от перехвата; старый Implicit flow устарел. Всегда проверяйте параметр state против CSRF, проверяйте подпись ID-токена, issuer, audience и nonce и связывайте аккаунты соцсетей по стабильному subject id провайдера, а не только по email. iOS-приложения, которые предлагают сторонний вход через соцсети, обязаны предлагать и вариант с упором на приватность, например Sign in with Apple (правило App Store 4.8).
04Пароли, OTP и 2FA
Никогда не храните пароли — храните хэши. Используйте Argon2id (минимум по OWASP: 19 MiB памяти, 2 итерации, параллелизм 1) или bcrypt с cost 10 и выше; учтите, что bcrypt использует только первые 72 байта. Фреймворки делают это за вас: фасад Hash в Laravel или пакет argon2 в Node. Проверяйте новые пароли по спискам известных утечек и ограничивайте частоту попыток входа. Одноразовые пароли по SMS или email удобны на рынках, где всё завязано на телефон: генерируйте случайный 6-значный код, храните только его хэш с коротким сроком действия (около 5 минут), разрешайте несколько попыток и ограничивайте отправки на номер и на IP, чтобы не допустить злоупотребления стоимостью SMS. Двухфакторная аутентификация добавляет второе подтверждение: TOTP-приложения, passkeys (WebAuthn) или SMS как более слабый запасной вариант. Требуйте 2FA для персонала и администраторов.
// Laravel: OTP verification with rate limiting
$key = 'otp:'.$request->phone;
if (RateLimiter::tooManyAttempts($key, 5)) {
abort(429, 'Too many attempts, try again later.');
}
RateLimiter::hit($key, 600);
$otp = OtpCode::where('phone', $request->phone)
->where('expires_at', '>', now())
->latest()->first();
if (! $otp || ! Hash::check($request->code, $otp->code_hash)) {
throw ValidationException::withMessages(['code' => 'Invalid or expired code.']);
}
$otp->delete();
RateLimiter::clear($key);
05Роли, права и API-ключи
RBAC (role-based access control, управление доступом на основе ролей) назначает пользователям роли (owner, manager, cashier), а роли несут права (orders.refund, menu.edit). В коде проверяйте права, а не названия ролей: «может делать возврат» переживёт появление новых ролей, а «является менеджером» — нет. Держите проверки на сервере в одном месте, например в политиках и гейтах Laravel или в guards NestJS, а скрытие кнопок в UI используйте только для удобства. В мультитенантных приложениях каждая проверка учитывает и тенанта: менеджер компании A не имеет прав в компании B. Для машинных клиентов выдавайте API-ключи со scope: генерируйте длинный случайный ключ, показывайте его один раз, храните только хэш, добавляйте видимый префикс для идентификации (например, live_ или test_) и поддерживайте истечение срока, ротацию и лимиты запросов на ключ. Логируйте, кто каким ключом пользовался.
06Чек-лист безопасных настроек по умолчанию
Большинство багов авторизации — в деталях. Ставьте cookies с HttpOnly, чтобы JavaScript не мог их прочитать, Secure, чтобы они передавались только по HTTPS, SameSite=Lax или Strict против CSRF и узким Path. По возможности используйте префикс __Host- для сессионных cookies. Возвращайте одну и ту же ошибку для «неизвестного пользователя» и «неверного пароля», чтобы нельзя было перебирать аккаунты. Ограничивайте частоту входа, OTP и сброса пароля. Делайте ссылки для сброса одноразовыми и короткоживущими. Логируйте события безопасности (входы, неудачные попытки, смену ролей, имперсонацию) и настройте алерты на всплески. Имперсонацию для поддержки ограничивайте по времени, требуйте указать причину и записывайте в аудит.
Set-Cookie: __Host-session=9f8c...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=7200
Когда применять
- Сессии с безопасными cookies для вашего собственного сайта и админки на одном домене.
- Короткоживущие JWT access-токены плюс ротируемые refresh-токены для мобильных приложений и нескольких сервисов.
- Вход через соцсети по OIDC, чтобы упростить регистрацию; SMS OTP там, где пользователи живут в телефоне.
- API-ключи со scope для партнёров и интеграций между серверами.
Когда не стоит
- Не используйте JWT вместо сессий для простого сайта на одном домене только потому, что это модно.
- Не храните токены в localStorage, если подойдёт HttpOnly cookie.
- Не пишите собственную криптографию, хэширование или OAuth-сервер, когда есть поддерживаемая библиотека или провайдер.
Альтернативы
- Laravel Sanctum / Passport
Встроенная авторизация на сессиях и токенах или полноценный OAuth2-сервер для Laravel-приложений.
- Провайдеры авторизации (Auth0, Clerk, Cognito, Keycloak)
Облачная или self-hosted идентификация; быстрый старт, меньше контроля, постоянные расходы.
- Passkeys (WebAuthn)
Беспарольный вход, устойчивый к фишингу; всё чаще лучший вариант по умолчанию для 2FA.
- Better Auth / Auth.js
Open-source библиотеки авторизации для проектов на TypeScript и Next.js.
Лучшие практики
- Хэшируйте пароли через Argon2id или bcrypt с помощью поддерживаемой библиотеки.
- Короткоживущие access-токены, ротируемые refresh-токены и стратегия отзыва.
- Authorization Code с PKCE для OAuth; проверяйте state, nonce и claims токена.
- Ограничивайте частоту входа, отправки и проверки OTP, а также сброса пароля.
- Проверяйте права на сервере и всегда в рамках тенанта.
- Храните только хэши OTP-кодов, API-ключей и токенов сброса.
- Требуйте 2FA для персонала и администраторов; логируйте события безопасности и настраивайте алерты.
Частые ошибки
- Долгоживущие JWT без возможности отозвать их после кражи телефона или увольнения сотрудника.
- Доверие id пользователя или роли, присланным клиентом, вместо аутентифицированного контекста.
- Неограниченная отправка OTP, которая превращает ваш SMS-шлюз в бесплатный инструмент атаки для кого-то другого.
- Принятие алгоритма «none» или отсутствие проверки audience и issuer у JWT.
- Скрытие кнопок админки в UI при незащищённых API-эндпоинтах.
Глубже
07Где живут токены: браузеры, паттерн BFF и хранилище на мобильных
Любой токен, который может прочитать JavaScript, может украсть XSS-баг. Поэтому хранить access и refresh токены в localStorage для одностраничного приложения рискованно. Паттерн Backend-for-Frontend (BFF) полностью убирает токены из браузера: тонкий сервер на вашем домене выполняет OAuth-поток, хранит токены на сервере и отдаёт браузеру только сессионную cookie с HttpOnly, Secure и SameSite. Вызовы API идут через BFF, который прикрепляет access token. На мобильных нет хранилища cookie, на которое можно опереться, поэтому храните refresh токены в защищённом хранилище платформы — Keychain на iOS и хранилище на базе Keystore на Android, — а не в обычных preferences. OAuth 2.0 Security Best Current Practice (RFC 9700, январь 2025) также рекомендует по возможности токены с привязкой к отправителю, например DPoP (RFC 9449), который связывает токен с ключом клиента, так что украденный токен сам по себе бесполезен.
- SPA на вашем собственном домене: сессионная cookie или BFF, а не localStorage.
- Мобильные: защищённое хранилище плюс ротация refresh токенов с привязкой к устройству.
- Сервис–сервис: client credentials или mTLS, короткоживущие токены.
08Passkeys и WebAuthn изнутри
Passkey — это пара ключей, которую устройство пользователя создаёт для одного сайта. Приватный ключ никогда не покидает устройство или менеджер паролей пользователя; сервер хранит только публичный ключ. Для входа сервер отправляет случайный challenge, устройство запрашивает отпечаток, лицо или PIN и подписывает challenge. Сервер проверяет подпись сохранённым публичным ключом. Принципиально важно, что браузер включает origin в подписываемые данные, поэтому поддельный сайт на похожем домене не получит действительную подпись: passkeys устойчивы к фишингу так, как SMS-коды и TOTP — нет. Ваш сайт — это relying party, идентифицируемая RP ID, например example.com. Используйте поддерживаемую библиотеку на обеих сторонах, сохраняйте хотя бы один путь восстановления и позволяйте пользователям регистрировать несколько passkeys.
// Browser, using @simplewebauthn/browser
import { startAuthentication } from '@simplewebauthn/browser';
const optionsJSON = await api.post('/webauthn/login/options'); // fresh, single-use challenge
const assertion = await startAuthentication({ optionsJSON });
await api.post('/webauthn/login/verify', assertion);
// Server (@simplewebauthn/server verifyAuthenticationResponse) must check:
// expected challenge, expected origin and RP ID, the signature against the
// stored public key, and user verification if your policy requires it.
09Атаки по времени, перебор аккаунтов и credential stuffing
Атакующие учатся на мельчайших различиях. Если «неизвестный email» возвращается за 5 мс, а «неверный пароль» за 80 мс, потому что хэш выполняется только во втором случае, они могут составить список существующих аккаунтов. Выполняйте фиктивный хэш для неизвестных пользователей и возвращайте одно и то же сообщение. Сравнивайте секреты — API-ключи, токены сброса и подписи webhook — функциями с постоянным временем, hash_equals в PHP или crypto.timingSafeEqual в Node, и никогда через ===. Credential stuffing — перебор утёкших с других сайтов пар email и пароля — самая частая атака на формы входа. Защищайтесь слоями: rate limits на аккаунт и на IP, проверка по утёкшим паролям (range API Have I Been Pwned использует k-анонимность, так что вы отправляете только первые пять символов SHA-1 хэша), CAPTCHA после подозрительных паттернов и алерты на всплески неудачных входов. Жёсткие блокировки аккаунта можно использовать, чтобы заблокировать реальных пользователей, поэтому лучше прогрессивные задержки.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isValidSignature(rawBody: Buffer, headerHex: string, secret: string) {
const expected = createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(headerHex, 'hex');
// timingSafeEqual throws on different lengths, so check that first
return received.length === expected.length && timingSafeEqual(received, expected);
}
10Авторизация за пределами ролей: ABAC, ReBAC и фильтрация списков
Роли отвечают на вопрос «что это за пользователь?», но реальные правила часто зависят от большего. Attribute-based access control (ABAC) добавляет условия: менеджер может вернуть деньги только за заказы своего филиала и только до определённого лимита. Relationship-based access control (ReBAC), популяризированный статьёй Google о Zanzibar и реализованный в OpenFGA и SpiceDB, моделирует права как граф: пользователь может редактировать документ, потому что состоит в команде, которой принадлежит его папка. Движки политик вроде OPA или Cedar держат правила вне кода. Самое сложное — списки: проверка каждой строки после загрузки ломает пагинацию и раскрывает количества. Вместо этого встраивайте авторизацию в запрос как scope, добавляющий условия по тенанту, филиалу или владельцу, чтобы база возвращала только строки, которые пользователю разрешено видеть. Тестируйте оба направления: разрешённые пользователи видят данные, все остальные получают 404.
Реальные ситуации
У менеджера украли телефон
Проблема
Менеджер филиала в 11 вечера сообщает, что у него украли телефон. На телефоне было залогинено админ-приложение с правами на возврат заказов и изменение цен.
Как решить
Действуйте за минуты, а не за часы. Отзовите все семейства refresh токенов и серверные сессии этого пользователя и увеличьте token version в записи пользователя, чтобы ещё действующие access токены проваливали проверку на каждом запросе. Закройте все активные сокет-соединения пользователя. Ротируйте все API-ключи или токены устройств, выданные этому телефону. Затем проверьте журнал аудита на возвраты, изменения цен или экспорты с момента кражи и откатите то, что нужно. После этого требуйте от сотрудников 2FA или passkey, держите access токены короткими и добавьте кнопку «выйти на всех устройствах», которой поддержка может пользоваться без разработчика.
Тысячи неудачных входов с множества IP
Проблема
За ночь эндпоинт входа получил 400,000 попыток с тысяч IP, каждая с другим email и правдоподобным паролем. Несколько аккаунтов были захвачены.
Как решить
Это credential stuffing с утёкшими паролями. Немедленно добавьте rate limits на аккаунт, а не только на IP, и при всплесках неудач ставьте перед входом CAPTCHA или challenge proof-of-work; WAF или защита от ботов на edge помогают справиться с объёмом. Принудительно сбросьте пароли скомпрометированных аккаунтов и инвалидируйте их сессии. Сделайте ответ одинаковым для неизвестных пользователей и неверных паролей. Затем устраните корневую причину: проверяйте пароли по спискам утечек при регистрации и входе, предлагайте passkeys или 2FA и настройте алерты на необычное соотношение неудач, чтобы следующую волну поймать за минуты.
Корпоративный клиент хочет single sign-on
Проблема
Крупный клиент SaaS хочет, чтобы его сотрудники входили через корпоративный identity provider, а аккаунты автоматически удалялись, когда сотрудники уходят.
Как решить
Поддержите OIDC, а если клиент требует — и SAML, с настройкой на уровне тенанта: issuer, client id, secret или метаданные. Проверьте, что клиент владеет своим почтовым доменом, затем направляйте пользователей с этим доменом к identity provider тенанта. Создавайте пользователей just in time при первом входе, сопоставляя группы с вашими ролями, и связывайте аккаунты по subject id провайдера, а не по email. Позвольте тенанту принудительно включить SSO, чтобы пароли его сотрудников были отключены, но сохраните аварийный вход владельца. Для автоматического удаления реализуйте SCIM provisioning или хотя бы периодические проверки и сократите сессии, чтобы отключённые пользователи быстро теряли доступ.
Шпаргалка
- Laravel: новый id сессии после входа
$request->session()->regenerate();- Laravel: обновить старые хэши при входе
if (Hash::needsRehash($user->password)) { $user->update(['password' => Hash::make($plain)]); }- Токен Sanctum с abilities
$user->createToken('pos-terminal', ['orders:create'])->plainTextToken- Sanctum: проверить ability
$request->user()->tokenCan('orders:create')- Безопасный случайный токен (PHP)
bin2hex(random_bytes(32))- Безопасный случайный токен (Node)
crypto.randomBytes(32).toString('base64url')- Сравнение за постоянное время (PHP)
hash_equals($expected, $given)- Прочитать claims JWT без проверки (только для отладки)
import { decodeJwt } from 'jose'; decodeJwt(token)- Документ OIDC discovery
https://auth.example.com/.well-known/openid-configuration
В моих реальных проектах
- В мультитенантном POS SaaS: JWT на RS256 с повторной проверкой на каждом запросе и denylist, 2FA для персонала и операторов, токены устройств для касс, SMS OTP для гостей и API-ключи live/test со scope, лимитами запросов и ротацией.
- Тот же SaaS поддерживает ограниченную по времени имперсонацию персонала с обязательным указанием причины, а также роли уровня платформы, компании и пользовательские роли, собранные из ключей прав.
- На платформе для сети ресторанов: email и пароль, регистрация с SMS OTP, вход через Google и Apple в вебе и нативных приложениях, вход в админку по OTP и 12 ролей администраторов.
- При миграции магазина с WordPress на Laravel я сохранил совместимость с хэшами паролей WordPress, чтобы клиентам не пришлось сбрасывать пароли.
Вопросы и ответы
Q1Сессии или JWT: что вы выбираете и почему?
Для сайта и админки на собственном домене — серверные сессии с безопасной HttpOnly cookie. Они простые, мгновенно отзываются и хорошо поддерживаются фреймворками. JWT имеют смысл, когда многим сервисам нужно проверять личность без общего хранилища сессий, или для мобильных и сторонних клиентов. Даже тогда я делаю access-токены короткоживущими и сочетаю их с ротируемыми refresh-токенами и механизмом отзыва. Типичная ошибка — выбрать JWT для одного веб-приложения на одном домене, а потом плохо пересобрать сессии с помощью denylist. Настоящий вопрос — кто ваши клиенты и как будет работать отзыв.
Q2Как работают ротация refresh-токенов и обнаружение повторного использования?
У клиента есть короткоживущий access-токен и долгоживущий refresh-токен. Когда access-токен истекает, клиент отправляет refresh-токен и получает новый access-токен и новый refresh-токен; старый refresh-токен помечается как использованный. Все токены одного входа принадлежат одному семейству (family id). Если использованный refresh-токен предъявляют снова, значит, копия есть либо у легитимного клиента, либо у атакующего, и понять, у кого именно, нельзя — поэтому вы отзываете всё семейство и требуете новый вход. Храните refresh-токены на сервере в виде хэшей, привязывайте их к устройству и истекайте неактивные.
Q3В чём разница между OAuth2 и OpenID Connect?
OAuth2 — про авторизацию: он позволяет клиенту получить access-токен, чтобы вызывать API от имени пользователя без его пароля. Кто такой пользователь, OAuth2 не определяет. OpenID Connect — слой идентификации поверх OAuth2. Он добавляет ID-токен — подписанный JWT с claims вроде subject id, email и времени аутентификации, — а также эндпоинт userinfo и discovery. Поэтому «Войти через Google» — это OIDC, а «разрешить этому приложению читать мой календарь» — OAuth2. В обоих случаях используйте Authorization Code flow с PKCE и проверяйте state, nonce, issuer и audience.
Q4Как безопасно хранить пароли?
Хэшировать их медленным алгоритмом с солью и высокими требованиями к памяти, созданным специально для паролей: первый выбор — Argon2id, не ниже минимума OWASP (19 MiB памяти, две итерации, параллелизм один). bcrypt с cost 10 и выше тоже допустим, если помнить о его ограничении на ввод в 72 байта. Никогда не используйте простой SHA-256 или MD5 и никогда не шифруйте пароли обратимо. Используйте API хэширования фреймворка: он сам работает с солью и позволяет перехэшировать пароли при смене параметров. Добавьте ограничение частоты попыток и проверку по утёкшим паролям, а сообщения об ошибках делайте общими, чтобы атакующие не могли выяснить, какие аккаунты существуют.
Q5Как бы вы реализовали роли и права в мультитенантном приложении?
У пользователей есть членство в каждом тенанте, и у каждого членства одна или несколько ролей. Роли — это наборы ключей прав вроде orders.refund или reports.view; есть встроенные роли, а тенанты могут создавать свои. Код проверяет права, никогда не названия ролей, через один центральный механизм — например, политики или guard. Каждая проверка также убеждается, что ресурс принадлежит текущему тенанту, а доступ к чужому тенанту возвращает 404. Роли уровня платформы для моих собственных операторов отделены от ролей тенантов, а чувствительные действия вроде имперсонации требуют указать причину и пишутся в аудит. В тестах для каждого модуля есть случай с «чужим» тенантом.
Q6Как защитить вход по SMS OTP от злоупотреблений?
Угроз две: подбор кодов и сжигание денег на SMS. Против подбора: случайный 6-значный код, хранение только его хэша, истечение примерно через пять минут, около пяти попыток и аннулирование после использования. Против злоупотребления стоимостью SMS: ограничение отправок на номер телефона, на IP и на устройство, паузы между повторными отправками, блокировка премиальных номеров и неожиданных кодов стран и CAPTCHA после повторных запросов. Следите за объёмом отправок и настройте алерты на всплески. Повторяйте отправку SMS только при явных ошибках соединения, чтобы медленный провайдер не привёл к дублированию сообщений одному и тому же пользователю.
Вопросы уровня senior
S1Спроектируйте аутентификацию для одного продукта с сайтом, мобильными приложениями и партнёрским API.
Одно ядро идентификации, три стиля входа. Сайт и админка используют серверные сессии в cookie с HttpOnly и SameSite, с защитой от CSRF и 2FA или passkeys для сотрудников. Мобильные приложения используют Authorization Code flow с PKCE или собственный эндпоинт входа и получают короткоживущий access token и ротируемый refresh token, который хранится в защищённом хранилище и привязан к записи устройства, чтобы поддержка могла отозвать одно устройство. Партнёры получают либо OAuth client credentials, либо API-ключи с ограниченными правами, хранящиеся в виде хэша, с ротацией и rate limits на ключ. Все три пути приводят к одной модели пользователя или клиента и одним и тем же проверкам прав с ограничением по тенанту. Компромисс: поддерживать три механизма дороже, чем один, но навязывание единой модели токенов всем обычно ослабляет сайт или усложняет жизнь партнёрам.
S2Где одностраничному приложению хранить токены?
В идеале там, куда JavaScript не дотянется. localStorage и sessionStorage читаются любым скриптом на странице, поэтому один XSS-баг или скомпрометированная зависимость приводит к утечке долгоживущих учётных данных. Хранение в памяти лучше, но теряется при перезагрузке, так что механизм обновления всё равно нужен. Я предпочитаю сессионную cookie, когда SPA и API находятся на одном сайте, или Backend-for-Frontend, который выполняет OAuth на сервере и отдаёт браузеру только cookie с HttpOnly, Secure и SameSite. Cookies приносят риск CSRF, с которым хорошо справляются SameSite плюс CSRF-токены или проверка Origin. Если токены всё-таки должны попасть в браузер, держите access токены в памяти на минуты, ротируйте refresh токены с обнаружением повторного использования и рассмотрите DPoP. Компромисс — лишний переход через BFF в обмен на гораздо меньший радиус поражения от XSS.
S3Как сделать авторизацию эффективной на эндпоинтах списков и поиска?
Проверка каждой строки после загрузки ломает пагинацию, тратит запросы и может раскрывать информацию через итоговые количества. Поэтому я перевожу права в фильтры запросов. Каждый запрос к данным тенанта получает обязательное условие по тенанту, а правила по ролям или атрибутам становятся scopes: менеджеры филиалов видят заказы, где branch_id входит в их филиалы, кассиры — только свои смены. Политики по-прежнему охраняют действия над одним объектом вроде возврата или удаления. Для поисковых движков я индексирую атрибуты доступа — тенант, филиал и видимость — вместе с каждым документом и всегда фильтрую по ним. С моделями на основе связей системы вроде OpenFGA могут вернуть список id объектов, доступных пользователю, но для больших результатов нужен фильтр, который можно передать в базу. Компромисс — дублирование логики правил между политиками и scopes, поэтому и то и другое нужно покрывать тестами.
S4Как бы вы добавили SSO для корпоративных тенантов в мультитенантный SaaS?
Отношусь к identity providers как к конфигурации на уровне тенанта. Каждый тенант может зарегистрировать OIDC-провайдера или SAML, если корпорация настаивает, с подтверждёнными почтовыми доменами. Страница входа сначала спрашивает email и направляет к провайдеру тенанта, если домен совпадает. В callback я полностью валидирую ID token или assertion, затем нахожу пользователя по провайдеру и subject id, создаю его just in time и сопоставляю группы identity provider с ролями тенанта. Тенанты могут принудительно включить SSO, отключив вход по паролю для своих пользователей, при этом остаётся аварийный аккаунт владельца. Для offboarding SCIM позволяет провайдеру деактивировать пользователей сразу; без него риск ограничивают короткие сессии. Главные компромиссы — нагрузка на поддержку, ведь неправильно настроенные провайдеры становятся вашими тикетами, и матрица тестирования по разным провайдерам.
Глоссарий
- Credential stuffing
- Автоматические попытки входа с парами email и пароля, утёкшими с других сайтов.
- Перебор аккаунтов
- Выяснение, какие аккаунты существуют, по различиям в сообщениях об ошибках или времени ответа.
- PKCE
- Proof Key for Code Exchange: одноразовый секрет, который не даёт кому-то другому обменять украденный OAuth authorization code.
- DPoP
- Demonstrating Proof of Possession: привязывает токен к ключу клиента, так что утёкший токен сам по себе использовать нельзя.
- BFF
- Backend-for-Frontend: небольшой сервер, который хранит OAuth-токены веб-приложения и отдаёт браузеру только сессионную cookie.
- Passkey
- Устойчивые к фишингу учётные данные: приватный ключ на устройстве и публичный ключ на сервере.
- Relying party
- Сайт или приложение, принимающее вход, например OIDC-клиент или сайт, проверяющий passkey.
- ReBAC
- Relationship-based access control: права, выводимые из связей вроде пользователь, команда, папка и документ.
- Session fixation
- Атака, при которой жертву заставляют использовать id сессии, уже известный атакующему; предотвращается сменой id при входе.
- SCIM
- Стандартный API, позволяющий identity provider компании автоматически создавать, обновлять и удалять аккаунты пользователей в вашем приложении.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.