О чем эта статья: мы разберемся, что такое JSON Web Token, как он устроен и для чего используется, рассмотрим такие приемы, как «black-list токенов» и «контроль версий» токенов. Для наглядности, в конце будут блок-схемы клиент-серверных запросов с пояснениями.
Для кого эта статья: для тех, кто хочет детально понять что такое JWT, а так же для тех, кто просто ищет схему реализации.
Термины
Идентификация — процесс получения идентификатора пользователя: логин / e-mail /id
Аутентификация — подтверждение личности пользователя (с помощью пароля, отпечатка пальца, и т.п.)
Авторизация — предоставление прав пользователю, выдача токена
Валидация — процесс проверки «куска» информации на соответствие требованиям программы, или просто на совпадение с копией, хранимой в базе данных.
Токен — ключ аутентификации пользователя
Credentials — учетные данные пользователя: логин, пароль, google id, и т.п.
БД — база данных
Клиент — уровень представления данных (см клиент-серверная архитектура). Имеет графический интерфейс для взаимодействия с пользователем. пример: веб-сайт в интернете.
Сервер — уровень получения и обработки данных (см клиент-серверная архитектура). Не имеет графического интерфейса, принимает запросы от клиентов через API.
API — Application Program Interface, набор команд, позволяющий обратиться к приложению
Метод API — конкретная команда, позволяющая обратиться к приложению
Публичные методы API — те, которые доступны без аутентификации пользователя, например: главная страница сайта в интернете.
Защищенные методы API — требующие обязательной аутентификации пользователя, например: личный кабинет пользователя на сайте.
Эндпоинт — url адрес метода API в интернете
Что такое JWT
JWT (Json Web Token) — ключ аутентификации пользователя. Используется для запросов к защищенным методам API.
Для чего нужны JWT: чтобы не передавать учетные данные пользователя с каждым запросом к серверу.
Чем JWT лучше учетных данных:
Учетные данные пользователя, как правило хранятся долго (месяцы). Как бы хорошо не был зашифрован запрос, при достаточном количестве времени его можно расшифровать. Если запрос, содержащий учетные данные перехвачен злоумышленником, у него будет много времени на расшифровку. Токены доступа имеют ограниченный срок годности (обычно ~15 минут). Этого времени не достаточно, чтобы расшифровать надежный шифр. К тому времени, когда зловредный алогритм расшифрует запрос, токен уже выйдет из обращения и будет бесполезен.
Использовать учетные данные, это медленно. Для валидации учетных данных сервер должен запросить их сохраненную копию из БД и сравнить с данными, которые пришли в запросе. Обращение к БД — дорогостоящая процедура, она сильно увеличивает время обработки запроса. Токены, с другой стороны, не требуют обращения к БД для валидации. Это позволяет снизить нагрузку на БД и ускорить обработку запросов сервером.
Время жизни токенов. Каждый токен имеет определенный срок годности. Эта информация зашита в его теле. При валидации, сервер извлекает данные из токена и проверяет, не истек ли срок.
Как хранить токены. Существует мнение, что некоторые виды токенов нужно хранить в БД вместе с остальными данными, это не так. JWT были придуманы специально для того, чтобы не хранить их и не сверять с БД при каждом запросе. Валидация токенов происходит прямо на сервере. Как это реализовано, я расскажу ниже.
Оговорюсь: бывают ситуации, когда нам нужно отозвать токен до истечения его срока годности и добавить его в «черный список». Для хранения этого списка используют
In-Memory Cache или специальную базу данных — Redis (см раздел "Black-list токенов").
Как передавать токены
от сервера к клиенту:
в заголовке запроса «Authrorization» с добавлением слова Bearer
либо используя заголовок «Set-Cookie»:
Set-Cookie: accessToken=<jwt>; HttpOnly; Sequre; SameSite=Strict;
Set-Cookie: refreshToken=<refresh-token>; HttpOnly; Sequre; SameSite=Strict;
от клиента к серверу:
в заголовке запроса «Authrorization» с добавлением слова Bearer
Authorization: Bearer <jwt>
либо используя заголовок «Cookie»
Cookie: accessToken=<jwt>
Виды JWT
-
«access token» — проверяется при каждом обращении к защищенному API
многоразовый
присылается с каждым запросом к API в заголовке «authorization»
имеет короткий срок годности (обычно ~15 мин)
когда срок годности выходит, сервер возвращает #401
-
«refresh token» — токен для получения новой пары токенов (access и refresh)
одноразовый
имеет длительный срок годности (обычно несколько дней)
отправляется клиентом на эндпоинт ~/auth/refresh, когда истечет срок годности access токена и сервер вернет #401
«bearer token» — частный случай access токена. В рамках веб приложений эти термины можно использовать, как синонимы.
Структура JWT
Токен состоит из 3 частей разделенных точкой:
header — содержит информацию об алгоритме шифрования и типе токена (JWT)
-
payload — данные токена. Стандартные поля:
iss (Issuer) — издатель токена. Как правило — uuid приложения, выпустившего токен.
sub (Subject) — собственник токена. Как правило — uuid пользователя
aud (Audience) — массив url серверов, для которых предназначен токен
exp (Expiration Time) — время, в течение которого токен считается валидным.
nbf (Not Before) — временная метка, до которй токен не считается валидным
iat (Issued At) — время создания токена
jti (JWT ID) — уникальный идентификатор токена
signature — строка, полученная из частей токена (header + payload) при помощи шифрования.
Валидация токенов
Для всех современных языков программирования написаны библиотеки для создания и валидации JWT, писать этот код вручную нет никакого смысла, но понимать, как это происходит важно.
Что тут происходит:
1. Извлекаем JWT из заголовка запроса
2. определяем алгоритм шифрования токена. (параметр “header.alg”)
3. при помощи алгоритма, шифруем:
header + “.” + payload
4. сравниваем полученное значение с третьей частью токена (signature)
Значения совпали? — идем дальше. Нет? — возвращаем на клиент #401
5. проверяем срок годности токена. (“payload.exp”)
Срок не истек? — идем дальше.
Истек? — возвращаем #401
6. дополнительно можно проверить остальные параметры payload: iss, sub, aud, nbf
7. отдаем на клиент запрошенные данные
Black-list токенов
Когда мы выходим из учетной записи, или сбрасываем пароль, нам нужно отозвать ранее выданные токены, чтобы никто уже не смог зайти с ними в приложение. Для этого токены добавляются в специальный «черный список». При проверке токена мы сначала проверяем, не добавлен ли он в этот список, а затем уже валидируем его, как было описано выше. Если токен найден в «черном списке», возвращаем #401.
Токен — коротко живущая информация. Чтобы токены не накапливались в «черном списке» их можно периодически удалять, но проще — использовать специальную базу данных с поддержкой TTL (Time to Live). Такие БД (например Redis) позволяют назначить записи срок годности, после истечения которого данные будут удалены автоматически.
Вопрос: если мы используем БД с поддержкой TTL, зачем нам вообще «черный список»? Можно просто хранить все токены в БД, удалять отозванные, и проверять, есть ли такой токен при каждом запросе.
Ответ: конечно можно, но количество таких токенов будет существенно больше. Это увеличит объем потребляемой памяти, и замедлит запросы к БД. больше данных => медленнее поиск в БД.
Контроль версий
Разберем ситуацию:
Ваши учетные данные были украдены.
Злоумышленник входит в приложение от вашего имени и получает пару токенов. Когда срок жизни токенов истекает, он запрашивает новые в обмен на refresh token, и т. д.
Вы узнаете, что страница взломана и сбрасываете пароль. Но, вы не можете отозвать все старые токены потому, что у вас их нет, они нигде не хранятся.
Чтобы решить эту проблему используют «контроль версий учетных данных»
В таблицу нашей БД, где хранятся учетные данные, добавляем поле «version»
При создании refresh токена добавляем поле «version» в payload токена.
При каждой проверке refresh токена сверяем номер версии с номером из БД
Если номер версии не совпал, возвращаем #401
Вопрос: а чем это лучше, чем хранить сам refresh токен в базе данных?
Ответ 1: Утечка данных из БД (такое бывает) никогда не приведет к утечке токенов, а украденный хеш пароля ничего не даст злоумышленнику, потому что его еще нужно дешифровать.
Ответ 2: Если пользователь входите в приложение с разных устройств, сервис авторизации выдаст токены и сохранит их в БД. Выдавать всем один refresh token небезопасно, => количество записей в БД = количеству токенов. Больше записей => медленнее обработка запроса
Ответ 3: В случае сброса пароля серверу придется удалить из базы все токены, привязанные к пользователю. Удаление данных из таблицы — трудоемкий процесс, он требует переиндексации всей таблицы => возрастает нагрузка на БД => медленнее обработка запроса.
Использование JWT
Что тут происходит:
ввод учетных данных, получение новой пары токенов
запрос данных с access токеном
проверка, не внесен ли токен в black-list
валидация access токена, передача данных на клиент
Обновление JWT
Что тут происходит:
запрос данных с access токеном
валидация access токена не прошла, возврат ошибки #401
запрос обновления токенов с refresh токеном,
проверка, не внесен ли токен в black-list
получение версии учетных данных из БД
валидация токена, проверка версии токена
генерация новой пары токенов, отправка на клиент
Неуспешное обновление JWT
Что тут происходит:
запрос обновления токенов
проверка, не внесен ли токен в black-list
получение версии учетных данных из БД
неуспешная валидация токена, редирект на страницу ввода учетных данных
В заключение
Вот, пожалуй, и все, что я хотел рассказать о JWT. Надеюсь, вышло не слишком нудно. Если появятся вопросы, пишите, буду рад любым комментариям.
P.S. Эта статья, на самом деле, часть исследования, посвященного методам авторизации. Основной материал я опубликовал в статье "Auth сервис без библиотек". Возможно, он будет интересен в контексте изучения JWT.
Еще по теме:
Комментарии (14)
lear
10.09.2024 08:41+4От JWT нет пользы в большинстве проектов.
Он нужен только для оптимизации запроса данных из других сервисов, только тогда выигрыш производительности нивелирует проблемы безопасности.
Если вы в JWT храните недостаточно данных (id пользователя и, допустим, роль) и затем запрашиваете все остальные данные в бд, то совокупного выигрыша нет.
GRD-1 Автор
10.09.2024 08:41Случайно "отклонил" комментарий к статье, извиняюсь. Мне бы хотелось все-таки на него ответить.
Комментарий был такой:
"Как бы хорошо не был зашифрован запрос, при достаточном количестве времени его можно расшифровать. Если запрос, содержащий учетные данные перехвачен злоумышленником, у него будет много времени на расшифровку."
JWT токен не шифруется, а подписывается. Все что он содержит можно просто просмотреть декодировав текст из base64. Атака может быть, только на получение приватного ключа шифрования, если он слабый, для дальнейшей подмены токенов. —
Мой ответ:
Здесь сравнивалась пересылка данных в зашифрованном запросе (например, по протоколу tls). Обсуждаемая атака предполагалась на ключ шифрования запроса, а не на приватный ключ токена. Поскольку подбор ключей шифрования — трудоемкая операция, занимающая много времени, к моменту подбора ключа токен уже устареет и будет бесполезен, а вот пароль — нет. Отсюда я сделал вывод, что пересылка токенов безопаснее, чем пересылка паролей. Согласен, формулировка мутная, нужно перефразировать, спасибо.
santanico
10.09.2024 08:41Затронута интересная тема, спасибо! Но со своей стороны не могу не добавить:
refresh token изначально выдается при авторизации пользователя вместе с access token
Поле version инкрементируется при каждом изменении данных авторизации (пароля)
Описываемая схема «Контроль версий» используется для того, чтобы заблеклистить refresh token (потому что система его не помнит и он не может быть просто добавлен в blacklist)
northrop
10.09.2024 08:41+1Еще бы услышать хотелось в чем смысл наличия id token и почему недостаточно иметь вместо него access token. А также про scope, implicit and explicit flow.
SamDark
10.09.2024 08:41Ответ: конечно можно, но количество таких токенов будет существенно больше. Это увеличит объем потребляемой памяти, и замедлит запросы к БД. больше данных => медленнее поиск в БД.
В Redis поиск по ключу — это алгоритм сложности
O(1)
. То есть не зависит от количества ключей и не замедлит запросы к БД.Также Redis хранит данные довольно компактно и лимит по ключам на одном инстансе даже со скромным количеством памяти — десятки миллионов. Учитывая expiration, упереться в это вряд-ли получится для большинства сервисов.
Отсюда ещё раз вопрос: а так ли нужен JWT если инвалидировать токены нужно?
GRD-1 Автор
10.09.2024 08:41Про то, что алгоритм сложности редиса
O(1)
я не знал. Сейчас почитал, спасибо.
По вашему вопросу: уточните пожалуйста, а с чем мы сравниваем использование JWT?
Я вижу у JWT такие преимущества:
1. JWT универсальны. Вам будет легче проводить интеграцию со сторонними сервисами, если вы будете использовать универсальный формат ключей доступа.
2. У JWT есть payload. Мы можем добавить туда uuid пользователя, uuid клиента, и дополнительно все, что захотим. В любом стеке для них есть библиотеки, которые предлагают набор параметров для валидации и сами проводят ее. Если мы будем хранить ту же информацию, скажем, в редисе, нам придется самостоятельно писать код валидации.
3. Тому, кто придет работать с тем же кодом после нас, придется разбираться в логике валидации вмето того, чтобы использовать стандартные методы библиотек.
4. У библиотек, как правило, есть нормальная документация, где можно посмотреть зачем нужен каждый параметр.
Но, опять же, нужно понимать, с чем мы сравниваем JWT.
gun_dose
10.09.2024 08:41+2Set-Cookie: refreshToken=<refresh-token>;
Насколько секьюрно хранить рефреш-токен в кукисах? Ведь в таком случае оба токена всегда передаются одновременно и полностью теряется весь смысл.
Cere8ellum
10.09.2024 08:41Они оба передаются не всегда, а лишь при запросе на обновление accessToken'а.
Вернее, правильнее делать так.
gun_dose
10.09.2024 08:41Так в том то и дело. Если используются куки, то как правило в качестве клиента подразумевается браузер. А браузер всегда отправляет вместе с запросом все куки, что у него есть. Соответственно, оба токена автоматически передаются всегда вместе. Поэтому я и спрашиваю.
С другой стороны, тут интересная ситуация, если клиент - это браузер. Допустим браузер был закрыт какое-то время, за которое access-токен просрочился. И вот я запускаю браузер, открываю сайт. Он идёт получать данные, а токен просрочился. И если в куках есть сразу два токена, то можно авторизовать юзера сразу. Если же refresh-токен хранится не в куках (что более правильно), то он будет доступен только после загрузки страницы, т.к. обратиться к нему можно будет только через javascript. То есть в случае decoupled-приложения, когда фронт и бэк по отдельности, и бэк выдаёт фронту токены для авторизации, то единственно правильным решением будет при первоначальной загрузке фронта грузить заглушку с лоадером, которая сначала проверит наличие токенов на клиенте, затем актуализирует их, и только потом будет грузить данные. Получается, что в вебе использование jwt удобно только в SPA, где нет SSR.
Cere8ellum
10.09.2024 08:41Про хранение rt НЕ в кукис, согласен.А так можно например использовать тот же axios. По дефолту запретить в запросах печеньки:
withCredentials: false
И юзать их лишь при ообновлении токена.
Тут скорее вопрос архитектуры.
Ну а accessToken всё же лучше хранить например в памяти приложения, тот же state в React.
GRD-1 Автор
10.09.2024 08:41+1Абсолютно не секьюрно, вы правы! я отредактирую это место. Сам я всегда передаю токены от сервера клинету в теле запроса, а от ключента к серверу через заглоовок Authorization. Пример с куками привел только для полноты картины, поэтому и пропустил это.
Cere8ellum
10.09.2024 08:41Про Bearer хочется подметить. Не обязательно писать именно Bearer. Там может быть абсолютно любое слово. Просто принятый стандарт.
malinichevvv
Вот кажется, элементарная тема, но столько нюансов... И я более чем уверен что JWT использует не более 15% сервисов