О чем эта статья: мы разберемся, что такое 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 токенов").
Как передавать токены
от сервера к клиенту:
-
в заголовке запроса «Authrorisation» с добавлением слова Bearer
-
используя заголовок «Cookie»:
Set-Cookie: accessToken=<jwt>; HttpOnly; Sequre; SameSite=Strict;
Set-Cookie: refreshToken=<refresh-token>; HttpOnly; Sequre; SameSite=Strict;
от клиента к серверу:
-
в заголовке запроса «Authrorisation» с добавлением слова Bearer
Authorization: Bearer <jwt>
-
используя заголовок «Cookie»
Cookie: accessToken=<jwt>
Виды JWT
-
«access token» — проверяется при каждом обращении к защищенному API
-
многоразовый
-
присылается с каждым запросом к API в заголовке «authorization»
-
имеет короткий срок годности (обычно ~15 мин)
-
когда срок годности выходит, сервер возвращает #401
-
-
«refresh token» — токен для получения новой пары токенов (access и refresh)
-
одноразовый
-
имеет длительный срок годности (обычно несколько дней)
-
отправляется клиентом на эндпоинт ~/auth/refresh, когда истечет срок годности access токена и сервер вернет #401
-
-
«barer 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. Надеюсь, вышло не слишком нудно. Если появятся вопросы, пишите, буду рад любым комментариям.
Еще по теме:
Автор: GRD-1