
JWT без мифов: подпись, claims, проверка и безопасная сессия
Подробно разбираем JWT: JWS, base64url, claims, алгоритмы, issuer и audience, JWKS, ротацию ключей, access и refresh tokens, отзыв, cookies, XSS и CSRF.
Содержание
JWT, JSON Web Token, — компактный формат передачи набора утверждений, или claims. Чаще под JWT имеют в виду подписанный JWS из трёх частей: header, payload и signature. Получатель проверяет подпись и правила claims, не запрашивая исходные данные у выдавшего сервиса для каждой операции.
Сам формат не решает авторизацию, отзыв сессий и безопасное хранение в браузере. Большинство уязвимостей появляется не из-за математики подписи, а из-за неполной проверки: доверия алгоритму из токена, пропущенного audience, длинного срока жизни или утечки ключа.
Три части подписанного токена
Compact JWS выглядит как три base64url-фрагмента, разделённые точками:
header.payload.signature.
Header сообщает тип криптографической операции и algorithm. Payload содержит claims. Signature вычисляется над точным представлением первых двух частей.
Base64url — кодирование байтов символами, безопасными для URL. Это не шифрование. Любой, кто получил обычный JWT, способен декодировать header и payload. Поэтому туда не помещают пароль, секрет API, платёжные данные и лишние персональные сведения.
Подпись отвечает на два вопроса: изменились ли данные и обладал ли подписывающий нужным ключом. Она не доказывает, что токен уместен для конкретного API, пока не проверены issuer, audience и время.
JWS и JWE решают разные задачи
JWS подписывает содержимое. JWE шифрует его для получателя. Оба формата входят в семейство JOSE, но обычная авторизация чаще использует JWS поверх TLS.
Шифрование token не устраняет необходимость подписи, корректного управления ключами и минимизации данных. Оно усложняет диагностику и ротацию. Если API не должен передавать claim клиенту, часто лучше вообще не включать его.
TLS защищает token по сети. JWS позволяет проверить содержимое после получения. Эти уровни дополняют друг друга.
Registered claims задают контекст
Стандарт описывает известные имена:
iss— issuer, кто выдал token;sub— subject, идентификатор субъекта;aud— audience, для какого получателя token;exp— время окончания;nbf— не принимать раньше указанного времени;iat— время выдачи;jti— уникальный идентификатор.
Наличие claim не гарантирует его смысл. Приложение должно знать ожидаемый issuer и точный audience. Если сервис B принимает token, предназначенный сервису A, корректная подпись превращается в межсервисную уязвимость.
exp, nbf и iat обычно выражаются NumericDate. Проверяющий допускает небольшой clock skew, но не часы. Системное время узлов нужно синхронизировать.
Пользовательские роли и permissions требуют схемы и владельца. Claim admin: true не имеет значения вне согласованного контракта.
Алгоритм выбирает конфигурация, а не вход
В header находится alg, но сервер не должен слепо доверять этому значению. Он обязан заранее разрешить конкретный набор алгоритмов для данного issuer и типа token. RFC 8725 прямо требует algorithm verification.
Симметричный HMAC использует общий secret для подписи и проверки. Любой сервис с правом проверки одновременно способен выпускать token. Это приемлемо внутри узкой доверенной границы, но плохо масштабируется между независимыми получателями.
Асимметричная схема использует private key у issuer и public key у проверяющих. Получатели проверяют, но не могут подписывать. Private key никогда не передают API.
Известный класс ошибки — confusion между HMAC и RSA/ECDSA, когда библиотека принимает public key как HMAC secret. Защита — современная библиотека, жёсткий allowlist algorithms и ключ, привязанный к конкретному алгоритму.
alg: none нельзя принимать там, где ожидается подписанный token.
Порядок безопасной проверки
Принимающая сторона должна:
- ограничить размер и корректно разобрать compact-формат;
- разрешить ожидаемый алгоритм;
- выбрать доверенный ключ, не позволяя произвольный URL из header;
- проверить криптографическую подпись;
- сверить точный
iss; - убедиться, что
audсодержит этот сервис; - проверить
exp,nbfи политикуiat; - проверить тип token и обязательные claims;
- применить авторизацию к ресурсу.
Используйте библиотеку, а не собственную криптографию. Псевдокод подчёркивает, что настройки приходят от приложения:
const claims = verifyJwt(token, {
algorithms: ["EdDSA"],
issuer: "https://identity.example.com",
audience: "orders-api",
requiredClaims: ["sub", "exp", "iat"],
clockToleranceSeconds: 30
});
authorize(claims.sub, "orders:read", orderId);
Имена опций различаются по библиотекам; не копируйте пример буквально. Успех означает не только отсутствие exception, но и разрешение конкретного действия над конкретным объектом. Роль пользователя не даёт автоматически доступ к чужому заказу.
kid и JWKS
kid в header помогает выбрать ключ при ротации. Issuer может публиковать публичные ключи как JWKS. Проверяющий кэширует набор и обновляет его по контролируемому URL.
Нельзя превращать kid в путь к файлу, SQL-фрагмент или произвольный сетевой адрес. Не доверяйте jku и x5u из входного token без строгой политики: это может привести к SSRF или доверию ключу атакующего.
Ротация проходит с перекрытием: новый private key начинает подписывать, старый public key остаётся доступным до истечения всех выданных им tokens и допустимого запаса. Срочная компрометация требует немедленного удаления доверия и отдельного плана принудительной повторной аутентификации.
Access и refresh token
Access token предъявляется API и должен жить недолго. Refresh token используется для получения нового access и имеет более строгую защиту. Это разные credentials; один длинный JWT не заменяет пару.
Refresh rotation выдаёт новый refresh при каждом использовании и помечает старый использованным. Повтор старого token сигнализирует возможную кражу, после чего семейство сессии отзывают. Для этого issuer всё равно хранит состояние.
Сроки выбирают по риску и удобству. Чем дольше access token, тем больше окно после кражи; слишком короткий срок увеличивает refresh-трафик и чувствительность к сбоям identity-сервиса.
Отзыв требует компромисса
Самодостаточный JWT действителен до exp, если ключ остаётся доверенным. Способы сократить окно:
- короткий access token;
- denylist по
jti; - server-side версия сессии или пользователя;
- introspection для opaque token;
- ротация signing key при крупном инциденте.
Denylist на каждый запрос возвращает сетевую или локальную проверку состояния, уменьшая преимущество автономной валидации. Иногда обычная server-side session проще и безопаснее.
Logout в браузере удаляет локальный credential, но не обязательно отзывает уже скопированный token. Интерфейс не должен обещать больше, чем реализует backend.
Хранение в браузере
localStorage доступен JavaScript страницы. При XSS вредоносный скрипт может прочитать token и унести его. HttpOnly cookie недоступна JavaScript, но автоматически отправляется браузером, поэтому возникает CSRF.
Для cookie используйте Secure, HttpOnly, подходящий SameSite, узкие Domain/Path и CSRF-защиту для изменяющих запросов. Значение SameSite зависит от реальной схемы доменов и внешних переходов.
Хранение access token только в памяти уменьшает долговременную кражу из storage, но перезагрузка требует восстановления сессии. Backend-for-Frontend может держать tokens на сервере и выдавать браузеру обычную cookie-сессию.
Ни один вариант не компенсирует XSS полностью: скрипт может совершать действия от имени пользователя, даже не прочитав HttpOnly cookie. Нужны экранирование вывода, CSP, обновление зависимостей и отсутствие опасного HTML.
JWT не заменяет authorization
Authentication отвечает «кто это», authorization — «что ему разрешено». Claim с ролью может быть входом в решение, но API сверяет действие, tenant и ресурс.
Права меняются быстрее срока token. Если сотрудника отключили, старый access token может действовать до exp. Для критичных операций проверяйте актуальное состояние либо используйте особенно короткое окно.
Не передавайте сотни разрешений в payload: token раздувает каждый запрос, а cookies имеют практические лимиты. Большой JWT создаёт ошибки заголовков в proxy и лишний трафик.
Межсервисное использование
Один универсальный token для всех API опасен. Делите audience и scopes. Сервис, получивший пользовательский token, не должен безусловно пересылать его во все зависимости.
Для service-to-service применяйте отдельную identity workload, короткоживущие credentials и свой audience. Сохраняйте trace пользователя отдельно, не смешивая делегированные полномочия с правами самого сервиса.
Логи должны содержать issuer, subject в допустимой форме, jti или correlation ID, причину отказа и время, но не полный token. Полный JWT является credential и может содержать персональные claims.
Типичные ошибки
Наиболее опасны:
- декодировать payload без verify;
- принимать algorithm из token без allowlist;
- не проверять audience;
- использовать один key для разных сред и типов token;
- хранить секрет в payload;
- выдавать access на месяцы;
- использовать слабый HMAC secret;
- доверять внешнему URL ключа из header;
- считать logout мгновенным отзывом;
- путать role с доступом к объекту.
Тесты должны включать изменённую подпись, неверные iss/aud, просроченный и будущий token, неподдерживаемый alg, неизвестный kid, отсутствие обязательного claim и попытку доступа к чужому ресурсу.
Когда JWT уместен
JWT удобен, когда несколько сервисов проверяют token локально, а issuer и ключи управляются централизованно. Он подходит для короткоживущих access tokens и федеративных сценариев.
Для обычного сайта с одним backend server-side session часто проще отзывать, меньше по размеру и не раскрывает claims клиенту. Opaque token с introspection удобен, когда актуальное состояние важнее автономной проверки.
Выбирайте не формат, а модель доверия: кто выпускает, кто проверяет, как ротируются ключи, как быстро отзывается доступ и что происходит при недоступности identity-сервиса.
Источники
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
JWT шифрует данные внутри токена?
Обычный подписанный JWT в форме JWS не шифрует payload: его base64url-представление может прочитать любой получатель. Подпись защищает целостность и подлинность при корректной проверке.
Можно ли отозвать JWT до истечения срока?
Самодостаточный токен не обращается к серверной сессии автоматически. Для досрочного отзыва применяют короткий срок access token, ротацию refresh token, denylist, версию сессии или проверку состояния на сервере.
Где безопаснее хранить access token в браузере?
Универсального варианта нет. HttpOnly Secure cookie уменьшает доступ из JavaScript, но требует защиты от CSRF; хранение в JavaScript-памяти уменьшает автоматическую отправку, но чувствительно к XSS. Модель выбирают вместе с архитектурой.


