Always getting invalid signature in jwt.io

Extract the first key from the keys array in the JSON returned by the https://example.com/.well-known/jwks , and paste it in the first textbox of VERIFY SIGNATURE section of jwt.io page. Of course, example.com is the domain where you hosted your OpenIddict auth server. Could also be something like https://example.com/my/auth/server/ .
The whole story:
When you paste the JWT in jwt.io, it does this:
- decodes the token, and shows the header and the payload on the right
- tries to validate the signature
If the step 1. fails to decode the payload, that’s because the token is encoded. To solve this problem, modify the OpenIddict config by adding .DisableAccessTokenEncryption();

The step 2, signature validation, is done by getting the issuer iss field from the PAYLOAD section:
and uses it as the base URI to invoke the /.well-known/openid-configuration , which includes the JWKS uri, which looks like «jwks_uri»: «https://example.com/.well-known/jwks»
jwt.io can fail to get this data for example:
- if you’re testing in https://localhost , which isn’t accessible from internet, just like the https://localhost:5001 of this example
- because the request is rejected by any other reason (unknown domain, firewall. )
If this is the case, there is an option to solve the problem: paste the appropriate string in the upper textbox of VERIFY SIGNATURE section, which has this placeholder:
Public key in SPKI, PKCS #1, X.509 certificate, or JWK string format.
What is the right string to paste there? It’s easy if you take into account 2 details:
- you can get the JSON with this info from the aforementioned https://example.com/.well-known/jwks endpoint
- the info returned is a JWKS string, but a JWK is required.
So, invoke the enpoint, get the JWKS which looks like this:
and extract the JWK which is simply the first entry in the «keys» array, i.e
Paste this value in the textbox, and you’ll get the blue «Signature verified» message, as you can see at the bottom of the first snapshot.
NOTE: depending on the configuration ( AddEphemeralSigningKey() , AddDevelopmentSigningCertificate() , etc.), the JWKS keys can have more or less properties, but it should work anyway.
Пять простых шагов для понимания JSON Web Tokens (JWT)

Представляю вам мой довольно вольный перевод статьи 5 Easy Steps to Understanding JSON Web Tokens (JWT). В этой статье будет рассказано о том, что из себя представляют JSON Web Tokens (JWT) и с чем их едят. То есть какую роль они играют в проверке подлинности пользователя и обеспечении безопасности данных приложения.
Для начала рассмотрим формальное определение.
JSON Web Token (JWT) — это JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д. и подписи (signature).
Кстати, правильно JWT произносится как /dʒɒt/
Простыми словами, JWT — это лишь строка в следующем формате header.payload.signature .
Предположим, что мы хотим зарегистрироваться на сайте. В нашем случае есть три участника — пользователь user , сервер приложения application server и сервер аутентификации authentication server . Сервер аутентификации будет обеспечивать пользователя токеном, с помощью которого он позднее сможет взаимодействовать с приложением.

Приложение использует JWT для проверки аутентификации пользователя следующим образом:
- Сперва пользователь заходит на сервер аутентификации с помощью аутентификационного ключа (это может быть пара логин/пароль, либо Facebook ключ, либо Google ключ, либо ключ от другой учетки).
- Затем сервер аутентификации создает JWT и отправляет его пользователю.
- Когда пользователь делает запрос к API приложения, он добавляет к нему полученный ранее JWT.
- Когда пользователь делает API запрос, приложение может проверить по переданному с запросом JWT является ли пользователь тем, за кого себя выдает. В этой схеме сервер приложения сконфигурирован так, что сможет проверить, является ли входящий JWT именно тем, что был создан сервером аутентификации (процесс проверки будет объяснен позже более детально).
Структура JWT
JWT состоит из трех частей: заголовок header , полезные данные payload и подпись signature . Давайте пройдемся по каждой из них.
Шаг 1. Создаем HEADER
Хедер JWT содержит информацию о том, как должна вычисляться JWT подпись. Хедер — это тоже JSON объект, который выглядит следующим образом:
header =
Поле typ не говорит нам ничего нового, только то, что это JSON Web Token. Интереснее здесь будет поле alg , которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256 , для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3). Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности.
Шаг 2. Создаем PAYLOAD
Payload — это полезные данные, которые хранятся внутри JWT. Эти данные также называют JWT-claims (заявки). В примере, который рассматриваем мы, сервер аутентификации создает JWT с информацией об id пользователя — userId.
payload =
Мы положили только одну заявку (claim) в payload. Вы можете положить столько заявок, сколько захотите. Существует список стандартных заявок для JWT payload — вот некоторые из них:
- iss (issuer) — определяет приложение, из которого отправляется токен.
- sub (subject) — определяет тему токена.
- exp (expiration time) — время жизни токена.
Эти поля могут быть полезными при создании JWT, но они не являются обязательными. Если хотите знать весь список доступных полей для JWT, можете заглянуть в Wiki. Но стоит помнить, что чем больше передается информации, тем больший получится в итоге сам JWT. Обычно с этим не бывает проблем, но все-таки это может негативно сказаться на производительности и вызвать задержки во взаимодействии с сервером.
Шаг 3. Создаем SIGNATURE
Подпись вычисляется с использование следующего псевдо-кода:
const SECRET_KEY = 'cAtwa1kkEy' const unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload) const signature = HMAC-SHA256(unsignedToken, SECRET_KEY)
Алгоритм base64url кодирует хедер и payload, созданные на 1 и 2 шаге. Алгоритм соединяет закодированные строки через точку. Затем полученная строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа.
// header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 // payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ // signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
Шаг 4. Теперь объединим все три JWT компонента вместе
Теперь, когда у нас есть все три составляющих, мы можем создать наш JWT. Это довольно просто, мы соединяем все полученные элементы в строку через точку.
const token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature) // JWT Token // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
Вы можете попробовать создать свой собственный JWT на сайте jwt.io.
Вернемся к нашему примеру. Теперь сервер аутентификации может слать пользователю JWT.
Как JWT защищает наши данные?
Очень важно понимать, что использование JWT НЕ скрывает и не маскирует данные автоматически. Причина, почему JWT используются — это проверка, что отправленные данные были действительно отправлены авторизованным источником. Как было продемонстрировано выше, данные внутри JWT закодированы и подписаны, обратите внимание, это не одно и тоже, что зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных. Таким образом закодирование и подпись данных не защищает их. С другой стороны, главная цель шифрования — это защита данных от неавторизованного доступа. Для более детального объяснения различия между кодированием и шифрованием, а также о том, как работает хеширование, смотрите эту статью. Поскольку JWT только лишь закодирована и подписана, и поскольку JWT не зашифрована, JWT не гарантирует никакой безопасности для чувствительных (sensitive) данных.
Шаг 5. Проверка JWT
В нашем простом примере из 3 участников мы используем JWT, который подписан с помощью HS256 алгоритма и только сервер аутентификации и сервер приложения знают секретный ключ. Сервер приложения получает секретный ключ от сервера аутентификации во время установки аутентификационных процессов. Поскольку приложение знает секретный ключ, когда пользователь делает API-запрос с приложенным к нему токеном, приложение может выполнить тот же алгоритм подписывания к JWT, что в шаге 3. Приложение может потом проверить эту подпись, сравнивая ее со своей собственной, вычисленной хешированием. Если подписи совпадают, значит JWT валидный, т.е. пришел от проверенного источника. Если подписи не совпадают, значит что-то пошло не так — возможно, это является признаком потенциальной атаки. Таким образом, проверяя JWT, приложение добавляет доверительный слой (a layer of trust) между собой и пользователем.
В заключение
Мы прошлись по тому, что такое JWT, как они создаются и как валидируются, каким образом они могут быть использованы для установления доверительных отношений между пользователем и приложением. Но это лишь кусочек пазла большой темы авторизации и обеспечения защиты вашего приложения. Мы рассмотрели лишь основы, но без них невозможно двигаться дальше.
Что дальше?
Подумаем о безопасности и добавим Refresh Token . Смотрите следующую мою статью на эту тему.
Полезные ссылки
- 5 Easy Steps to Understanding JSON Web Tokens (JWT)
- Securing React Redux Apps With JWT Tokens
- Зачем нужен Refresh Token, если есть Access Token?
Invalid jwt что значит
В последнее время все чаще можно встретить приложения, использующие для аутентификации пользователей механизмы JSON Web Tokens. Особую популярность JWT завоевал с ростом популярности микросервисной архитектуры: он возлагает задачу по обработке аутентификационных данных на сами микросервисы, а следовательно позволяет избежать различных ошибок авторизации, увеличить производительность и улучшить масштабируемость приложения.
Вместе с тем неправильное использование JWT может негативно сказаться на безопасности приложения. Мы приведем примеры использования JWT, разберем распространенные ошибки в реализации схем аутентификации с применением JWT, рассмотрим основные виды атак на эти схемы и дадим рекомендации по их предотвращению.
Формат JWT: описание
В этом разделе статьи мы расскажем, что такое JSON Web Tokens, из каких частей он состоит, как используется для аутентификации пользователей и в чем заключается преимущество JWT в сравнении с классической схемой аутентификации с использованием сессий.
Структура JWT
Согласно RFC-7519, JSON Web Tokens — один из способов представления данных для передачи между двумя или более сторонами в виде JSON-объекта.
Как правило, структурно JWT состоит из трех частей:
- header — заголовок,
- payload — полезная нагрузка,
- signature — подпись.
Бывают и исключения, когда в JWT отсутствует подпись. Подобный случай будет рассмотрен далее.
Заголовок и полезная нагрузка — обычные JSON-объекты, которые необходимо дополнительно закодировать при помощи алгоритма base64url. Закодированные части соединяются друг с другом, и на их основе вычисляется подпись, которая также становится частью токена.
В общем случае токен выглядит следующим образом:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
На рис. 1 можно увидеть, что токен состоит из трех частей, разделенных точками.

Рис. 1. JSON Web Token (пример с сайта jwt.io)
Красная часть — заголовок:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
В исходном виде:
Фиолетовая часть — полезная нагрузка:
eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ
В исходном виде:
Голубая часть — подпись:
ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
Рассмотрим структуру полей более подробно.
Заголовок
Заголовок — служебная часть токена. Он помогает приложению определить, каким образом следует обрабатывать полученный токен.
Эта часть, как было ранее упомянуто, является JSON-объектом и имеет следующий формат:
Здесь присутствуют следующие поля:
- typ — тип токена, например JWT;
- alg — алгоритм, использованный для генерации подписи.
Значение поля typ зачастую игнорируется приложениями, однако стандарт не рекомендует отказываться от него для обеспечения обратной совместимости.
Поле alg обязательно для заполнения. В приведенном случае был применен алгоритм HS256 (HMAC-SHA256), в котором для генерации и проверки подписи используется единый секретный ключ.
Для подписи JWT могут применяться и алгоритмы асимметричного шифрования, например RS256 (RSA-SHA256). Стандарт допускает использование и других алгоритмов, включая HS512, RS512, ES256, ES512, none и др.
Использование алгоритма none указывает на то, что токен не был подписан. В подобном токене отсутствует часть с подписью, и установить его подлинность невозможно.
Полезная нагрузка
В полезной нагрузке передается любая информация, которая помогает приложению тем или иным образом идентифицировать пользователя. Дополнительно могут передаваться определенные служебные поля, однако они не обязательны для заполнения, поэтому на них останавливаться мы не будем.
В нашем случае полезная нагрузка содержит следующий JSON-объект:
Здесь присутствуют следующие поля:
- id — уникальный идентификатор пользователя;
- username — имя пользователя;
- iat — служебное поле, время генерации токена в формате Unix time;
- role — роль пользователя, например admin, user, guest.
Поскольку набор полей в части полезной нагрузки произвольный, приложение может хранить в этой части практически любые данные. Например, для ускорения работы приложения в полезной нагрузке могут храниться Ф. И. О. пользователя, чтобы не запрашивать эти сведения каждый раз из базы данных.
Подпись
Подпись генерируется следующим образом.
Заголовок и полезная нагрузка кодируются при помощи алгоритма base64url, после чего объединяются в единую строку с использованием точки ( «.» ) в качестве разделителя.
Генерируется подпись (в нашем примере — с применением алгоритма HMAC-SHA256), которая добавляется к исходной строке так же через точку.
На псевдокоде алгоритм выглядит примерно так:
signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECRET_KEY ) JWT = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + base64UrlEncode(signature)
Получив JWT от пользователя, приложение самостоятельно вычислит значение подписи и сравнит его с тем значением, которое было передано в токене. Если эти значения не совпадут, значит, токен был модифицирован или сгенерирован недоверенной стороной, и принимать такой токен и доверять ему приложение не будет.
Подпись приведенного в пример токена можно проверить с использованием секретного ключа test (например, на сайте jwt.io).
Аутентификация с использованием JWT
Схема аутентификации с использованием JWT предельно проста.
Пользователь вводит свои учетные данные в приложении или доверенном сервисе аутентификации. При успешной аутентификации сервис предоставляет пользователю токен, содержащий сведения об этом пользователе (уникальный идентификатор, Ф. И. О., роль и т. д.).
При последующих обращениях токен передается приложению в запросах от пользователя (в cookie, заголовках запроса, POST- или GET-параметрах и т. д.).
Получив токен, приложение сперва проверяет его подпись. Убедившись, что подпись действительна, приложение извлекает из части полезной нагрузки сведения о пользователе и на их основе авторизует его.
Преимущества JWT
Перечислим преимущества использования JWT в сравнении с классической схемой аутентификации, использующей сессии.
Во-первых, подход с использованием токенов позволяет не хранить информацию обо всех выданных токенах, как при классической схеме. Когда пользователь обращается к приложению, он передает ему свой токен. Приложению остается только проверить подпись и извлечь необходимые поля из полезной нагрузки.
Во-вторых, приложению вообще не обязательно заниматься выдачей и валидацией токенов самостоятельно, зачастую для этих целей используется отдельный сервис аутентификации.
В-третьих, при использовании отдельного сервиса аутентификации становится возможным организовать единую точку входа в различные сервисы с одними и теми же учетными данными. Единожды пройдя процедуру аутентификации, пользователь сможет получить доступ со своим токеном к тем ресурсам, которые доверяют этому сервису аутентификации.
В-четвертых, как было указано ранее, приложение может хранить в части полезной нагрузки практически любые данные, что при грамотной архитектуре приложения может существенно увеличить производительность.
Благодаря перечисленным факторам схема аутентификации с использованием JWT широко используется в различных корпоративных приложениях. Особенно популярна эта схема в тех приложениях, которые реализуют парадигмы микросервисной архитектуры: при таком подходе каждый сервис получает необходимые ему сведения о пользователе непосредственно из токена, а не тратит время на получение этой информации из базы данных.
Формат JWT: атаки
В этом разделе будут рассмотрены основные атаки на JWT и даны рекомендации по их предотвращению.
Перехват токена
Перехват пользовательского токена может привести к ряду неприятных последствий.
Во-первых, так как JWT передается в открытом виде, для получения хранящихся в части полезной нагрузки исходных данных достаточно применить к этой части функцию base64UrlDecode. То есть злоумышленник, перехвативший токен, сможет извлечь хранящиеся в токене данные о пользователе.
В соответствии с лучшими практиками для предотвращения подобной угрозы рекомендуется:
- использовать при передаче токенов защищенное соединение;
- не передавать в токенах чувствительные пользовательские данные, ограничившись обезличенными идентификаторами.
Во-вторых, злоумышленник, перехвативший токен, сможет его переиспользовать и получить доступ к приложению от лица пользователя, чей JWT был перехвачен.
Здесь рекомендации будут следующие:
- как и в первом случае, использовать защищенное соединение при передаче токенов;
- ограничить время жизни JWT и использовать механизм refresh tokens.
Refresh tokens
В современных схемах аутентификации, основанных на JWT, после прохождения аутентификации пользователь получает два токена:
- access token — JWT, на основе которого приложение идентифицирует и авторизует пользователя;
- refresh token — токен произвольного формата, служащий для обновления access token.
Access token при таком подходе имеет сильно ограниченное время жизни (например, одну минуту). Refresh token же имеет длительное время жизни (день, неделя, месяц), но он одноразовый и служит исключительно для обновления access token пользователя.
Схема аутентификации в таком случае выглядит следующим образом:
- пользователь проходит процедуру аутентификации и получает от сервера access token и refresh token;
- при обращении к ресурсу пользователь передает в запросе свой access token, на основе которого сервер идентифицирует и авторизует клиента;
- при истечении access token клиент передает в запросе свой refresh token и получает от сервера новые access token и refresh token;
- при истечении refresh token пользователь заново проходит процедуру аутентификации.
Подбор ключа симметричного алгоритма подписи
При использовании симметричных алгоритмов для подписи JWT (HS256, HS512 и др.) злоумышленник может попытаться подобрать ключевую фразу.
Подобрав ее, злоумышленник получит возможность манипулировать JWT-токенами так, как это делает само приложение, а следовательно сможет получить доступ к системе от лица любого зарегистрированного в ней пользователя.
В нашем примере из первой части статьи для подписи JWT в качестве ключевой фразы была использована строка test. Она простая, короткая и содержится во всех основных словарях для перебора паролей. Злоумышленнику не составит труда подобрать эту ключевую фразу с использованием программ John the Ripper или hashcat .
Рекомендации для защиты от атаки в этом случае такие:
- использовать ключевые фразы большой длины, состоящие из больших и малых букв латинского алфавита, цифр и спецсимволов, и хранить их в строгой конфиденциальности;
- обеспечить периодическую смену ключевой фразы. Это снизит удобство использования для пользователей (поскольку время от времени им придется проходить процедуру аутентификации заново), но поможет избежать компрометации ключевой информации.
Использование алгоритма none
Как было упомянуто в первой части статьи, использование в заголовке JWT алгоритма none указывает на то, что токен не был подписан. В подобном токене отсутствует часть с подписью, и установить его подлинность становится невозможно.
Рассмотрим подобную атаку на нашем примере. Наш токен в незакодированном виде выглядит следующим образом:
header: < "typ": "JWT", "alg": "HS256" >payload: < "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" >signature: ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
Предположим, мы хотим, чтобы приложение считало нас администратором. Для этого необходимо установить значение admin в поле role полезной нагрузки. Но при внесении в токен этого изменения подпись токена станет невалидной, и приложение не примет такой JWT.
Для обхода защитного механизма мы можем попытаться изменить значение поля alg в заголовке токена на none. Наш токен примет следующий вид:
header: < "typ": "JWT", "alg": "none" >payload:
Поскольку мы используем алгоритм none, подпись отсутствует. В закодированном виде наш JWT будет выглядеть так:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0
Этот токен мы и передадим на сервер. Уязвимое приложение, проверив заголовок JWT и обнаружив в нем alg: none, примет этот токен без всяких проверок, как если бы он был легитимным, в результате чего мы получим привилегии администратора.
Чтобы защититься от такой атаки:
- необходимо вести на стороне приложения белый список разрешенных алгоритмов подписи и отбрасывать все токены с алгоритмом подписи, отличным от разрешенного на сервере;
- желательно работать строго с одним алгоритмом, например HS256 или RS256.
Изменение алгоритма подписи
При использовании асимметричных алгоритмов подпись токена осуществляется с использованием приватного ключа сервиса, а проверка подписи — с использованием публичного ключа сервиса.
Некоторые реализации библиотек для работы с JWT содержат логические ошибки, заключающиеся в том, что при получении токена, подписанного с использованием симметричного алгоритма (например, HS256), для проверки подписи в качестве ключевой фразы будет использован публичный ключ сервиса. Поскольку публичный ключ сервиса не засекречен, злоумышленник может легко получить его и использовать для подписи собственных токенов.
Для рассмотрения примера этого варианта атаки нам понадобится новый JWT:
header: < "alg": "RS256", "typ": "JWT" >payload: < "id": "1337", "username": "bizone", "iat": 1594209600, "role": "user" >signature: YLOVSKef-paSnnM8P2JLaU2FiS8TbhYqjewLmgRJfCj1Q6rVehAHQ-lABnKoRjlEmHZX-rufHEocDxGUYiGMjMexUQ3zt-WqZITvozJ4pkvbV-mJ1nKj64NmqaR9ZkBWtmF-PHJX50eYjgo9rzLKbVOKYOUa5rDkJPHP3U0aaBXFP39zsGdOTuELv436WXypIZBeRq2yA_mDH13TvzegWCK5sjD4Gh177bCq57tBYjhGIQrDypVe4cWBPlvwFlmG8tdpWGu0uFp0GcbTAfLUlbTSuGROj88BY0XeUs0iqmGlEICES3uqNx7vEmdT5k_AmL436SLedE0VHcyxve5ypQ
В кодированном виде он будет выглядеть следующим образом:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6InVzZXIifQ.YLOVSKef-paSnnM8P2JLaU2FiS8TbhYqjewLmgRJfCj1Q6rVehAHQ-lABnKoRjlEmHZX-rufHEocDxGUYiGMjMexUQ3zt-WqZITvozJ4pkvbV-mJ1nKj64NmqaR9ZkBWtmF-PHJX50eYjgo9rzLKbVOKYOUa5rDkJPHP3U0aaBXFP39zsGdOTuELv436WXypIZBeRq2yA_mDH13TvzegWCK5sjD4Gh177bCq57tBYjhGIQrDypVe4cWBPlvwFlmG8tdpWGu0uFp0GcbTAfLUlbTSuGROj88BY0XeUs0iqmGlEICES3uqNx7vEmdT5k_AmL436SLedE0VHcyxve5ypQ
Поскольку в этом случае мы используем для подписи алгоритм RS256, нам понадобятся публичный и приватный ключи.
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV 3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2 QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM +bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9 D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC 0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+ hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X +jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC 2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz 5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9 Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0 NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j 8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma 3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE= -----END RSA PRIVATE KEY-----
Для тестов мы будем использовать сайт jwt.io (рис. 2).

Рис. 2. Исходный JWT
Как и в предыдущем примере, модифицируем токен:
header: < "typ": "JWT", "alg": "HS256" >payload:
В кодированном виде заголовок и полезная нагрузка будут выглядеть следующим образом:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0
Остается только подсчитать подпись с использованием публичного ключа сервиса.
Для начала переводим ключ в hex-представление (рис. 3).

Рис. 3. Hex-представление ключа
Затем генерируем подпись с использованием openSSL (рис. 4).

Рис. 4. Генерация подписи для JWT
Полученное значение E1R1nWNsO-H7h5WoYCBnm6c1zZy-0hu2VwpWGMVPK2g добавляем к уже имеющейся строке, и наш токен принимает следующий вид:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdCI6MTU5NDIwOTYwMCwicm9sZSI6ImFkbWluIn0.E1R1nWNsO-H7h5WoYCBnm6c1zZy-0hu2VwpWGMVPK2g
Подставляем в поле secret на jwt.io наш публичный ключ, и JWT успешно проходит проверку (не забудьте поставить галочку secret base64 encoded!) (рис. 5).

Рис. 5. Успешная проверка подписи JWT
Для предотвращения такой атаки рекомендуется:
- работать только с одним алгоритмом, например HS256 или RS256;
- выбирать хорошо известные и проверенные библиотеки для работы с JWT, которые с меньшей вероятностью содержат логические ошибки в процедурах проверки токенов.
Манипуляция ключевыми идентификаторами
Стандарт RFC-7515 описывает параметр заголовка kid (Key ID, идентификатор ключа). Вместе с тем стандарт говорит о том, что формат этого поля строго не определен. Поэтому разработчики вольны интерпретировать его так, как удобно им, что зачастую приводит к различным ошибкам.
Для примера возьмем следующий заголовок JWT:
Предполагается, что для проверки токена будет использован хранящийся в БД ключ с идентификатором 1337. В случае ошибок кодирования это поле может быть уязвимо к SQL-инъекции:
В таком случае для проверки подписи ключа в качестве ключевой фразы вместо предполагаемого ключа из базы данных будет использована строка SECRET_KEY.
В следующем примере предположим, что для проверки токена будет использован ключ из файла keys/service3.key.
Если параметр не валидируется, злоумышленник сможет провести атаку Path Traversal (Directory Traversal) и вместо предполагаемого пути до файла с ключом передаст в поле kid путь до какого-либо публичного файла:
Злоумышленник может получить доступ к файлу cat.png и подписать JWT с использованием содержимого этого файла, поскольку этот файл общедоступный (например, он опубликован на одной из страниц сервиса). Сервис же, получив в поле kid путь до файла cat.png, использует его содержимое в качестве ключевого файла для проверки подписи токена (которая окажется успешной, ведь злоумышленник заранее об этом позаботился).
Рекомендация по предотвращению подобных атак простая: необходимо всегда валидировать и санитизировать полученные от пользователя данные, даже если они были получены в виде JWT.
Заключение
JSON Web Tokens — популярная и удобная технология. При правильном использовании JWT избавляет от распространенных ошибок недостаточной авторизации, позволяет просто и удобно распределить информационные потоки между сервисами, организовать единую точку входа в различные сервисы с одними и теми же учетными данными и даже повысить производительность сервиса.
Вместе с тем при неправильном использовании JWT можно подвергнуть свою систему существенным рискам, вплоть до компрометации учетных записей абсолютно всех пользователей системы.
Итак, для безопасного использования JWT следует:
- использовать защищенное соединение при передаче токенов;
- не передавать в токенах чувствительные пользовательские данные;
- ограничить время жизни JWT и использовать механизм refresh tokens;
- использовать ключевые фразы большой длины;
- обеспечить периодическую смену ключевой фразы;
- вести на стороне приложения белый список разрешенных алгоритмов подписи;
- в идеальном случае работать строго с одним алгоритмом подписи;
- выбирать хорошо известные и проверенные библиотеки для работы с JWT;
- всегда валидировать и санитизировать полученные от пользователя данные.
JSONWebToken
JSONWebToken — это библиотека для создания (подписания) и подтверждения (проверки) токенов, используемых для аутентификации/авторизации пользователей. Данная библиотека является реализацией JSON Web Tokens для Node.js .
Установка
yarn add jsonwebtoken # или npm i jsonwebtoken
Подписание (создание) токена
const jwt = require('jsonwebtoken') jwt.sign(payload, secretOrPrivateKey, [options, callback]) // Пример jwt.sign( username: 'John', email: 'john@email.com' >, 'secret', expiresIn: '1h' > )
Если передан колбек, то метод выполняется асинхронно. Данный колбек получает объект ошибки err . В противном случае, метод выполняется синхронно, возвращая токен в виде строки.
Аргументы
- payload — полезная нагрузка: объект, буфер или строка, представляющие валидный JSON. Объект преобразуется с помощью метода JSON.stringify()
- secretOrPrivateKey — строка, буфер или объект, содержащие секрет для алгоритмов HMAC или зашифрованный с помощью схемы PEM приватный ключ для RSA и ECDSA
Настройки
- Registered claim names
- Creating and Validating JWTs
- algorithm (по умолчанию HS256 )
- expiresIn — время, в течение которого токен считается действительным: 100 — 100 с, ‘100’ — 100 мс, ‘1h’ — 1 час, ‘2d’ — 2 дня
- notBefore — время, по истечении которого токен будет считаться действительным
- audience
- issuer
- jwtid
- subject
- noTimestamp
- header
- keyid
- mutatePayload — если имеет значение true , функция sign() будет модифицировать объект payload напрямую. Это бывает полезным, когда нам нужна «сырая» ссылка на полезную нагрузку после применения к ней настроек, но до шифрования.
Настройки expiresIn , notBefore , audience , subject , issuer могут быть определены прямо в payload как exp , nbf , aud , sub и iss , соответственно.
Заголовок может быть кастомизирован через объект options.header .
В сгенерированный jwt по умолчанию включается iat (время выпуска, создания), если не определено noTimestamp: true .
Пример синхронного подписания токена с использованием RSA SHA256
const privateKey = fs.readFileSync('private.key') const token = jwt.sign( name: 'John Smith' >, privateKey, algorithm: 'RS256' >)
Пример асинхронного подписания токена
jwt.sign( name: 'John Smith' >, privateKey, algorithm: 'RS256' >, (err, token) => if (err) console.error(err) console.log(token) >)
Подтверждение (проверка) токена
const jwt = required('jsonwebtoken') jwt.verify(token, secretOrPublicKey, [options, callback])
Если передан колбек, то метод выполняется асинхронно. Данный колбек получает декодированную полезную нагрузку при условии валидной сигнатуры и опциональных настроек. В противном случае, выбрасывается исключение.
Без колбека метод выполняется синхронно, возвращая декодированный payload при условии валидной сигнатуры и опциональных настроек. В противном случае, выбрасывается исключение.
Аргументы
- token
- secretOrPublicKey — если jwt.verify() вызывается асинхронно, данный аргумент может быть функцией, запрашивающей секрет или публичный ключ. Некоторые библиотеки в качестве секрета ожидают получить строку в формате base64. В этом случае вместо secret следует передать Buffer.from(secret, ‘base64’)
Настройки
- algorithms — массив с названиями разрешенных алгоритмов ( [‘HS256’, ‘HS384’] )
- audience
- complete — возвращает объект < payload, header, signature >вместо содержимого полезной нагрузки
- issuer
- jwtid
- ignoreExpiration — если true , время жизни токена будет игнорироваться
- ignoreNotBefore
- subject
- clockTolerance — допустимая разница во времени в сек
- maxAge — максимально допустимый «возраст» токена (100, ‘100’, ‘1h’, ‘2d’)
- clockTimestamp — время в сек, используемое в качестве текущего для всех сравнений
- nonce — используется в Open ID для ID токенов
Примеры
// Синхронное подтверждение симметричного токена try const decoded = jwt.verify(token, 'secret') console.log(decoded.name) // John > catch (err) console.error(err) > // или jwt.verify(token, 'secret', (err, decoded) => if (err) console.error(err) console.log(decoded.name) // John >) // Подтверждение асимметричного токена const cert = fs.readFileSync('public.pem') // получаем публичный ключ jwt.verify(token, cert, (err, decoded) => if (err) console.error(err) console.log(decoded.name) // John >) // Подтверждение опциональных настроек const cert = fs.readFileSync('public.pem') // получаем публичный ключ jwt.verify(token, cert, audience: 'urn:john', issuer: 'urn:issuer', jwtid: 'jwtid', subject: 'subject' >, (err, decoded) => >) // Подтверждение с помощью колбека `getKey()` const jwksClient = require('jwks-rsa') const client = jwksClient( jwksUri: 'https://example.com/.well-known/jwks.json' >) function getKey(header, callback) client.getSigningKey(header.kid, (err, key) => const signingKey = key.publicKey || key.rsaPublicKey callback(null, signingKey) >) > jwt.verify(token, getKey, options, (err, decoded) => if (err) console.error(err) console.log(decoded.name) // John >)
Расшифровка (декодирование) токена
jwt.decode(token, [options])