О чем эта статья: мы разберемся, что такое 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 лучше учетных данных:

  1. Учетные данные пользователя, как правило хранятся долго (месяцы). Как бы хорошо не был зашифрован запрос, при достаточном количестве времени его можно расшифровать. Если запрос, содержащий учетные данные перехвачен злоумышленником, у него будет много времени на расшифровку. Токены доступа имеют ограниченный срок годности (обычно ~15 минут). Этого времени не достаточно, чтобы расшифровать надежный шифр. К тому времени, когда зловредный алогритм расшифрует запрос, токен уже выйдет из обращения и будет бесполезен.

  2. Использовать учетные данные, это медленно. Для валидации учетных данных сервер должен запросить их сохраненную копию из БД и сравнить с данными, которые пришли в запросе. Обращение к БД — дорогостоящая процедура, она сильно увеличивает время обработки запроса. Токены, с другой стороны, не требуют обращения к БД для валидации. Это позволяет снизить нагрузку на БД и ускорить обработку запросов сервером.

Время жизни токенов. Каждый токен имеет определенный срок годности. Эта информация зашита в его теле. При валидации, сервер извлекает данные из токена и проверяет, не истек ли срок.

Как хранить токены. Существует мнение, что некоторые виды токенов нужно хранить в БД вместе с остальными данными, это не так. 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 частей разделенных точкой:

Структура JWT
Структура JWT
  • 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, писать этот код вручную нет никакого смысла, но понимать, как это происходит важно.

валидация JWT
валидация 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

использование JWT
использование JWT

Что тут происходит:

  1. ввод учетных данных, получение новой пары токенов

  2. запрос данных с access токеном

  3. проверка, не внесен ли токен в black-list

  4. валидация access токена, передача данных на клиент

Обновление JWT

Обновление JWT
Обновление JWT

Что тут происходит:

  1. запрос данных с access токеном

  2. валидация access токена не прошла, возврат ошибки #401

  3. запрос обновления токенов с refresh токеном,

  4. проверка, не внесен ли токен в black-list

  5. получение версии учетных данных из БД

  6. валидация токена, проверка версии токена

  7. генерация новой пары токенов, отправка на клиент

Неуспешное обновление JWT

Неуспешное обновление JWT
Неуспешное обновление JWT

Что тут происходит:

  1. запрос обновления токенов

  2. проверка, не внесен ли токен в black-list

  3. получение версии учетных данных из БД

  4. неуспешная валидация токена, редирект на страницу ввода учетных данных

В заключение

Вот, пожалуй, и все, что я хотел рассказать о JWT. Надеюсь, вышло не слишком нудно. Если появятся вопросы, пишите, буду рад любым комментариям.

P.S. Эта статья, на самом деле, часть исследования, посвященного методам авторизации. Основной материал я опубликовал в статье "Auth сервис без библиотек". Возможно, он будет интересен в контексте изучения JWT.

Еще по теме:

Комментарии (14)


  1. malinichevvv
    10.09.2024 08:41

    Вот кажется, элементарная тема, но столько нюансов... И я более чем уверен что JWT использует не более 15% сервисов


  1. lear
    10.09.2024 08:41
    +4

    От JWT нет пользы в большинстве проектов.

    Он нужен только для оптимизации запроса данных из других сервисов, только тогда выигрыш производительности нивелирует проблемы безопасности.

    Если вы в JWT храните недостаточно данных (id пользователя и, допустим, роль) и затем запрашиваете все остальные данные в бд, то совокупного выигрыша нет.


  1. GRD-1 Автор
    10.09.2024 08:41

    Случайно "отклонил" комментарий к статье, извиняюсь. Мне бы хотелось все-таки на него ответить.

    Комментарий был такой:

    "Как бы хорошо не был зашифрован запрос, при достаточном количестве времени его можно расшифровать. Если запрос, содержащий учетные данные перехвачен злоумышленником, у него будет много времени на расшифровку."

    JWT токен не шифруется, а подписывается. Все что он содержит можно просто просмотреть декодировав текст из base64. Атака может быть, только на получение приватного ключа шифрования, если он слабый, для дальнейшей подмены токенов. —

    Мой ответ:
    Здесь сравнивалась пересылка данных в зашифрованном запросе (например, по протоколу tls). Обсуждаемая атака предполагалась на ключ шифрования запроса, а не на приватный ключ токена. Поскольку подбор ключей шифрования — трудоемкая операция, занимающая много времени, к моменту подбора ключа токен уже устареет и будет бесполезен, а вот пароль — нет. Отсюда я сделал вывод, что пересылка токенов безопаснее, чем пересылка паролей. Согласен, формулировка мутная, нужно перефразировать, спасибо.


  1. santanico
    10.09.2024 08:41

    Затронута интересная тема, спасибо! Но со своей стороны не могу не добавить:

    1. refresh token изначально выдается при авторизации пользователя вместе с access token

    2. Поле version инкрементируется при каждом изменении данных авторизации (пароля)

    3. Описываемая схема «Контроль версий» используется для того, чтобы заблеклистить refresh token (потому что система его не помнит и он не может быть просто добавлен в blacklist)


    1. northrop
      10.09.2024 08:41
      +1

      Еще бы услышать хотелось в чем смысл наличия id token и почему недостаточно иметь вместо него access token. А также про scope, implicit and explicit flow.


      1. GRD-1 Автор
        10.09.2024 08:41

        Если коротко - я не знаю. Насколько я понимаю, этот вид токенов используется в OpenId - протоколе, работающем поверх протокола OAuth2. Там архитектура значительно сложнее, чем описанная мной тут. Спасибо за вопрос, буду разбираться!


  1. SamDark
    10.09.2024 08:41

    Ответ: конечно можно, но количество таких токенов будет существенно больше. Это увеличит объем потребляемой памяти, и замедлит запросы к БД. больше данных => медленнее поиск в БД.

    В Redis поиск по ключу — это алгоритм сложности O(1). То есть не зависит от количества ключей и не замедлит запросы к БД.

    Также Redis хранит данные довольно компактно и лимит по ключам на одном инстансе даже со скромным количеством памяти — десятки миллионов. Учитывая expiration, упереться в это вряд-ли получится для большинства сервисов.

    Отсюда ещё раз вопрос: а так ли нужен JWT если инвалидировать токены нужно?


    1. GRD-1 Автор
      10.09.2024 08:41

      Про то, что алгоритм сложности редиса O(1) я не знал. Сейчас почитал, спасибо.
      По вашему вопросу: уточните пожалуйста, а с чем мы сравниваем использование JWT?

      Я вижу у JWT такие преимущества:
      1. JWT универсальны. Вам будет легче проводить интеграцию со сторонними сервисами, если вы будете использовать универсальный формат ключей доступа.
      2. У JWT есть payload. Мы можем добавить туда uuid пользователя, uuid клиента, и дополнительно все, что захотим. В любом стеке для них есть библиотеки, которые предлагают набор параметров для валидации и сами проводят ее. Если мы будем хранить ту же информацию, скажем, в редисе, нам придется самостоятельно писать код валидации.
      3. Тому, кто придет работать с тем же кодом после нас, придется разбираться в логике валидации вмето того, чтобы использовать стандартные методы библиотек.
      4. У библиотек, как правило, есть нормальная документация, где можно посмотреть зачем нужен каждый параметр.

      Но, опять же, нужно понимать, с чем мы сравниваем JWT.


  1. gun_dose
    10.09.2024 08:41
    +2

    Set-Cookie: refreshToken=<refresh-token>;

    Насколько секьюрно хранить рефреш-токен в кукисах? Ведь в таком случае оба токена всегда передаются одновременно и полностью теряется весь смысл.


    1. Cere8ellum
      10.09.2024 08:41

      Они оба передаются не всегда, а лишь при запросе на обновление accessToken'а.

      Вернее, правильнее делать так.


      1. gun_dose
        10.09.2024 08:41

        Так в том то и дело. Если используются куки, то как правило в качестве клиента подразумевается браузер. А браузер всегда отправляет вместе с запросом все куки, что у него есть. Соответственно, оба токена автоматически передаются всегда вместе. Поэтому я и спрашиваю.

        С другой стороны, тут интересная ситуация, если клиент - это браузер. Допустим браузер был закрыт какое-то время, за которое access-токен просрочился. И вот я запускаю браузер, открываю сайт. Он идёт получать данные, а токен просрочился. И если в куках есть сразу два токена, то можно авторизовать юзера сразу. Если же refresh-токен хранится не в куках (что более правильно), то он будет доступен только после загрузки страницы, т.к. обратиться к нему можно будет только через javascript. То есть в случае decoupled-приложения, когда фронт и бэк по отдельности, и бэк выдаёт фронту токены для авторизации, то единственно правильным решением будет при первоначальной загрузке фронта грузить заглушку с лоадером, которая сначала проверит наличие токенов на клиенте, затем актуализирует их, и только потом будет грузить данные. Получается, что в вебе использование jwt удобно только в SPA, где нет SSR.


        1. Cere8ellum
          10.09.2024 08:41

          Про хранение rt НЕ в кукис, согласен.А так можно например использовать тот же axios. По дефолту запретить в запросах печеньки:

          withCredentials: false

          И юзать их лишь при ообновлении токена.

          Тут скорее вопрос архитектуры.

          Ну а accessToken всё же лучше хранить например в памяти приложения, тот же state в React.


    1. GRD-1 Автор
      10.09.2024 08:41
      +1

      Абсолютно не секьюрно, вы правы! я отредактирую это место. Сам я всегда передаю токены от сервера клинету в теле запроса, а от ключента к серверу через заглоовок Authorization. Пример с куками привел только для полноты картины, поэтому и пропустил это.


  1. Cere8ellum
    10.09.2024 08:41

    Про Bearer хочется подметить. Не обязательно писать именно Bearer. Там может быть абсолютно любое слово. Просто принятый стандарт.