awesome-everything EN
↑ Обратно к восхождению

Безопасность

Ротация refresh-токенов и scope-based least privilege

Суть Как ротация refresh-токенов OAuth 2.1 обнаруживает украденные токены через replay, почему гранулярные scope ограничивают breach radius, и разница между introspection и local JWT validation.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 9 min

Refresh-токен утекает через log entry. Без ротации атакующий тихо обновляет новые access-токены месяцами — пока refresh-токен не истечёт естественно. С ротацией — первый раз, когда либо атакующий, либо легитимный клиент использует токен после своего counterpart, сервер обнаруживает replay, инвалидирует всю цепочку и форсирует re-аутентификацию.

Как работает ротация refresh-токенов

OAuth 2.1 мандатит, что каждое использование refresh-токена выдаёт новый refresh-токен и инвалидирует старый. Сервер отслеживает, какой refresh-токен заменил какой.

Нормальный flow:

  1. Клиент держит RT1. Представляет на /token. Сервер выдаёт access_token_2 + RT2. Помечает RT1 как used, linked to RT2. Клиент сохраняет RT2.

Под атакой:

  1. Атакующий крадёт RT1 (через log leak, network capture и т.д.).
  2. Атакующий использует RT1 первым → получает access_token_A + RT2. RT1 теперь помечен used.
  3. Легитимный клиент позже представляет RT1 (не зная о краже) → сервер видит RT1 уже used → replay detected.
  4. Сервер отзывает всю цепочку (RT1, RT2, все descendants). Оба — атакующий и легитимный клиент — теряют доступ. Пользователь вынужден re-аутентифицироваться.

Без ротации: украденный RT1 работает до absolute expiry — дни или месяцы. С ротацией: window вреда ограничен rotation interval — обычно минуты-часы до следующего легитимного refresh.

Ротация refresh-токена под атакой
Атакующий крадёт RT1 из log файла
ATKPOST /token { refresh_token: RT1 } → получает RT2 + access_token_A (RT1 помечен used)
CLIPOST /token { refresh_token: RT1 } → REPLAY DETECTED
SRVОтзывает RT1 + RT2 + все descendants. Возвращает invalid_grant: token replay detected.
Оба — атакующий и клиент — должны re-аутентифицироваться.

Scope и least privilege

Каждый access-токен несёт claim scope — точные разрешения, выданные на consent-этапе. Приложение запрашивает scope-set на /authorize; пользователь видит consent-экран и одобряет или отклоняет.

Почему гранулярные scope важны:

  • read:photos vs read:everything — photo-printing app должен видеть только фото, не email или календарь.
  • Blast radius: если приложение скомпрометировано, атакующие могут делать только то, что выданные scope разрешают. read:photos позволяет атакующему скачивать фото. write:photos delete:photos — ещё и уничтожать их.
  • User trust: consent-экран с read:photos разумен. С full account access — тревожен, и пользователи правильно делают, что отказывают.

Промышленный паттерн: read-scope выдаются свободно; write-scope требуют явного per-action consent; admin-scope никогда не появляются в third-party flow. RFC 9396 (Rich Authorization Requests) расширяет это, позволяя запросы вроде “перевести до $500 на счёт 12345” вместо просто write:transfers.

Introspection vs JWT validation: реальный tradeoff

Когда resource server получает access-токен, он должен его верифицировать. Два подхода:

Token introspection (/oauth/introspect): real-time вызов к IdP на каждый запрос. Всегда актуален — знает об отзывах немедленно. Стоит один extra round-trip на запрос (~5–50ms latency). Требуется для opaque токенов.

Local JWT validation: токен — signed JWT. Resource server валидирует подпись через JWKS. Быстро — микросекунды. Масштабируется до миллионов RPS. Недостаток: не видит отзывы до истечения токена. Отозванный JWT продолжает работать до оставшегося TTL.

Production паттерн: local JWT validation для hot path + короткий access token TTL (5–15 минут, принимая что отзывы распространяются в этом окне). Для sensitive writes (transfers, deletes) — introspection поверх. Маленький server-side кеш “recently introspected tokens” (TTL 30с) амортизирует cost introspection.

Викторина

Атакующий украл refresh-токен. С включённой ротацией каков максимальный window вреда?

Викторина

Почему local JWT validation проблематична для high-sensitivity write endpoints вроде 'перевести деньги'?

Расставь шаги по порядку

Поставь события rotation-based stolen-token detection в порядке:

  1. 1 Атакующий получает refresh-токен RT1 из leaked log
  2. 2 Атакующий представляет RT1 на /token — сервер выдаёт RT2, помечает RT1 как used
  3. 3 Легитимный клиент представляет RT1 на следующем scheduled refresh
  4. 4 Сервер обнаруживает RT1 был уже использован — replay detected
  5. 5 Сервер отзывает всю цепочку: RT1, RT2 и все descendants
  6. 6 Оба — атакующий и легитимный клиент — получают ошибку invalid_grant
  7. 7 Пользователь вынужден re-аутентифицироваться; атакующий теряет доступ
Вспомните перед уходом
  1. 01
    Как ротация refresh-токенов обнаруживает украденный refresh-токен?
  2. 02
    Почему использовать гранулярные scope вроде read:photos вместо широкого scope?
  3. 03
    Когда resource server должен использовать introspection вместо local JWT validation?
Итог

Ротация refresh-токенов (обязательная в OAuth 2.1) превращает украденный токен из неограниченного credential в timed detection window: следующий легитимный refresh-вызов видит replay и убивает цепочку. Гранулярные scope применяют least privilege — consent-экран показывает что приложение может делать, и токен это применяет. Валидация токенов предлагает два режима: local JWT validation быстрая, но не видит отзывы; introspection real-time, но добавляет latency. Production системы используют local validation для read paths, introspection для sensitive writes, и короткие TTL (5–15 мин) как backstop.

Связанные уроки
встречается в178
Продолжить восхождение ↑Sender-constrained токены: DPoP и mTLS
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.