XNOVA
← Все статьи

СТАТЬЯ / engineering

JWT-токены: что это, как устроены Access и Refresh Token

Что такое JWT, как API проверяет access token и зачем нужен refresh token. Разбираем подпись, claims, обновление и ротацию на примере.

Автор: XNova

Клиент отправляет запрос к API заказов с токеном доступа. API проверяет токен, а затем решает, разрешена ли запрошенная операция. Когда срок действия токена заканчивается, запрос отклоняется. Чтобы пользователю не приходилось заново проходить процедуру получения доступа, во многих схемах используют access token и refresh token.

Выражение «JWT-авторизация» часто объединяет разные механизмы. JWT определяет формат передаваемых утверждений, OAuth 2.0 описывает выдачу и использование токенов, а права на конкретный заказ проверяет само приложение. Даже правильно подписанный токен не даёт автоматически доступ ко всем данным пользователя.

Разберём структуру подписанного JWT, назначение двух токенов и их жизненный цикл. На примере API заказов покажем проверку JWT, обновление пары токенов и отказ при повторном использовании старого refresh token. Для понимания достаточно знать, что такое HTTP-запрос, заголовок Authorization и код ответа 401.

JWT: формат и содержимое токена

JWT (JSON Web Token) — формат компактного представления утверждений, описанный в RFC 7519. Утверждение (claim) — поле, которому издатель токена придаёт определённый смысл: например, идентификатор субъекта или момент окончания действия. JWT используют в разных протоколах; само слово JWT не указывает ни на OAuth 2.0, ни на способ входа пользователя.

Для начала рассмотрим подписанный JWT в компактной форме JWS. Он состоит из трёх частей:

base64url(header).base64url(payload).base64url(signature)

В первой части находится заголовок с алгоритмом (alg) и, возможно, типом (typ). Во второй — JSON с утверждениями. Третья содержит цифровую подпись или код аутентификации сообщения (MAC), рассчитанный по первым двум частям. Точки отделяют сегменты, а base64url делает их пригодными для передачи как текст. Это кодирование не шифрует содержимое: полезную нагрузку можно прочитать без секретного ключа.

Подпись позволяет проверяющей стороне обнаружить изменение токена и убедиться, что его создал обладатель соответствующего ключа. Например, в учебном коде используется HS256 — HMAC с SHA-256 и общим секретом. В других системах применяют асимметричную подпись, где сервер авторизации подписывает закрытым ключом, а API проверяет открытым. Способ хранения ключей и список допустимых алгоритмов определяются архитектурой системы.

Три сегмента — характеристика компактного JWS, а не любого JWT. JWT может представлять и зашифрованные данные с использованием JWE; у его компактной формы пять частей. Разница между подписью и шифрованием описана в RFC 7516. Не помещайте пароли, секреты или лишние персональные данные в обычный подписанный JWT: подпись не скрывает его содержимое.

Claims: какие сведения проверяет API

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

ПолеЗначениеНа что влияет проверка
subСубъект, от имени которого выдан токенКому относится запрос
issИздатель токенаДоверяет ли API этому издателю
audАдресат токенаПредназначен ли токен данному API
expВремя окончания действия (NumericDate)Можно ли принимать токен сейчас
nbfВремя начала допустимого использованияНе предъявлен ли токен слишком рано
iatВремя выдачиКогда токен был создан

Время в NumericDate задаётся количеством секунд от эпохи Unix, а не строкой с датой. Поля iss, aud и exp не обязательны для любого JWT по RFC 7519; требования определяет приложение или профиль токена. Для нашего API заказов они обязательны по локальной политике. В учебном токене ожидаются iss=xnova-demo-auth, aud=orders-api и sub=user-42. Дополнительные поля scope=orders:read и token_use=access описывают разрешение и назначение токена в данном примере; они не обязательны для любого JWT.

API проверяет подпись доверенным ключом, ограничивает допустимые алгоритмы и сравнивает iss и aud с ожидаемыми значениями. Затем проверяет срок действия, назначение токена и разрешение orders:read. Успешная проверка подписи ещё не означает, что токен подходит конкретному API. Например, токен для платёжного сервиса должен быть отклонён API заказов из-за другого aud, даже если его подпись корректна.

Список алгоритмов задаёт проверяющая сторона, а не входящий JWT. RFC 8725 дополнительно требует избегать путаницы между токенами разного назначения: проверки для access token нельзя бездумно применять к ID token или любому другому подписанному JWT. В учебном примере назначение явно проверяется полем token_use=access.

Access token: зачем он нужен

Access token — значение, которое клиент предъявляет серверу ресурсов при обращении к защищённому API. В распространённой схеме bearer-токен передают в заголовке HTTP-запроса:

GET /orders HTTP/1.1
Host: api.example.test
Authorization: Bearer <access_token>

Сервер ресурсов — это API, к которому обращается клиент. Он проверяет токен и разрешения на операцию; наличие действительного токена само по себе не гарантирует доступ к любому ресурсу. По RFC 6750 bearer-токеном способен воспользоваться любой, у кого оказалось его значение, если нет дополнительных ограничений. Поэтому его необходимо защищать при хранении и передаче; для сетевых запросов нужен TLS.

Access token не обязан иметь формат JWT. OAuth 2.0 допускает значения, внутреннее устройство которых клиенту неизвестно. Для непрозрачного токена API может получать сведения о его статусе у сервера авторизации. При подписанном JWT подпись и часть claims можно проверять локально, но право на конкретный объект может потребовать дополнительных данных. Какой подход выбрать, зависит от политики отзыва, архитектуры и требований к доступности.

Отдельный RFC 9068 определяет профиль access token в формате JWT с дополнительными требованиями к заголовку и claims. Учебный токен ниже иллюстрирует общие проверки JWT; дополнительных требований профиля RFC 9068 он не реализует полностью.

Недействительный bearer-токен может приводить к 401 Unauthorized с WWW-Authenticate: Bearer error="invalid_token". Если токен действителен, но недостаточен по области разрешений, возможен 403 Forbidden с insufficient_scope. Наличие токена ещё не доказывает право просматривать конкретный заказ другого пользователя: такое право API проверяет отдельно.

Refresh token: как обновляют доступ

Refresh token отправляют серверу авторизации, чтобы получить новый access token без повторного прохождения всей процедуры выдачи доступа. Refresh token предназначен для получения новых access token и не передаётся серверу ресурсов. В OAuth 2.0 выдача refresh token необязательна: сервер авторизации может вообще не поддерживать его для выбранного клиента.

Refresh token не обязан быть JWT. В примере это случайная непрозрачная строка: клиент сохраняет её и отправляет только на адрес сервера авторизации, предназначенный для обновления. Учебный сервер авторизации хранит SHA-256-отпечаток выданной высокоэнтропийной строки для сравнения с предъявленным значением. Сам refresh token API заказов не получает.

Один из способов защиты — ротация. После успешного обмена сервер выдаёт новую пару access/refresh, делает старый refresh недействительным и сохраняет информацию о предыдущем значении. Если старый токен предъявят повторно, сервер сможет обнаружить повторное использование и отозвать активный токен этой цепочки. При этом он не способен по одному такому событию определить, кто предъявил значение — злоумышленник или законный клиент.

Для публичных клиентов RFC 9700 требует защищать refresh token с помощью ротации или криптографической привязки к отправителю. Ротация — один допустимый механизм, а не обязательный для всех клиентов способ работы. В реальной системе понадобятся защита при передаче и хранении, привязка к клиенту, политика срока жизни и атомарность обмена. Конкретные меры зависят от типа клиента и модели угроз.

От запроса к API до обновления пары токенов

В обмене участвуют клиентское приложение, сервер авторизации и API заказов. Сервер авторизации выдаёт токены, а API проверяет доступ к ресурсу. Эти роли могут быть реализованы отдельными сервисами или в одном приложении.

  1. После успешного предоставления доступа сервер авторизации выдаёт клиенту access token и, если предусмотрено, refresh token. В примере access имеет формат подписанного JWT, а refresh — случайная непрозрачная строка.
  2. Клиент отправляет access token в Authorization: Bearer на API заказов. API проверяет подпись, iss, aud, exp, назначение и требуемое разрешение.
  3. Когда access token истекает, API отклоняет его. Для bearer-токенов RFC 6750 описывает 401 Unauthorized с ошибкой invalid_token.
  4. Клиент обращается к серверу авторизации с refresh token. При допустимом обмене сервер выдаёт новый access token и, в схеме с ротацией, новый refresh token. Клиент заменяет оба сохранённых значения.
  5. После этого клиент может повторить исходный запрос с новым access token. Старый refresh token использовать нельзя: его повторное предъявление может привести к отзыву активной цепочки обновления.

Для трёх запросов токены используются по-разному:

ЗапросАдресатЧто предъявляетсяНазначение
GET /ordersAPI заказовAccess tokenПолучить защищённый ресурс
POST /tokenСервер авторизацииRefresh tokenПолучить новый access token
GET /orders повторноAPI заказовНовый access tokenПовторить операцию

POST /token обозначает типичный OAuth token endpoint, а не работающий адрес учебной программы. В реальном OAuth-обмене клиент указывает grant_type=refresh_token и соблюдает требования к аутентификации своего типа. Правила такого запроса описаны в RFC 6749.

Не каждую ошибку 401 следует автоматически исправлять обновлением: причина может быть иной, например отзыв доступа или неверная конфигурация клиента. Ошибка 403 из-за недостаточных разрешений обычно не устраняется заменой истёкшего токена. Повторять запрос после обновления тоже нужно осмотрительно: для операций с побочными эффектами необходима политика повторов и идемпотентности.

Пример на Python: проверка JWT и ротация refresh token

В репозитории с примером показаны три действия: проверка access JWT, обмен refresh token на новую пару и отказ при повторном предъявлении старого значения. Ссылка закреплена за конкретным коммитом, поэтому код и зависимости можно воспроизвести в одной версии.

jwt_example/
├── access_tokens.py   # выпуск JWT и политика проверки
├── domain.py          # типизированные claims, ошибки и пара токенов
└── refresh_tokens.py  # ротация, replay и отзыв семьи
article_demo.py        # короткий положительный сценарий
tests/                 # 18 позитивных и негативных проверок

Access token в примере — подписанный HS256 JWT с адресатом orders-api, издателем xnova-demo-auth и разрешением orders:read. Метод AccessTokenService.validate() сначала проверяет токен библиотекой PyJWT, затем — назначение, разрешение и типы полей, возвращая объект AccessClaims. Право читать конкретный заказ этот метод не проверяет.

Refresh token остаётся случайной непрозрачной строкой. Учебный сервер хранит только её SHA-256-отпечаток, при успешном обмене выдаёт новую пару, а повтор уже использованного значения распознаёт как replay и отзывает текущее значение этой семьи. За ротацию и выявление повторного использования отвечает RotatingRefreshSession. Все операции выполняются в памяти одного процесса.

Главный сценарий намеренно короткий:

from jwt_example import AccessTokenService, RefreshRejected, RefreshRejection, RotatingRefreshSession

access_tokens = AccessTokenService()
refresh_session = RotatingRefreshSession(access_tokens)

original = refresh_session.begin()
assert access_tokens.validate(original.access).subject == "user-42"
print("access: verified")

replacement = refresh_session.exchange(original.refresh)
assert access_tokens.validate(replacement.access).audience == "orders-api"
print("refresh: rotated")

try:
    refresh_session.exchange(original.refresh)
except RefreshRejected as error:
    assert error.reason is RefreshRejection.REPLAY
    print("refresh_replay: rejected")
else:
    raise AssertionError("spent refresh token was accepted")

assert refresh_session.revoked
print("refresh_family: revoked")

Для самостоятельного запуска на Python 3.12 клонируйте репозиторий, переключитесь на указанный коммит и установите зависимости:

git clone https://github.com/xnova-tech/jwt-access-refresh-example
cd jwt-access-refresh-example
git checkout 69315e5cf84c9e9789d572e060a67326458ec731
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
python article_demo.py
python -m pytest -q

Вывод demo:

access: verified
refresh: rotated
refresh_replay: rejected
refresh_family: revoked

Первое сообщение появляется после проверки подписи, iss, aud, exp и локальных требований к разрешению. Тесты дополнительно отклоняют изменённую подпись, истёкший JWT, чужие issuer и audience, неподдерживаемый алгоритм, неправильные scope и назначение, отсутствие exp и неверные типы claims. Для refresh token они проверяют успешную ротацию, неизвестное значение, replay и отзыв активной семьи.

HS256 с открытым учебным ключом используется только для автономного примера. Не переносите этот ключ в клиентское приложение и не используйте его в реальной системе. Проект не реализует OAuth-сервер, HTTP/TLS, аутентификацию клиента, постоянное хранилище, конкурентные обмены, срок жизни refresh token и объектную авторизацию. В production обмен refresh token должен быть атомарным, а ключи и токены — защищены в соответствии с моделью угроз.

Ошибки, из-за которых проверка токенов становится небезопасной

Доверять декодированным данным без проверки токена. Декодирование base64url не требует ключа. Любой может сконструировать JSON с sub=admin; право доступа появляется только после проверки подписи, назначения токена и политики API. Просмотр без проверки допустим при диагностике, но не при принятии решения о доступе.

Принимать алгоритм из заголовка без ограничений. Поле alg присылает клиент вместе с токеном. Если проверяющая сторона доверяет ему без собственного списка разрешённых алгоритмов, возможны уязвимости проверки подписи. У RFC 8725 есть отдельные требования к выбору алгоритма и проверке ключей. В коде выше список задан явно: algorithms=["HS256"].

Проверять только подпись и exp. Корректно подписанный JWT, адресованный другому сервису, должен быть отклонён по aud. Токен для другого сценария нельзя принимать как access token. Даже подходящий по claims токен не даёт автоматического доступа к любому заказу: принадлежность объекта и разрешения проверяются независимо.

Передавать refresh token каждому API. Серверу ресурсов он не нужен. Раскрытие refresh token опасно тем, что с его помощью можно получать новые access token. Используйте его только в предусмотренном обмене с сервером авторизации и защищайте от утечек.

Забывать о повторном использовании refresh token. Серверу нужно отличать предъявление неизвестного значения от повторного использования ранее действительного токена. В учебной модели повтор старого значения приводит к отзыву текущего токена в этой цепочке. При нескольких одновременных запросах к обновлению эти проверки и замена состояния должны быть атомарными, иначе законный клиент может выглядеть как повторно использующий токен.

Считать удаление refresh token немедленным отзывом access token. Самодостаточный JWT может оставаться действительным до exp, если API не проверяет актуальное состояние доступа. Для немедленной блокировки нужен отдельный механизм, например серверная проверка состояния или отзыв, рассмотренный в RFC 7009.

Способ хранения зависит от типа клиента: для браузера важны XSS, cookie-атрибуты и CSRF; для мобильного или серверного приложения — свои механизмы защиты. Без модели угроз нельзя объявить один способ хранения универсально безопасным.

Итог: что определяет JWT, а что решает приложение

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

Access token предъявляют API для доступа к ресурсу, refresh token — серверу авторизации для обновления доступа. В нашем примере первый имеет формат JWT, второй — непрозрачная строка. Формат каждого токена выбирают в соответствии с архитектурой и требованиями безопасности.

Учебный пример показывает один маршрут с ротацией и обнаружением повторного использования старого refresh token. Он не демонстрирует полный OAuth-поток, работу реального API или мгновенный отзыв уже выданных access token. В частности, если API проверяет самодостаточный JWT только локально, то удаление refresh token не прекращает действие такого JWT до окончания его срока. Это ограничение необходимо учитывать при выборе времени жизни и механизма отзыва.

Для своего API зафиксируйте ожидаемые iss и aud, необходимые разрешения, сроки действия токенов и правила обновления. Затем отдельно проверьте отказ для чужого адресата, истёкшего access token, недостающего разрешения и повторно использованного refresh token. Это даст проверяемые критерии вместо общего требования «использовать JWT».

Источники