Вокруг JWT сложилась много распространенных заблуждений. Одно из них, например, что JWT зашифровано (на самом деле только подписано и закодировано base64url). На практике, я часто встречаюсь с довольно странными решениями, когда в JWT хранится только один идентификатор сессии (на самом деле если вы работаете с сессией то JWT вам не нужен). Или же в JWT хранится только один идентификатор пользователя, профайл которого запрашивается при каждом запросе из базы данных (на самом деле JWT имеет смысл, если Вы хотите уменьшить количество запросов в базу данных). Или, что еще более странно — сами JWT хранятся в базе данных.


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


Желание написать об этом у меня возникло уже давно. И после просмотра очередного проекта с немного странным использованием JWT я все же решился.


Начнем с самой главной демистификации. JWT может выглядеть вот так:


eyJhbGciOiJSUzI1NiJ9.eyJpcCI6IjE3Mi4yMS4wLjUiLCJqdGkiOiIwNzlkZDMwMGFiODRlM2MzNGJjNWVkMTlkMjg1ZmRmZWEzNWJjYzExMmYxNDJiNmQ5M2Y3YmIxZWFmZTY4MmY1IiwiZXhwIjoxNjA3NTE0NjgxLCJjb3VudCI6MiwidHRsIjoxMH0.gH7dPMvf2TQaZ5uKVcm7DF4glIQNP01Dys7ADgsd6xcxOjpZ7yGhrgd3rMTHKbFyTOf9_EB5NEtNrtgaIsWTtCd3yWq21JhzbmoVXldJKDxjF841Qm4T6JfSth4vvDF5Ex56p7jgL3rkqk6WQCFigwwO2EJfc2ITWh3zO5CG05LWlCEOIJvJErZMwjt9EhmmGlj9B6hSsEGucCm6EDHVlof6DHsvbN2LM3Z9CyiCLNkGNViqr-jkDKbn8UwIuapJOrAT_dumeCWD1RYDL-WNHObaD3owX4iqwHss2yOFrUfdEynahX3jgzHrC36XSRZeEqmRnHZliczz99KeiuHfc56EF11AoxH-3ytOB1sMivj9LID-JV3ihaUj-cDwbPqiaFv0sL-pFVZ9d9KVUBRrkkrwTLVErFVx9UH9mHmIRiO3wdcimBrKpkMIZDTcU9ukAyaYbBlqYVEoTIGpom29u17-b05wY3y12lCA2n4ZqOceYiw3kyd46IYTGeiNmouG5Rb5ld1HJzyqsNDQJhwdibCImdCGhRuKQCa6aANIqFXM-XSvABpzhr1UmxDijzs30ei3AD8tAzkYe2cVhv3AyG63AcFybjFOU8cvchxZ97jCV32jYy6PFphajjHkq1JuZYjEY6kj7L-tBAFUUtjNiy_e0QSSu5ykJaimBsNzYFQ

Если его декодировать base64url — миф о "секретности" сразу же разрушается:


{"alg":"RS256"}{"ip":"172.21.0.5","jti":"079dd300ab84e3c34bc5ed19d285fdfea35bcc112f142b6d93f7bb1eafe682f5","exp":1607514681,"count":2,"ttl":10} O2Mrn%!OPzN{hk11l\9Mkd    Z&?WJP%^DA8?*X??|!?C&D0Di?Ak
nue7b?B 6AV*9)S.jNv    `EcG9?*6kQDv_xz?Edgbs<wP(?"?K ?WxiHp<>,/EU]T?-Q+\}Pfbu?
7??ZTJ jhon??-v 6j9u
:!z#fEewQ*44    bl"&t!F    ?*s>]+U&8z-@Fap2p\S?}0h?Ty*?b1H/A?U3bA$)   j)

Первая часть в фигурных скобках называется JOSE Header и описана https://tools.ietf.org/html/rfc7515. Может содержать поля, из которых наиболее важное alg. Если задать {"alg":"none"} — токен считается валидным без подписи. И таким образом к Вашему API может получить доступ любой с токеном, сформированным вручную без подписи. В настоящее время большинство библиотек отвергают такие токены по умолчанию, но все же проверьте свои API на всякий случай.


Вторая часть в фигурных скобках — это полезная нагрузка. Немного неудобно, что в этой части размещаются и пользовательские данные, и стандартные заголовки, например jti — идентификатор токена и exp — срок окончания действия токена. Такие поля как срок окончания действия токена, срок начала действия токена могут проверяться или не проверяться библитеками для работы с JWT — в последнем случае придется в явном виде проверять срок действия на уровне приложения.


И, наконец, подпись. Подпись может быть сформирована при помощи разных алгоритмов. Наиболее удобно, если алгоритмы основаны на паре ключей (публичном и приватном). Тогда проверку подписи можно проводить публичным ключом, который можно без опасения распространять между серверами, принимающими JWT.


Таким образом JWT — это просто текст JSON, имеющий криптографическую подпись.


"Открыв" этот факт, можно поставить вопрос, в каких случаях рационально или не рационально использовать JWT. Можно предположить два основных кейса (если кто-то найдет еще — готов добавить в текст).


  1. Микросервисы. Данные (любые не обязательно авторизация, а например "корзина" с товарами и зафиксированными ценами товаров) формируются и подписываются на одном микросервисе, а используются на другом микросервисе, который проверяет подпись токена публичным ключом.


  2. Авторизация. Этот кейс может быть полезен и для монолита, если нужно сократить количество запросов в базу данных. При реализации "традиционной" сессии каждый запрос API генерирует дополнительный запрос профайла пользователя к базе данных. С JWT все, что берется в базе данных — помещается в JWT и подписывается.



Поняв это, мы поймем на какое время нужно выпускать JWT — на то время, которое не будет чувствительно, если в исходной информации что-то поменялось, а токен еще действует.


Все же есть случаи, когда информация слишком чувствительна, и нужно иметь возможность аннулировать JWT еще до окончания его срока действия. Для этого формируется реестр (например в Redis) аннулированных токенов. Поскольку время действия токена небольшое — реестр будет также небольшим, если удалять из реестра просроченные токены.


Следующий вопрос. Если время жизни JWT небольшое, то как обновлять JWT после окончания срока действия (это актуально для авторизационных токенов)? Для этого предлагается использовать второй, "условно-постоянный" токен с более продолжительным сроком действия. По истечении срока действия авторизационного токена его обновляют с использованием "условно-постоянного" токена.


Есть рекомендации обновлять "условно-постоянный" токен вместе с выдачей нового авторизационного токена. В этом случае некоторые рекомендуют аннулировать "условно-постоянный" токен сразу после его использвания, делая такое использование однократным. В последнем варианте, сессия может прерваться, если сервер зафиксирует выдачу нового токена, но из-за сетевых проблем этот токен не будет получен клиентом.


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


apapacy@gmail.com
9 декабря 2020 года