Большинство багов JWT в TypeScript находятся не в токене, а в конфигурации проверки. Отсутствующее ограничение алгоритма, непроверенный claim издателя или короткоживущий секрет могут незаметно подорвать аутентификацию так, что это проявится только в продакшене. Это руководство проходит по всем аспектам корректности конфигурации JWT в TypeScript: алгоритмы, валидация claims, гигиена секретов, обработка срока действия и браузерные инструменты, ускоряющие отладку.
Что охватывает валидация конфигурации JWT
JSON Web Token (JWT) — компактная, безопасная для URL строка из трёх частей, закодированных в Base64URL и разделённых точками: заголовок, объявляющий алгоритм и тип токена, payload с claims и подпись, связывающая их. Валидация JWT означает подтверждение, что все три части целы и что claims соответствуют требованиям вашего приложения, — а не только что подпись математически корректна.
Два уровня валидации JWT
Криптографическая валидация подтверждает подпись: сервер проверяет, что токен подписан ожидаемым ключом и не был изменён. Валидация конфигурации идёт дальше: она проверяет, что токен выпущен правильным издателем, предназначен именно для этого сервиса, не истёк и несёт ожидаемые пользовательские claims. Большинство уязвимостей JWT возникают из-за неполной валидации конфигурации, а не из-за сломанной криптографии.
- Алгоритм (`alg`) - должен в точности соответствовать конфигурации сервера; никогда не выводите его из заголовка токена
- Срок действия (`exp`) - токен не должен пройти отметку истечения, с учётом допуска по часам
- Не-раньше (`nbf`) - токен нельзя использовать до его самого раннего валидного времени
- Издатель (`iss`) - токен должен происходить из вашего доверенного сервиса аутентификации
- Аудитория (`aud`) - токен должен быть предназначен для конкретно этой API или сервиса
- Пользовательские claims - роль, scope, ID тенанта или любые прикладные поля, от которых зависит ваша логика
Note
Проверки алгоритма и конфигурации ключей
Конфигурация алгоритма — самый критичный для безопасности параметр в проверке JWT. Ошибка в нём открывает класс атак, полностью обходящих аутентификацию. Библиотеки JWT для TypeScript дают инструменты, чтобы enforce это правильно, — но только если вы используете их явно.
HS256 против RS256 - выбор правильного алгоритма
| Свойство | HS256 (симметричный) | RS256 (асимметричный) |
|---|---|---|
| Тип ключа | Общий секрет (один ключ для подписи и проверки) | Пара ключей RSA (приватный для подписи, публичный для проверки) |
| Распределение ключей | Каждый проверяющий хранит секрет | Только издатель хранит приватный ключ |
| Безопасность для нескольких сервисов | ✗ Рискованно - все проверяющие могут подделать токены | ✓ Проверяющие хранят только публичный ключ |
| Поддержка OIDC / JWKS | ✗ Неприменимо | ✓ Публичные ключи раздаются через JWKS-эндпоинт |
| Производительность | ✓ Быстро (HMAC) | ✗ Медленнее (математика RSA) |
| Лучше всего для | Внутренние инструменты, API одного сервиса | Продакшен-API, распределённые системы, OIDC |
Атака подмены алгоритма - и как её предотвратить
Подмена алгоритма происходит, когда сервер читает поле `alg` из заголовка JWT, чтобы решить, как проверять токен, вместо принудительного задания алгоритма собственной конфигурацией. Атакующий меняет заголовок, подставляя `HS256` вместо `RS256`, и подписывает токен публичным ключом сервера, использованным как HMAC-секрет. Неправильно настроенный сервер примет его как валидный. Исправление — одна строка кода, но она должна присутствовать.
// ✗ Vulnerable - algorithm inferred from token header
jwt.verify(token, publicKey);
// ✓ Correct - algorithm enforced from server configuration
jwt.verify(token, publicKey, { algorithms: ['RS256'] });Warning
Проверка стойкости ключа для HS256
При использовании HS256 секрет должен быть не короче 256 бит (32 байта), чтобы соответствовать размеру выхода SHA-256. Короткие секреты - менее 32 символов, словарные слова или статические строки вроде `"secret"` или `"development"` - тривиально подбираются перебором инструментами вроде `hashcat` или `jwt_tool`. Генерируйте секреты криптостойким источником случайности: `crypto.randomBytes(32).toString('hex')` в Node.js даёт шестнадцатеричную строку из 64 символов, удовлетворяющую минимальному требованию энтропии.
Валидация стандартных claims JWT в TypeScript
Спецификация JWT определяет набор стандартных зарегистрированных claims, которые должна понимать каждая реализация. Библиотека `jsonwebtoken` автоматически проверяет несколько из них, когда вы передаёте правильные опции, - но ключевое слово «когда передаёте». Без явной конфигурации большинство проверок claims молча пропускается.
Claims exp, nbf и iat
Claim exp (истечение) — метка времени Unix, после которой токен больше не валиден. Библиотека jsonwebtoken проверяет exp по умолчанию в jwt.verify(). Однако рассинхронизация часов между издателем токена и проверяющим может привести к отклонению валидных токенов — частая причина ошибок «токен истёк» в распределённых системах, где серверные часы расходятся на секунды. Передайте clockTolerance, чтобы допустить небольшое окно: clockTolerance = 30 принимает токены до 30 секунд после значения exp.
interface JwtPayload {
sub: string;
iss: string;
aud: string;
exp: number;
iat: number;
role: 'admin' | 'user';
}
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 30, // seconds of clock skew to tolerate
}) as JwtPayload;Claims iss и aud
Claim `iss` (издатель) идентифицирует происхождение токена. Claim `aud` (аудитория) идентифицирует предполагаемого получателя. Оба опциональны в спецификации JWT, но критичны на практике. Без валидации `iss` любой сервис, способный выпускать валидные токены вашим ключом подписи, может аутентифицироваться в вашей API. Без валидации `aud` токен, выпущенный для вашего мобильного приложения, может быть переиспользован против админ-API. Передавайте обе опции в `jwt.verify()`, чтобы библиотека принуждала их как жёсткие требования, а не информационные поля.
Tip
Реализация валидации JWT в TypeScript
Полная функция проверки JWT в TypeScript обрабатывает криптографическую валидацию, валидацию claims и классификацию ошибок в одном месте. Вот как её структурировать - с четырьмя шагами, соответствующими схеме HowTo.
Убедитесь, что алгоритм соответствует типу ключа
Прежде чем писать код проверки, подтвердите вашу комбинацию алгоритма и ключа. RS256 требует приватный RSA-ключ для подписи и соответствующий публичный ключ для проверки. HS256 требует одинаковый общий секрет с обеих сторон. Их смешивание вызывает исключения времени выполнения, которые трудно диагностировать. Храните публичный ключ или общий секрет в переменных окружения - никогда не хардкодьте их в исходных файлах.
Явно валидируйте claims exp и nbf
Всегда задавайте `clockTolerance`, чтобы обрабатывать небольшие расхождения часов между сервисами. Значение 30 секунд — разумный вариант по умолчанию для большинства распределённых систем. Отлаживая `TokenExpiredError` в продакшене, логируйте `payload.exp * 1000` и `Date.now()` вместе - это показывает, на сколько миллисекунд токен прошёл срок, и отличает реальное истечение от проблемы синхронизации часов между сервисом аутентификации и API-сервером.
Проверяйте claims iss и aud по ожидаемым значениям
Передайте опции `issuer` и `audience` в `jwt.verify()`, чтобы библиотека отклоняла токены с несовпадающими значениями до выполнения вашего прикладного кода. Если в системе несколько валидных аудиторий (например, и `api.example.com`, и `admin.example.com`), передайте массив: `audience: ['api.example.com', 'admin.example.com']`. Библиотека принимает токен, если claim `aud` совпадает с любым элементом массива.
Протестируйте конфигурацию с JWT-декодером
Перед запуском тест-сьюта TypeScript вставьте образец токена из среды разработки или staging в JWT-декодер и валидатор. Визуально подтвердите, что каждый claim - `alg`, `exp`, `iss`, `aud` и ваши пользовательские claims - соответствует опциям вашего `jwt.verify()`. Эта минутная проверка ловит расхождения между содержимым токена и ожиданиями кода до того, как вы потратите время на отладку в тестовой среде.
JWT-декодер и валидатор
Декодируйте JWT-токены и мгновенно проверяйте все claims, поля заголовка и распространённые проблемы безопасности прямо в браузере - без регистрации и загрузки на сервер.
Частые ошибки конфигурации JWT
Вот ошибки конфигурации, которые чаще всего встречаются в реализациях JWT на TypeScript. Каждая из них молчит при старте и проявляется только как сбой аутентификации или инцидент безопасности в продакшене.
Использование `jwt.decode()` вместо `jwt.verify()`
Функция `jwt.decode()` извлекает payload без проверки подписи. Она полезна для инспекции уже доверенного токена - например, извлечения ID пользователя из токена, уже проверенного middleware. Она не заменяет `jwt.verify()`. Код, который берёт claims через `jwt.decode()` и на их основе принимает решения авторизации, принимает непроверенные токены. Это полный обход аутентификации.
Игнорирование типа ошибки в блоках catch
Библиотека `jsonwebtoken` выбрасывает три разных типа ошибок: `JsonWebTokenError` (неисправный токен или неверная подпись), `TokenExpiredError` (после claim `exp`) и `NotBeforeError` (до claim `nbf`). Ловить все ошибки как общий `Error` и возвращать `401 Unauthorized` во всех случаях — потеря диагностической информации. Обрабатывайте каждый тип отдельно и возвращайте конкретные сообщения - «токен истёк» против «неверный токен» - чтобы клиенты и системы мониторинга отличали проблемы конфигурации от настоящих попыток атаки.
Отсутствие ротации секретов и пар ключей
Долгоживущие секреты подписи накапливают риск со временем. Секрет, который никогда не ротировался, означает, что при компрометации секрета остаются валидными все выпущенные с ним токены. Реализуйте поле идентификатора ключа (`kid`) в заголовке JWT, чтобы проверяющий мог найти правильный публичный ключ через JWKS-эндпоинт. Этот паттерн позволяет ротацию ключей без аннулирования токенов, подписанных прежним ключом, - каждый токен несёт ссылку на конкретный ключ, которым подписан. Инспектор JWK помогает валидировать вывод JWKS-эндпоинта и обнаруживать материал приватных ключей, который не должен быть публично доступен.
Warning
Принятие алгоритма `none`
Алгоритм `none` создаёт неподписанный JWT - любой payload с валидной структурой проходит проверку. Ранние версии JWT-библиотек принимали `none` по умолчанию. Современные библиотеки его отклоняют, но только когда вы явно указываете разрешённые алгоритмы в опциях проверки. Всегда указывайте `algorithms: ['RS256']` (или ваш конкретный алгоритм), чтобы отклонение `none` было явным и устойчивым к смене версий библиотеки.
Отладка проблем JWT браузерными инструментами
Написать тест для воспроизведения ошибки JWT часто медленнее, чем напрямую осмотреть токен. Браузерные инструменты позволяют за секунды изучить claims, состояние истечения и структуру ключей токена - без локальной среды, без выполнения кода и без отправки чувствительных данных стороннему сервису.
Декодирование и инспекция claims
JWT-декодер и валидатор декодирует заголовок и payload любой строки JWT и представляет все claims в структурированном читаемом виде. Он проверяет распространённые проблемы безопасности - отсутствие `exp`, слабый алгоритм, отсутствие `aud` - и помечает их понятными диагностиками. Вставьте любой токен из среды разработки, staging или продакшена и за десять секунд убедитесь, что claims соответствуют ожиданиям вашего вызова `jwt.verify()`.
Диагностика проблем истечения
Калькулятор обратного отсчёта истечения JWT читает claim `exp` из любого токена и показывает точное оставшееся время жизни или время с момента истечения как в UTC, так и в локальном времени. Когда пользователь сообщает «токен истёк», а ваши логи показывают, что токен ещё должен быть валиден, вставьте его в калькулятор. Сравнение меток времени с точностью до миллисекунд выявит, в чём дело: реальное истечение, расхождение часов между сервисами или значение `exp`, сохранённое в миллисекундах вместо секунд, - на удивление частый баг с множителем 1000.
Инспекция наборов ключей JWKS
При валидации токенов от OIDC-провайдера или любого сервиса, публикующего JWKS-эндпоинт, Инспектор JWK разбирает и валидирует структуру набора ключей. Он проверяет наличие у каждого ключа обязательных полей (`kty`, `use`, `alg`, `kid`), валидирует тип ключа и кривую для EC-ключей и помечает материал приватных ключей, который не должен быть публично доступен. Вставьте JSON из вашего JWKS-эндпоинта прямо в инструмент или укажите URL для загрузки.
Калькулятор обратного отсчёта истечения JWT
Вычислите точный обратный отсчёт истечения JWT по claim exp - увидьте оставшееся время или время с момента истечения в UTC и локальном времени, без кода.
Лучшие практики валидации JWT
Корректно настроенная функция проверки JWT необходима, но недостаточна для безопасной аутентификации. Эти практики дополняют картину для продакшен-сервисов на TypeScript.
Используйте типизированный интерфейс payload
Определите TypeScript-интерфейс для вашего JWT-payload и приводите результат проверки к нему. Это даёт безопасность на этапе компиляции для имён claims и типов значений - неверно написанное имя claim (`userId` против `user_id`) станет ошибкой TypeScript вместо тихого `undefined` в рантайме. Храните интерфейс в общем модуле типов, чтобы он был согласован во всех сервисах, проверяющих тот же формат токена.
Короткие сроки жизни токенов с refresh-токенами
Токены доступа должны жить недолго - от 15 минут до 1 часа для большинства API. Долгоживущие токены доступа (дни, недели) расширяют окно, в котором скомпрометированный токен может использоваться. Используйте отдельный механизм refresh-токенов с долгим сроком для поддержания сессии. Refresh-токен ротируется при каждом использовании, а списки отзыва нужны только для refresh-токенов - не для токенов доступа, - пока сроки жизни остаются короткими.
Централизуйте логику проверки
Пишите проверку JWT в одном месте - функция middleware или общая утилита - и используйте её везде. Дублированная логика проверки провоцирует дрейф конфигурации: один эндпоинт проверяет `aud`, другой забывает, и несоответствие эксплуатируется раньше, чем кто-то заметит. Express-middleware, guards NestJS и обработчики маршрутов Next.js имеют чистые паттерны централизации проверок аутентификации. Поместите опции `algorithms`, `issuer` и `audience` в единый объект конфигурации, импортируемый по всему коду.
Настройте проверку один раз, применяйте везде. Единственная опция JWT, которая может отличаться между эндпоинтами, — ожидаемая аудитория.
Tip
Key takeaways
- Всегда явно передавайте `algorithms: ['RS256']` (или ваш конкретный алгоритм) в `jwt.verify()` - никогда не позволяйте библиотеке выводить его из заголовка токена.
- Валидируйте claims `iss` и `aud` в каждом вызове проверки; их пропуск позволяет токенам из других сервисов аутентифицироваться в вашей API.
- Используйте `clockTolerance` для расхождений часов между распределёнными сервисами и логируйте метки `exp` вместе с `Date.now()` при отладке ошибок истечения.
- Никогда не используйте `jwt.decode()` для решений авторизации - он полностью пропускает проверку подписи и принимает любой payload токена.
- JWT-декодер и валидатор позволяет осмотреть все claims и пометить проблемы конфигурации за секунды, не пиша и не запуская код.
- RS256 предпочтительнее HS256 для продакшен-API - он устраняет риск распространения общего секрета и поддерживает ротацию ключей на базе JWKS.
- Держите сроки жизни токенов доступа короткими (15-60 минут) и централизуйте всю логику проверки в единой middleware или утилите, чтобы предотвратить дрейф конфигурации.