Все уроки
БэкендСредний14 мин чтенияВ моём стеке

Аутентификация и авторизация

Сначала доказать, кто пользователь, затем решить, что ему разрешено.

  • Sessions
  • JWT
  • OAuth2
  • OTP
  • Roles & permissions
  • 2FA
Шаг 1/6
Вы нажимаете «Sign in with ID» на app.com. Приложению ваш пароль видеть не нужно — в этом весь смысл OAuth.

За 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.

ts
// 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 для персонала и администраторов.

php
// 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 и сброса пароля. Делайте ссылки для сброса одноразовыми и короткоживущими. Логируйте события безопасности (входы, неудачные попытки, смену ролей, имперсонацию) и настройте алерты на всплески. Имперсонацию для поддержки ограничивайте по времени, требуйте указать причину и записывайте в аудит.

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

Лучшие практики

  1. Хэшируйте пароли через Argon2id или bcrypt с помощью поддерживаемой библиотеки.
  2. Короткоживущие access-токены, ротируемые refresh-токены и стратегия отзыва.
  3. Authorization Code с PKCE для OAuth; проверяйте state, nonce и claims токена.
  4. Ограничивайте частоту входа, отправки и проверки OTP, а также сброса пароля.
  5. Проверяйте права на сервере и всегда в рамках тенанта.
  6. Храните только хэши OTP-кодов, API-ключей и токенов сброса.
  7. Требуйте 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.

ts
// 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 после подозрительных паттернов и алерты на всплески неудачных входов. Жёсткие блокировки аккаунта можно использовать, чтобы заблокировать реальных пользователей, поэтому лучше прогрессивные задержки.

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

Реальные ситуации

01

У менеджера украли телефон

Проблема

Менеджер филиала в 11 вечера сообщает, что у него украли телефон. На телефоне было залогинено админ-приложение с правами на возврат заказов и изменение цен.

Как решить

Действуйте за минуты, а не за часы. Отзовите все семейства refresh токенов и серверные сессии этого пользователя и увеличьте token version в записи пользователя, чтобы ещё действующие access токены проваливали проверку на каждом запросе. Закройте все активные сокет-соединения пользователя. Ротируйте все API-ключи или токены устройств, выданные этому телефону. Затем проверьте журнал аудита на возвраты, изменения цен или экспорты с момента кражи и откатите то, что нужно. После этого требуйте от сотрудников 2FA или passkey, держите access токены короткими и добавьте кнопку «выйти на всех устройствах», которой поддержка может пользоваться без разработчика.

02

Тысячи неудачных входов с множества IP

Проблема

За ночь эндпоинт входа получил 400,000 попыток с тысяч IP, каждая с другим email и правдоподобным паролем. Несколько аккаунтов были захвачены.

Как решить

Это credential stuffing с утёкшими паролями. Немедленно добавьте rate limits на аккаунт, а не только на IP, и при всплесках неудач ставьте перед входом CAPTCHA или challenge proof-of-work; WAF или защита от ботов на edge помогают справиться с объёмом. Принудительно сбросьте пароли скомпрометированных аккаунтов и инвалидируйте их сессии. Сделайте ответ одинаковым для неизвестных пользователей и неверных паролей. Затем устраните корневую причину: проверяйте пароли по спискам утечек при регистрации и входе, предлагайте passkeys или 2FA и настройте алерты на необычное соотношение неудач, чтобы следующую волну поймать за минуты.

03

Корпоративный клиент хочет 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 компании автоматически создавать, обновлять и удалять аккаунты пользователей в вашем приложении.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.