Подкаст про JWT: история, как работает
Предположим, вы владелец веб-сайта в 2014 году. Ваш сайт очень популярен, на нём зарегистрировано несколько тысяч человек. Но как ваш сайт понимает какой именно пользователь авторизован в данный момент?
Возможно, вы храните информацию об авторизованном пользователе в PHP сессии. Либо же вы пошли немного дальше, и храните эту информацию в зашифрованной куке. Если же вы любите всё новое и неизведанное, то наверняка уже знаете о существовании объединения JOSE – JSON Object Signing and Encryption group, целью которой c 2011 года, как раз и является стандартизация метода безопасной идентификации пользователя. Другими словами, участники этой группы как раз и пытались придумать единый и безопасный способ идентификации. Если что, идентификация – это процедура, после которой вы чётко понимаете какой конкретный пользователь перед вами. Вернее перед вашим веб-сайтом.
И, как вы догадываетесь из заголовка подкаста, результатом работы JOSE и был стандарт JWT. А если в 2014 вы ещё и достаточно терпеливы или ленивы, то буквально в следующем году группа IETF официально и стандартизирует JWT.
И сегодня я и расскажу вам про этот формат токенов доступа; про то, как он устроен; и про то, почему он лучше чем хранение сессий на сервере.
Из чего состоит JWT?
JWT токен состоит из трёх частей, разделённых точкой: заголовка, полезных данных или payload‘а и подписи. Каждая из частей кодируется форматом base64.
Заголовок никак не шифруется, и более того и существует, чтобы понять какой алгоритм шифрования используется.
Вторая часть токена или “полезные данные” содержит так называемые JWT claims. На русский это переводится как “заявки”, но для понимания я бы назвал их “полями” или “набором полей”.
Таких наборов существует три вида:
- Зарегистрированные или стандартные: это поля, в которых можно хранить, например, следующее:
- Имя субъекта, который выпустил токен
- Время, когда токен перестанет быть валидным
- Когда токен был выпущен, ну и так далее. Хоть эти поля и стандартизированы, но ни одно из них не является обязательным. Идём дальше
- Существует публичный набор полей, таких как;
- Полное имя пользователя
- Пол
- Ссылка на аватарку и так далее. Такой набор нужен для того, чтобы не ходить лишний раз в базу за информацией и экономить ресурсы
- И приватный набор полей для вашего приложения. Здесь можно указать любые данные на вкус и цвет вашего приложения, и они будут специфичными для него.
С заголовкой и полезными данными разобрались, и осталось самое главное, что и делает токен защищённым: подпись. Формула JWT подписи состоит из следующих компонентов:
- Из заголовка токена (header) или его первой части;
- Из полезного содержимого (payload) или второй части;
- И из секретного ключа (secret), который знает только ваш сервер.
И затем всё это складывается алгоритмом шифрования, который как раз и указан в заголовке. Как вы понимаете, если изменить любую из первых двух частей токена, то подпись перестанет быть валидной. За счёт чего JWT токен и можно считать защищённым.
Как отправлять JWT токен на сервер?
В целом, как пожелает ваша творческая душа, но обычно его отправляют двумя способами:
- Через куки
- Либо через HTTP заголовок Authorization с префиксом Bearer. Правда, в этом случае стоит придерживаться лимита в 8 КБ. Если вдруг вы привысили этот лимит, то стоит попробовать сократить количество данных во второй части токена (payload).
При использовании JWT важно помнить, что данные в первой и второй частях не шифруются, поэтому в них не стоит хранить чувствительную информацию. Но благодаря третьей части или подписи мы можем быть уверены в валидности токена.
А как же приложению понять, что пришедший токен валидный? Очень просто – сформировать подпись на основе секретного ключа, первой (header) и второй (payload) части. Если подпись совпадёт с пришедшей, то всё хорошо, можно пускать пользователя дальше.
Почему JWT токен лучше, чем сессии?
Дело в том, что при использовании сессий при каждом обращении пользователя мы должны обратиться к некоему хранилищу сессий, и достать хранимую информацию. И рано или поздно это хранилище сессий станет узким местом. А в случае с JWT – нет, – всё необходимую информацию о пользователе можно хранить в токене, а на сервере проверять лишь его валидность.
Но является ли JWT идеальным механизмом идентификации? К сожалению, нет. Есть большая проблема при работе с классическими JWT токенами – их нельзя отозвать на сервере. Единожды сгенерированный токен будет валиден до тех пор, пока не истечёт срок его действия. А поскольку его можно украсть, то это большая проблема.
И казалось бы: проблему можно решить, если положить в payload некий идентификатор, который при каждом обращении можно сверять на сервере со списком активных токенов. Но тогда мы лишаемся важного достоинства JWT токенов – отсутствие необходимости ходить в базу.
Хотя, можно хранить этот некий идентификатор, например, в Redis. Тогда получается вполне неплохо. Но важно тут помнить, что, конечно, весь токен в базе хранить ни в коем случае нельзя, потому что небезопасно. Поэтому я и сказал “идентификатор” – некая последовательность символов внутри полезных данных. А вообще, можно сделать ещё лучше!
Существует такое решение, как пара access и refresh токенов. В ней access токен живёт совсем немного – всего несколько минут, а refresh используется для обновления этой пары. Таким образом компроментация access токена позволит радоваться злоумышленнику совсем недолго, потому что совсем скоро access токен протухнет, а без refresh новую пару получить не удастся.
Как работать с JWT токенами на Go?
В этой задаче вам поможет библиотека, угадайте, с каким названием? Да, просто jwt.
У библиотеки удобный API и куча достоинств, загибайте пальцы:
- поддерживает многопоточное использование;
- не использует какие-либо зависимости ;
- в-третьих её код покрыт тестами;
- и самое главное то, что библиотека аж с 584 звёздочками, под MIT лицензией и не заброшена. Последний коммит был 8 октября. Кажется, тут будет актуальна шутка, что библиотека с прошлого года не обновлялась.
Как обычно, спасибо, что слушаете/смотрите/читаете. Услышимся через неделю!