Экспресс-курс · No. 08
За одним словом «auth» прячутся два разных вопроса. Аутентификация подтверждает, кто ты; авторизация решает, что тебе можно. Спутай их, пропусти любой или поверь клиенту — и дверь открыта.
Только суть · Один образ на идею · Стандарты важнее самоделок
За словом «auth» прячутся два разных вопроса. Большинство багов безопасности рождается из их путаницы — или из того, что отвечают на один и забывают про другой.
Аутентификация: кто ты?
Показать паспорт в аэропорту — доказать, что ты и правда тот человек, за кого себя выдаёшь.
Аутентификация (authN) — это подтверждение личности: вход, passkey, токен, который говорит «это и правда Алиса». Обычно случается один раз за сессию. Ошибёшься — и самозванец входит как кто-то другой. Но подтверждение того, кто ты, не говорит ничего о том, что тебе можно, — это совсем другой вопрос.
Авторизация: что тебе можно?
Карта-ключ от отеля — она ничего не доказывает о твоём имени, но открывает твой номер, а не чужие, и не кабинет управляющего.
Авторизация (authZ) — это решение, что личности разрешено делать: читать это, править то, никогда не трогать админку. В отличие от authN, её надо проверять на каждый запрос, потому что один залогиненный пользователь может трогать сотню разных вещей. Знать, кто человек, — это не разрешение делать что-то конкретное.
Два слоя — никогда не схлопывай их
Клуб: вышибала проверяет ID на входе один раз, а бармен всё равно проверяет твой возраст каждый раз, когда ты заказываешь. Две проверки, два момента.
Классическая ошибка — слить эти двое: подтвердить личность один раз и доверять этому навсегда или запихнуть все права в токен входа и больше не перепроверять. AuthN происходит на входной двери; authZ — на каждой двери внутри. Держи их раздельно и проверяй вторую постоянно.
Никогда не делай свою
Ты не варишь себе банковский сейф из листового железа в гараже — ты покупаешь тот, что построили и испытали люди, которые больше ничем не занимаются.
Личность — это фундамент, на котором стоит всё остальное, и ошибиться в ней тонко до жути легко. Используй проверенные стандарты и библиотеки — устоявшиеся протоколы, обкатанную auth-библиотеку или провайдера, — а не хитрую схему, которую ты придумал. История безопасности вымощена самодельным auth, который выглядел нормально, пока вдруг не переставал.
Аутентификация — это дверь. Авторизация — каждая дверь внутри. Пропусти вторую — и первая ничего не значит.
Аутентификация сводится к доказательству, что ты — это ты. Факторы тянутся от слабого, но универсального пароля до современного, устойчивого к фишингу passkey.
Пароли: слабый дефолт
Секретное слово, известное тебе и швейцару, — нормально, пока его не подслушали, не угадали или ты не используешь одно и то же везде.
Пароли универсальны и при этом — слабейший общий фактор: их переиспользуют, угадывают, выуживают фишингом. Если без них никак, незыблемое правило — никогда не хранить их текстом — храни медленный, солёный хеш (Argon2, bcrypt), чтобы украденная база не выдала всем чужие пароли. (Подробнее в главе про ошибки.)
Многофакторка: что-то знаешь, имеешь, есть
Банковской ячейке нужен твой ключ и ключ банка. Один сам по себе не открывает ничего.
MFA (двухфакторка) требует второе доказательство сверх пароля — что-то, что у тебя есть (код из приложения, аппаратный ключ) или что-то, чем ты являешься (отпечаток). Тогда даже украденный пароль сам по себе бесполезен. Код из приложения-аутентификатора (TOTP) надёжен; SMS-коды слабы (перехватываемы), но лучше, чем ничего. MFA — это самый большой и дешёвый выигрыш в безопасности, который можно добавить.
Passkeys: будущее без паролей
Ключ, который работает только у той одной двери, под которую был выточен, — подделай сайт, сфоткай замок, и ключ просто не повернётся.
Passkeys (на основе WebAuthn) заменяют пароли криптографической парой ключей, привязанной к твоему устройству и настоящему сайту, так что нечего фишить, переиспользовать или сливать — и Apple, Google и Microsoft уже делают их дефолтом. Приватный ключ никогда не покидает устройство; сайт видит лишь подпись. Вот куда движется аутентификация: красть нечего.
Пароль — это секрет, который можно потерять. Passkey — это доказательство, которое нельзя. Добавь второй фактор в любом случае.
HTTP забывает тебя между запросами, так что после входа системе нужен способ продолжать тебя узнавать. Два подхода с реальным разменом.
Проблема: у HTTP нет памяти
Магазин, где продавец забывает тебя, едва ты отвернулся, — поэтому нужен чек, чтобы на следующем шаге доказать, что ты уже заплатил.
Каждый HTTP-запрос независим; сервер не помнит, что ты вошёл секунду назад. Поэтому после аутентификации тебе вручают нечто — куку или токен, — что ты шлёшь с каждым запросом, говоря «это всё ещё я». Всё ниже — два способа это сделать. (Это та самая безсостоятельность из курса про протоколы.)
Сессии: сервер помнит
Гардероб — у тебя на руках маленький номерок, а вся реальная информация лежит за стойкой под этим номером.
При сессиях сервер хранит, кто ты, и вручает тебе куку, в которой лежит лишь случайный id сессии. На каждый запрос он этот id ищет. Сервер — источник правды, так что выйти или забанить кого-то — мгновенно: просто удали сессию. Цена в том, что серверу надо хранить это состояние и искать в нём. Правильный дефолт для большинства веб-приложений.
JWT: токен несёт правду
Фестивальный браслет, на котором уже напечатан и запаян от подделки твой уровень доступа, — охранник проверяет пломбу, а не список гостей.
JWT — это подписанный токен на руках у клиента, который содержит личность и права; сервер просто проверяет подпись, без поиска. Это делает его безсостоятельным и лёгким для масштабирования по сервисам — но тяжёлым для отзыва: утёкший токен валиден, пока не истечёт. Поэтому держи срок жизни коротким и сочетай с refresh-токеном. Отлично для API и микросервисов; избыточно для простого веб-приложения.
Где ты его хранишь — важно
Ключ в запертом ящике против ключа, приклеенного к входной двери, — ключ тот же, безопасность совсем разная.
Токен в браузере место в httpOnly-куке (JavaScript не может её прочитать), а не в localStorage, откуда любой внедрённый скрипт его украдёт. Хранилище — часть безопасности: ошибёшься — и один XSS-баг отдаёт атакующему все сессии. Как ты переносишь и хранишь креденшл, не менее важно, чем сам креденшл.
Сессии: сервер помнит и может забыть тебя мгновенно. JWT: помнит токен, и забрать его назад непросто.
Часто ты вообще не хочешь иметь дело с паролем — ты хочешь, чтобы пользователи входили под уже имеющимся аккаунтом или чтобы один сервис действовал за другой. Для этого и есть OAuth2 и OIDC.
OAuth2: дать доступ, не делясь паролем
Парковочный ключ, который заводит машину и открывает дверь, но не багажник и не бардачок, — ты отдаёшь ограниченный доступ, а не всю связку.
OAuth2 позволяет пользователю выдать твоему приложению ограниченный доступ к его аккаунту в другом сервисе — твоё приложение читает его Google Calendar — ни разу не дав тебе его пароль от Google. Другой сервис вручает твоему приложению ограниченный по области access-токен («читать календарь, больше ничего»). Это про делегированную авторизацию, а не про личность.
OIDC: OAuth2, который ещё и говорит, кто ты
Тот же парковочный ключ, но теперь с пристёгнутым удостоверением с фото — не только «что этот ключ открывает», но и «и вот чей он».
Один OAuth2 говорит, к чему приложению можно дотянуться, а не кто пользователь. OpenID Connect (OIDC) добавляет сверху тонкий слой личности — ID-токен с проверенными claims (это Алиса, вот её email). Именно это питает «Войти через Google» и single sign-on. Когда нужен соц-логин или SSO, OIDC — это стандарт.
API-ключи: простой машинный креденшл
Запасной ключ, который ты даёшь доверенному подрядчику, — без имени, просто «у кого он есть, тот может войти».
API-ключ — это длинная секретная строка, которая идентифицирует программу, а не человека, — для доступа «сервис — сервис» и программного доступа. Просто и эффективно, но он и есть креденшл: кто его найдёт, тот внутри. Поэтому ограничивай его область, ротируй и никогда не коммить в git и не клади в URL. Хорош для машин; не замена настоящей личности пользователя.
OAuth2 одалживает доступ без пароля. OIDC добавляет «и вот кто это». Используй стандарт — не изобретай свой танец с токенами.
Когда ты знаешь, кто человек, ты решаешь, что ему можно трогать. Модели тянутся от грубых ролей до тонкой политики — и правило под всеми ними одно: deny by default.
Deny by default, минимум привилегий
Бейдж нового сотрудника, который не открывает ничего, пока каждую дверь явно не разрешат, — а не открывает всё, пока кто-то не вспомнит запереть пару.
Фундамент авторизации: всё запрещено, пока явно не разрешено, и каждая личность получает минимум доступа, который ей нужен, не больше. Системы «по умолчанию открыто» текут — кто-то всегда забывает ограничить новый эндпоинт. Системы «по умолчанию закрыто» падают безопасно. Начинай с «нет» и выдавай осознанно.
RBAC: права по роли
Театр, где тип билета — партер, бельэтаж, бэкстейдж — решает, куда тебе можно, не называя тебя лично.
Role-Based Access Control группирует права в роли — admin, editor, viewer — и назначает пользователям роли. Просто, читаемо и достаточно для большинства приложений: «редакторы могут публиковать, зрители нет». Становится грубо, когда правила зависят от контекста или от конкретной записи, — тут на сцену выходят следующие модели.
ABAC: права по политике
Правило вместо списка — «менеджеры могут одобрять расходы до 10k, в рабочие часы, с корпоративного устройства» — вычисляемое заново каждый раз.
Attribute-Based Access Control решает по атрибутам пользователя, ресурса и контекста — роль и сумма и время и устройство. Куда гибче фиксированных ролей и куда сложнее. Сигнал, что пора: ты навешиваешь больше пары кастомных условий if на свои проверки ролей.
Владение: можешь ли ты трогать эту запись?
У тебя есть действующий читательский билет (роль), но это не даёт тебе читать заметки, нацарапанные в чужой взятой на руки книге.
Самая пропускаемая проверка: даже валидный «редактор» должен править только свои документы, а не все подряд. Роль говорит «редакторы могут править»; тебе всё равно надо проверить, что этот редактор владеет этой строкой. Забыть это — самый частый баг контроля доступа: поменял id в URL — и ты в чужих данных. Всегда проверяй объект, а не только роль.
Deny by default. Выдавай минимум. И всегда спрашивай не только «какая роль», но и «чья запись».
Auth ломается горсткой хорошо известных способов — одних и тех же, снова и снова, в приложении за приложением. Знать их — это половина защиты.
Сломанный контроль доступа: забыл проверить
Здание, где входная дверь под охраной, но каждая внутренняя — нараспашку: раз уж ты внутри, можешь ходить где угодно.
Уязвимость номер один в вебе: эндпоинт аутентифицирует пользователя, но забывает проверить, что ему авторизовано то, что он попросил. Её частейшая форма — IDOR: меняешь /orders/123 на /orders/124 и видишь чужой заказ, потому что сервер ни разу не проверил владение. Утечки миллионов записей восходят ровно к этому. Проверяй authZ на каждый запрос, на сервере, против реального объекта.
Неправильное хранение паролей
Держать запасные ключи от каждого дома на улице в стеклянной банке на крыльце.
Хранение паролей текстом — или с быстрым хешем вроде MD5 — означает, что одна утечка базы обнажает всех, а переиспользованные пароли компрометируют ещё и другие их аккаунты. Пароли надо медленно и индивидуально хешировать (Argon2, bcrypt), чтобы их было дорого взломать даже украденными. То, что хеш медленный, — это смысл, а не недостаток.
Утечка и доверие токенам
Написать код от сигнализации на стикере, приклеенном к двери, — замок в порядке; ты просто раздал секрет.
Токены и ключи утекают через URL (которые логируются), браузерный localStorage (который может прочитать скрипт) и секреты, закоммиченные в git. И токен надо проверять, а не доверять ему лишь потому, что он появился, — подделанный или истёкший надо отклонить. Относись к любому креденшлу как к тому, что однажды утечёт: короткий срок жизни, ротация и никогда — там, где это логируется или скрейпится.
Доверие клиенту
Магазин, который позволяет покупателям самим писать ценники и никогда не сверяет их на кассе.
Всё, что шлёт клиент — скрытое поле формы, роль в куке, флаг isAdmin=false, — можно подделать. Браузер, приложение, запрос — всё под контролем пользователя. Каждая настоящая проверка должна происходить на сервере, который пользователь править не может. Клиент — это предложение; сервер — это власть.
Почти любая утечка auth — это одно из трёх: пропущенная проверка, плохо хранимый секрет или поверили клиенту.
Хороший auth — это в основном про то, чтобы правильно использовать проверенные инструменты и неустанно проверять права, а не про изобретательность. Вот как выбирать.
Используй библиотеку или провайдера
Ты не производишь себе ремни безопасности — ты ставишь сертифицированные, спроектированные специалистами и испытанные в краш-тестах годами.
Почти для всех правильный ход — не строить auth самому: используй auth своего фреймворка или провайдера (Auth0, Clerk, Supabase, Cognito и подобные), который берёт на себя хеширование, сессии, MFA, OAuth и краевые случаи, о которых ты бы и не подумал. Твоя работа — корректно его подключить и владеть авторизацией — той частью, которую знаешь только ты.
Подбери механизм под приложение
Ключ от дома, ключ от машины и карта-ключ от отеля — всё это «ключи»; выбираешь ты по двери, а не по моде.
Сессии для обычного веб-приложения (просто, мгновенно отзываемо); короткоживущие JWT плюс refresh, когда нужен безсостоятельный масштаб по сервисам; OIDC для соц-логина и SSO; API-ключи для «машина — машина». Добавляй MFA везде, где это важно, особенно на админских аккаунтах. Выбирай по форме своего приложения, а не по тому, что звучит продвинуто.
- Пароли медленно хешируются (Argon2/bcrypt) и никогда не хранятся текстом? - Доступна ли MFA и обязательна ли для админов? - Каждый ли эндпоинт проверяет authZ — роль и владение объектом? - Токены в httpOnly-куках, короткоживущие и никогда не в URL или git? - Настоящая проверка на сервере, без доверия ролям от клиента? - Использую ли я проверенную библиотеку/провайдера вместо самопальной схемы?
- Смена id в URL показывает чужие данные. - Право живёт в куке или скрытом поле, которое клиент может править. - Пароли хранятся текстом или в MD5. - Нет способа отозвать залогиненного пользователя, кроме ожидания истечения. - Нет MFA на аккаунтах, способных нанести больше всего урона. - Ты написал свою схему токенов или крипту.
- AuthN и authZ раздельны, и authZ проверяется на каждый запрос. - Каждый доступ к объекту проверяет, что этот пользователь владеет этой вещью. - Креденшлы хешированы, ограничены по области, короткоживущи и ротируемы. - Сервер — это власть; ничто не доверяет клиенту. - Ты можешь отозвать доступ и увидеть кто что сделал в логе. - Тяжёлая работа лежит в проверенной библиотеке или провайдере, а не в твоём коде.
Auth — это не место, где умничают. Это место, где правильно используют стандарты, отказывают по умолчанию и никогда, ни за что не доверяют клиенту.