JWT — как безопасный способ аутентификации и передачи данных
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.
В простом понимании — это строка в специальном формате, которая содержит данные, например, ID и имя зарегистрированного пользователя. Она передается при каждом запросе на сервер, когда необходимо идентифицировать и понять, кто прислал этот запрос.
В этой статье разберу, что такое Access токен, Refresh токен и как с ними работать.
Для дальнейших разборов будет использован токен:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0
После того, как посетитель прошел авторизацию в нашей системе, указав свой логин и пароль, система выдает ему 2 токена: access token и refresh токен.
После чего посетитель, когда хочет получить с сервера данные, например, свой профиль, вместе с запросом он передает Access токен, как на примере выше. Сервер, получив его проверяет, что он действительный (об этом чуть ниже), вычитывает полезные данные из него (тот же user_id) и, таким образом, может идентифицировать пользователя.
Токен разделен на три основные группы: заголовок, полезные данные и сигнатура, разделенные между собой точкой.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 — это первая часть токена — есть заголовок. Она закодирована в Base64 и если её раскодировать, получим строку:
Это можно проверить прям в браузере, выполнив в консоле или js коде:
const header = atob(‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9’); console.log(header);
typ — это наш тип токена JWT. Alg — алгоритм шифрования HMAC-SHA256. Их может быть несколько, но здесь буду говорить именно об этом алгоритме.
PAYLOAD или полезные данные
Вторым блоком идет eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
Это есть полезные данные, так же закодированные в Base64. После раскодирования получим:
Данные могут быть любыми. Главное, чтобы по ним можно было идентифицировать пользователя. В нашем случае — это user_id и exp — время окончания действия текущего токена.
Поскольку необходимо ограничивать токен по времени, поле exp обязательно. По нему можно проверить, актуален ли токен или нет.
Последняя часть токена — наиболее важная. У нас это E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0
Как вы уже могли заметить — первые данные передаются практически в открытом виде и раскодировать их может любой. Но шифровать их нет необходимости. Цель токена — подтвердить, что эти данные не были изменены. Вот для этих целей и выступает сигнатура. И чтобы её сгенерировать нужен приватный ключ. Ну или некая секретная фраза, которая находится только на сервере. Только с помощью этого ключа мы можем создать сигнатуру и проверить, что она была создана именно с помощью его.
Она получается примерно следующим образом:
Берем заголовок, например и кодируем его в base64, получаем ту самую часть eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Тоже самое проделываем с данными eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
После этого склеиваем их и получаем eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
Далее эти данные шифруем с помощью нашего алгоритма HMAC-SHA256 и ключа.
const header = ‘<"alg":"HS256","typ":"JWT">‘ // строка const payload = » // строка // кодируем заголовок и данные в base64 const headerBase64 = base64urlEncode(header) const payloadBase64 = base64urlEncode(payload) // склеиваем точкой полученные строки const data = headerBase64 + ‘.’ + payloadBase64 // кодируем алгоритмом шифрования нашим ключем шифрования const secret = ‘123456’ const sig = HMAC-SHA256(data, secret) // и, наконец, получаем окончательный токен const jwt = data + ‘.’ + sig"alg":"HS256","typ":"JWT">
Проверка токена
Для проверка токена необходимо проделать ту же операцию.
Берем склейку заголовок + данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру с токена и сверяем с результатом кодирования. Если результаты совпадают — значит данные подтверждены и можно быть уверенным, что они не были подменены.
Refresh token
Основной токен, про который шла речь выше, обычно имеет короткий срок жизни — 15-30 минут. Больше давать не стоит.
Как только время выйдет, пользователю снова придется проходить авторизацию. Так вот чтобы этого избежать, существует Refresh токен. С помощью него можно продлить Access токен.
В действительности, Refresh токен обязательно должен быть одноразовым. Его задача — получить новую пару токенов. Как только это было сделано, предыдущий токен будет считаться недействительным. Срок жизни Refresh токена уже может быть большим — до года, а может даже и больше.
У него, обычно, нет какой-то структуры и это может быть некая случайная строка.
Для проекта odo24.ru я использовал следующий подход.
Генерируется Access токен и после случайная строка, например T6cjEbghMZmybUd_fhE
С нашего нового Access токена eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0 беру последние шесть знаков, получаю Ed4wF0
Склеиваю и получаю рефреш токен T6cjEbghMZmybUd_fhEEd4wF0
Это сделано для привязки Access токена к Refresh. Для получения новых токенов необходимо передать эти два токена. Делается проверка на их связку и только после валидируется Access токен. Если и второй этап прошел успешно, тогда получаем с базы данных по текущему user_id рефреш токен и сверяем с тем, что к нам пришел. Если они совпадают, тогда генерируются новые токены и в базе данных обновляется Refresh токен на новый.
Где хранить токены?
В моем случае я разделил оба токена и храню в разных местах. Access токен нужен только для идентификации пользователя и на клиенте (JS) он не нужен, поэтому он передается в Cookie (http only).
Refresh токен хранится в LocalStorage и используется только когда Access токен перестал быть актуальным.
Зачем 2 токена?
Представим ситуацию, когда у нас каким-то образом украли Access токен. Да, это уже плохо и где-то у нас брешь в безопасности. Злоумышленник в этом случае сможет им воспользоваться не более чем на 15-30 минут. После чего токен «протухнет» и перестанет быть актуальным. Ведь нужен второй токен для продления.
Если украли Refresh токен, то без Access токена (который недоступен в JS) продлить ничего нельзя и он оказывается просто бесполезным.
Самая неприятная ситуация — это когда удалось увести сразу 2 токена. В этом случае злоумышленник сможет пользоваться системой неограниченное время. Точнее когда пользователь попытается войти в систему, его не пустит, т.к. его Refresh токен уже будет неактуальным, и ему придется вводить логин и пароль. Только в этом случае злоумышленник потеряет контроль над чужой учетной записью.
Постскриптум
В своей реализации Refresh токена использовал общую длину 24 знака. Первые 6 знаков — это дата его «протухания», следующие 12 знаков — случайно сгенерированные данные. И в конце 6 знаков — это часть Access токена последней части сигнатуры.
Дату протухания внедрил прям в токен с той целью, чтобы не хранить эту информацию где-то в другом месте, например, в базе данных.
Дата содержит год, месяц, день, час и минуты. Хранится в ASCII
Кодирование даты на Golang:
// приводим к целочисленному числу uint32. Итого 32 бита. // расчет простой: год 12 бит, месяц 4 бита, день 5 бит и т.д. Таким образом в аккурат умещаемся в 32 бита или 4 байта. date := uint32(year<<20) | uint32(month<<16) | uint32(day<<11) | uint32(hour<<6) | uint32(minute) // в цикле кодируем байты в ASCII. 1 знак это шесть бит. Итого и получаем шесть знаков даты по таблице ASCII - печатные знаки. for n := 0; n < 6; n++ < b6Bit = byte(date>>i) & 0x3F sBuilder.WriteByte(byte8bitToASCII(b6Bit)) . >
Всю реализацию на Go можно изучить на Github-е
Заключение
В этой статье попытался рассказать о взаимодействии двух токенов и как ими пользоваться. В сети достаточно много информации о Access токенах, однако мало, как мне показалось, информации о Refresh токенах.
Jwt token что это
В последнее время все чаще можно встретить приложения, использующие для аутентификации пользователей механизмы 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;
- всегда валидировать и санитизировать полученные от пользователя данные.
Единый вход с использованием JWT
iSpring Learn позволяет использовать JWT (JSON Web Token) для реализации единого входа. JWT – это открытый стандарт для передачи пакетов между сторонами в веб-среде. Он используется для шифрования и передачи данных авторизованных пользователей между поставщиком идентификации (ваш корпоративный веб-сайт) и поставщиком услуг (iSpring Learn).
Для настройки нужно
- Навыки веб-разработки (PHP, JavaScript, JSON, JWT).
- Доступ к вашему хостингу с правами администратора.
- Аккаунт iSpring Learn с доступом администратора.
Для авторизации пользователя на iSpring Learn, сообщение JWT должно содержать уникальный идентификатор пользователя, также являющийся электронной почтой. Пароль и другая информация не требуется для осуществления единого входа.
JSON Web Token передается в iSpring Learn как GET-параметр.
Составляющие части JWT
JWT Токен передается в зашифрованном виде и состоит из трёх частей, разделенных точками:
- XXXXXX закодированный заголовок
Заголовок представляет собой base64-код, который содержит информацию о типе стандарта («JWT») и об алгоритме шифрования («HS256»).
- YYYYYY тело токена
Тело токена, также называемое “полезный груз” или payload содержит идентификатор пользователя (электронная почта). Он также представлен в формате base64-кода.
«IAT»: 123456789,
«JTI»: f4as6d5f4as6d54fasd6df4,
«ехр»: 123456849,
«электронная почта»:
«username@yourcompany.com»>
IAT (Issued At) — содержит время создания токена;
JTI (JWT ID) — идентификатор маркера, выдается автоматически, закодирован;
ехр — время истечения этого токена;
email — адрес электронной почты пользователя (или идентификатор пользователя)который требуется для проверки подлинности.
- ZZZ цифровая подпись
Эта часть содержит ключ для шифрования всего токена (все 3 части) и выглядит следующим образом:
HMACSHA256(base64UrlEncode(XXXXXX) + «.» + base64UrlEncode(YYYYYY), secret)
secret — это ключ шифрования, который используется обеими сторонами для кодирования сообщений.
Настройка параметров JWT для единого входа в СДО
Настройка JWT в аккаунте iSpring Learn достаточно проста.
- Войдите в свой аккаунт iSpring Learn под учётной записью администратора и введите путь к настройкам в строке браузера: https://yourcompany.ispringlearn.ru/settings/sso/jwt
- Поставьте галочку в поле Enable JWT login for this account.
- Укажите Security key. Это значение также является криптографическим ключом и секретной частью частью JWT токена.
- Identity provider URL содержит адрес страницы вашего сайта, на котором располагается скрипт для генерации JWT токенов для входа пользователя.
- Logout URL содержит адрес страницы вашего сайта, на котором располагается скрипт для генерации JWT токенов для выхода пользователя.
Мы рекомендуем использовать протокол HTTPS вместо HTTP для повышения безопасности.
Примечание: при необходимости можно использовать GET параметр no_jwt=1 для доступа к форме логинации в СДО в обход SSO: https://yourcompany.ispringlearn.ru/login?no_jwt=1 Например, если ввести этот параметр, то пользователь сможет войти, даже если единый вход по какой-то причине не работает.
Логика единого входа JWT
Весь процесс показан на временной UML-диаграме ниже:
На этапе проверки пользователя, если его нет в базе iSpring Learn, он будет автоматически добавлен и залогинен.
Единственным препятствием автоматического добавления нового пользователя может быть только лимит пользователей для текущего плана СДО.
Другим пример автоматической авторизации может быть тот случай, если пользователь сразу зашел на сайт iSpring Learn, без предварительной авторизации на вашем сайте. Если в аккаунте Learn включена опция JWT, то пользователь будет автоматически перенаправлен на соответствующую страницу вашего сайта (поставщика идентификации) https://www.yourwebsite.com/login-token/. После ввода логина и пароля на стороне вашего сайта, пользователь сможет беспрепятственно войти на портал Learn.
Примеры реализации на PHP
Аутентификация (вход)
Данный cерверный скрипт должен быть размещён на вашем сайте. Возможные случаи и результаты:
- Если пользователь авторизован на вашем сайте, система перенаправляет этого пользователя в СДО iSpring Learn.
- Если не авторизован, то пользователю предлагается ввести логин и пароль к вашему сайту. В случае успеха пользователь будет залогинен в СДО iSpring Learn.
Запрос на выход
Этот скрипт тоже должен быть размещён на вашем сайте. В данном примере система проверяет подлинность email пользователя и осуществляет его выход из системы.
Ссылки на англоязычные ресурсы по JWT:
- Introduction to JSON Web Tokens
- Anatomy of a JWT request
- JWT Debugger
- Libraries for Token Signing/Verification
- http://www.base64decode.org/
- Как создать портфолио курсов с iSpring Page
- Мобильное приложение iSpring Play
- Системные требования при конвертации в видео
JWT — коротко о главном

Есть такая штука, называется JWT — JSON Web Token. По сути это способ (стандарт) обернуть данные (payload) в подписанный пакет. В основном это используется для обмена данными, например авторизационными. Рассмотрим как это работает на простом примере авторизации.
- Пользователь аутентифицируется с использованием login/пароля (АУТЕНТИФИКАЦИЯ)
- В ответ получает jwt — json web token. Далее по тексту jwt-токен или просто “токен”.
- Когда пользователь хочет получить доступ к ресурсу, он отправляет токен
- Сервер проверяет валидность токена и предоставляет доступ к ресурсу (АВТОРИЗАЦИЯ)
Важно понимать, что токен не содержит в себе никакой секретной информации. Чаще всего туда помещают информацию о пользователе (id, username, email, role). Эта информация может быть прочитана кем угодно. Подписывается токен для того, чтобы убедиться в том, что информация в токене не была подделана/изменена. Подписан токен может либо закрытым ключом, либо парой открытым/закрытым ключом.
JWT похож на SAML, но гораздо проще. На картинке ниже проиллюстрировано то, как выглядит JWT-токен, подписанный парой ключей, открытым и закрытым. JWT токен состоит из заголовка, данных и подписи (“HEADER”+”PAYLOAD”+”SIGNATURE”).

Как видите, структура JWT-токена очень проста. Ниже показано сравнение того, как выглядит JWT-токен и SAML-сообщение.

Рассмотрим принцип использования JWT, описанный в начале, более подробно, приблизив его к реальности.
Действия, описанные далее, могут отличаться в зависимости от реализации программиста и использования разных протоколов аутентификации и авторизации. Описанная ниже логика и порядок действий даёт общее понимание того, как устроен процесс.
1. Пользователь аутентифицируется с использованием логин/пароля (АУТЕНТИФИКАЦИЯ)
Если говорить про WEB, то как правило пользователь заходит на сайт (клиент) и вводит логин пароль в форму авторизации. Клиент отправляет авторизационные данные серверу аутентификации, задача которого проверить, что пользователь является тем, за кого он себя выдаёт сверив пару логин/пароль с тем, что находится в его базе данных. И в зависимости от результата проверки возвращает положительный или отрицательный ответ клиенту (web-приложению, куда пользователь ввёл логин/пароль). Этот процесс называется аутентификация. Не путать с авторизацией.
2. Пользователь получает JWT-токены
Если пользователь успешно прошёл процесс аутентификации, то есть ввёл действительные логин и пароль, то клиент, получив положительный ответ от сервера аутентификации запрашивает у сервера, который выдаёт токен, пару токенов — AccessToken и RefreshToken. Далее я объясню, откуда вдруг взялся RefreshToken и зачем он нужен.
Это не обязательно разные сервера. В самом простом клиент-серверном приложении, сервер и аутентифицирует, и сразу же выдаёт ключи пользователю. Так или иначе пользователь получает токен и как правило хранит его в браузере. Например, в HttpOnly-Cookie.
Для хранения токена очень важно использование флага HttpOnly. Иначе это считается не безопасным.
3. Когда пользователь хочет получить доступ к ресурсу, он отправляет токен
Когда пользователь хочет получить доступ к ресурсу, он отправляет запрос на сервер и передаёт AccessToken. В случае с jwt это можно сделать с использованием схемы Bearer. Для этого в HTTP заголовке передаётся ключ “Authorization” со значением, содержащим ключевое слово “Bearer” и через пробел сам токен.
Authorization: Bearer
4. Сервер проверяет валидность токена и предоставляет доступ к ресурсу (АВТОРИЗАЦИЯ)
После того, как сервер получил токен, он проверяет его валидность и предоставляет доступ к ресурсу. И тут кроется самое интересное. Токен может быть не валиден по разным причинам. Например, он просрочен. Дело в том, что хранить токены длительное время — это не безопасно. В зависимости от чувствительности данных, к которым предоставляется доступ, время жизни токена может быть от нескольких секунд, до нескольких минут. В случае если токен не действителен, то сервер сообщает на клиент ошибку 401 (Unauthorized Error). В таком случае клиент может попросить пользователя ещё раз ввести логин пароль. Но чтобы не делать это слишком часто, придумали RefreshToken. Время жизни RefreshToken уже гораздо больше. Например 30-60 дней.
Когда пользователь первый раз получает пару AccessToken/RefreshToken, то RefreshToken сохраняется как в базе данных на стороне сервера, вместе с id пользователя или другим уникальным идентификатором, так и на стороне клиента(пользователя). Как правило это HttpOnly-Cookie, которая хранится в браузере. Если после отправки AccessToken клиент получает ошибку 401 (Unauthorized Error), он отправляет RefreshToken на обновление токенов (/api/refresh) и после валидации RefreshToken сервер предоставляет новую пару AccessToken/RefreshToken. Клиент их сохраняет и заново отправляет AccessToken. Если на этот раз валидация прошла успешно, то сервер предоставляет доступ к запрашиваемому ресурсу.
Если же и RefreshToken не прошёл проверку, то пользователя просят заново пройти процесс аутентификации — ввести логин/пароль и заново получить пару AccessToken/RefreshToken.
На картинке показана упрощённая схема описанного процесса.

Конечно схема авторизации может и должна быть гораздо сложнее. Серверное приложение может проверять статус активна или нет учётная запись. Позволяет ли роль пользователя получить доступ к ресурсу и т.д.. Должен быть механизм отзыва токенов. Но всё это относится к внутренней логике конкретного приложения.
Помните, что Вы никогда не можете быть уверены в том, что ваш логин/пароль безопасно передаётся от браузера к клиенту и от клиента к серверу. И в том, что клиент или сервер безопасно обрабатывает и хранит ваши пароли. Поэтому так важно использовать для каждого ресурса уникальный пароль и периодические его менять.
Скоро в этом посте появится видео, в котором я покажу пример простого клиент-серверного web-приложения и наглядно продемонстрирую работу JWT.