[Перевод] Перестаньте использовать JWT для сессий

47b353b641e74510ba225a6271bb65ee

К сожалению, в последнее время всё больше и больше людей советуют использовать JWT для управления пользовательскими сессиями в веб-приложениях. Это ужасная, ужасная идея, и в этом посте я объясню, почему.

Чтобы избежать недопонимания, я введу термины:

  • Stateless JWT — Токен JWT, который содержит сессионные данные, вшитые непосредственно в этот токен.

  • Stateful JWT — Токен JWT, который содержит лишь идентификатор сессии. Сессионные данные хранятся при этом на сервере.

  • Session token/cookie (сессионный токен либо кука/куки) — стандартный (опционально — подписанный) идентификатор сессии, как обычно оно и реализовывается во фреймворках. Сессионные данные хранятся на сервере.

Прим. пер.: Такое именование может показаться нелогичным: с чего бы называть схему stateless, если данные хранятся в токене?
Но здесь stateless относится ко всей системе в целом: если состояние вшито в токен, то хранение в системе не требуется, и поэтому вводится слово stateless.
Для stateful — аналогично.

Для ясности: эта статья не утверждает, что использовать JWT нельзя вообще — это всего лишь неподходящее средство в качестве механизма обеспечения сессионности, и использовать JWT в таком качестве попросту опасно. Нормальные сценарии использования существуют, но в других областях. В конце статьи я кратко пробегусь по этим сценариям.

Примечание

Многие люди ошибочно пытаются сравнить JWT и куки. Это сравнение бессмысленно, всё равно что сравнивать яблоки с апельсинами. Куки — это механизм хранения и передачи, а JWT — криптографически подписанные токены.

Они не противоположны друг другу, они могут использоваться как вместе, так и независимо друг от друга.
Корректное сравнение — это «сессии/Stateless JWT» или «куки / Local Storage»

В этой статье я буду сравнивать механизм сессий и stateless JWT, и затрону вопрос сравнения кук и Local Storage там, где это будет иметь смысл.

Заявленные преимущества JWT

Когда люди рекомендуют JWT, как правило, они подразумевают некоторые из этих преимуществ:

  • Легче горизонтально масштабировать

  • Легче использовать

  • Более гибкие

  • Более безопасные

  • Встроенное истечение (aka «протухание»)

  • Не надо спрашивать пользователя о «согласии на куки»

  • Предотвращают CSRF

  • Лучше работают на мобильных устройствах

  • Работают даже у тех, кто блокирует куки

Я разберу каждое из этих утверждений по отдельности и покажу, почему они неверны или обманчивы. Некоторые из объяснений будут слегка расплывчатыми, но это потому, что расплывчаты сами изначальные утверждения.
С радостью обновлю статью, чтобы разобрать более конкретные утверждения, мои контакты находятся в самом низу этой* статьи (прим. пер. — эти контакты действительно указаны на странице источника :-)).

Легче (горизонтально) масштабировать

Это единственное утверждение из списка, которое технически верно, но только если используются Stateless JWT. Однако, правда в том, что почти никому не нужна такого рода масштабируемость. Есть другие, более легкие способы масштабироваться, и, если вы не оперируете системой размаха Reddit, вам, вероятно, не понадобятся «сессии без состояния».

Вот некоторые примеры масштабирования сессий с состоянием:

  1. Если у вас запущено несколько процессов на одном сервере: допустимо запустить демон Redis (на этом сервере) для сессионного хранилища.

  2. Если ваше приложение запущено на нескольких серверах: отдельный сервер, на котором запущен Redis, исключительно для сессионного хранилища

  3. Если ваше приложение запущено на нескольких серверах в нескольких кластерах: липкие сессии (sticky session)

Все эти сценарии прекрасно поддерживаются текущим софтом и фреймворками. Ваше приложение вряд ли уйдет дальше второго шага.

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

Легче использовать

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

Более гибкие

Я так и не слышал внятного объяснения, в чем именно JWT гибче. Почти любая хоть сколько-то зрелая реализация сессий и так позволяет хранить в ней произвольные данные, и это ничем не отличается от того, как работает JWT. Насколько я могу судить, это просто баззворд. Если вы несогласны, пожалуйста, поделитесь примерами.

Более безопасные

Многие считают, что JWT «более безопасные», потому что используют криптографию. Куки можно и подписать, и подписанные куки более безопасны, чем неподписанные.
JWT в этом плане не уникален, и хорошие реализации сессий так же используют подписанные куки.

От того, что «но ведь там используется криптография», вещь не становится более безопасной. Криптография должна служить определенной цели, и быть эффективным ее решением. Неправильно примененные криптографические примитивы могут сделать решение менее безопасным.

Другое объяснение «большей безопасности», которое я слышал, заключалось в том, что JWT «не отправляется как куки». И это звучит просто бессмысленно, ведь куки — это всего лишь HTTP-заголовок, и в нем самом нет ничего небезопасного.
На самом деле, этот механизм особенно хорошо защищает от вредоносного кода на клиенте, но об этом чуть позже.

Если вы переживаете, что кто-то перехватит ваши сессионные куки, вам следует следует использовать защищенное соединение (TLS) — любой вид сессии будет перехватываемый, если соединение небезопасно, включая JWT.

Встроенное истечение (aka «протухание»)

Это ерунда, а не какая-то полезная фича. Истечение сессии может быть реализовано и на сервере, и в большинстве реализаций сделано именно так. Истечение сессии на стороне сервера предпочтительнее — оно позволяет приложению (серверу) очистить ненужные данные. И этого нельзя добиться, если используются Stateful JWT и вшитая в них дата истечения токена.

Не надо спрашивать у пользователя «согласие на куки»

Абсолютно неверно. Нет никакого «закона про куки» — законы, затрагивающие куки, на самом деле покрывают абсолютно любой вид персистентных идентификаторов, которые не являются строго необходимыми для работы сервиса.
Любой сессионный механизм попадает под действие подобных законов.

Вкратце:

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

  • Если вы используете сессию или токен для других целей (аналитика, отслеживание действий), то вам необходимо запросить согласие пользователя независимо от механизма хранения.

Предотвращает CSRF

По правде, нет. Грубо говоря, есть два способа хранить JWT:

  • В куках: такой способ все еще подвержен CSRF атакам, от которых все еще надо защищаться.

  • Где-то ещё, например, в Local Storage: да, этот способ не подвержен CSRF атакам, но теперь ваш сайт требует JavaScript для работы, и вы сделали приложение уязвимым для совершенно другого, потенциально худшего класса уязвимостей. Подробнее об этом поговорим ниже.

Единственный правильный способ защититься от CSRF атак — это CSRF-токен. Сессионный механизм здесь абсолютно ни при чем.

Лучше работают на мобильных устройствах

Глупость. Каждый актуальный браузер на мобильных устройствах поддерживает куки и, соответственно, сессии. То же верно и для любого крупного фреймворка для разработки мобильных приложений, и библиотеки для работы с HTTP.
Это вообще не проблема.

Работает даже у тех, кто блокирует куки

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

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

Недостатки

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

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

Они занимают больше места

JWT не слишком-то маленькие. Особенно при использовании Stateless JWT, где все данные вшиты непосредственно в токен, вы довольно быстро исчерпаете лимиты в куках. Вы могли бы решить их хранить в Local Storage — однако, …

Они менее безопасные

Когда JWT хранится в куке, он ничем не отличается от любого другого идентификатора сессии. Но когда вы сохраняете где-то еще, ваше решение становится уязвимым к целому классу атак, описанному в статье (а именно в разделе «Storing sessions»):

Возвращаемся к месту, на котором мы остановились: локальное хранилище (Local Storage), прекрасное нововведение HTML5, которое добавляет хранилище типа «ключ-значение» в браузеры. Так должны ли мы хранить наши JWT в локальном хранилище? Это может показаться разумным, учитывая размеры, которых может достигать токен. Куки обычно ограничены примерно 4Кб памяти. Для больших токенов вопрос хранения в куках может даже не стоять, и локальное хранилище будет очевидным решением. Однако, локальное хранилище не дает тех же гарантий безопасности, как куки.

Локальное хранилище, в отличие от кук, не посылает содержимое хранилища с каждым запросом. Единственный способ достать данные из хранилища — это использовать JavaScript, что означает, что любой злоумышленник, который может пройти Content Security Policy (CSP), может получить доступ к хранилищу и данным в нем.
А еще JavaScript не слишком интересуется тем, были ли такие данные переданы по HTTPS или нет. Для среды JavaScript это просто обычные данные, и обрабатываться в браузере они будут так же.
И теперь, после всех тех усилий, на которые шли инженеры, чтобы никто не стащил наши куки из хранилища кук (в оригинале cookie jar — «банка с печеньем» — прим.пер.), мы пытаемся обойти и проигнорировать все их ухищрения, которыми они нас вооружили. Как по мне, это шаг назад.

Проще говоря, использование кук — это не вопрос выбора, а необходимость, независимо от того, используете ли вы JWT или нет.

Невозможно инвалидировать единичный JWT

И это не все проблемы с безопасностью. В отличие от обычных сессий, — которые могут быть инвалидированы сервером, когда это необходимо, — отдельные Stateless JWT не могут быть инвалидированы.

По дизайну они валидны всегда, пока не истекло их время жизни, независимо от того, что произошло.

Это означает, что вы, например, не сможете отозвать сессию злоумышленника после обнаружения взлома. Также вы не можете инвалидировать старые сессии, когда пользователь меняет пароль.

По существу, в такие моменты вы бессильны, и вы не сможете инвалидировать сессию без построения сложной (и обладающей централизованным состоянием!) инфраструктуры, позволяющей выделять и инвалидировать такие токены.
А от этого потеряется весь смысл использования Stateless JWT.

Данные протухают

Отчасти пересекается с предыдущим пунктом, и является еще одной угрозой безопасности.
Как и в кэшах, данные в stateless-токене тоже в какой-то момент «протухнут», и больше не будут отражать актуальное состояние, сохраненное в базе данных.

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

Но — если серьезно — это также может означать, что у кого-то останется токен с сохраненной в нем ролью «Администратор», несмотря на то, что вы отозвали эту роль у пользователя.

А поскольку инвалидировать токен тоже нельзя, у вас нет никакого способа отобрать права Администратора, кроме как отключить всю систему.

Реализации либо менее испытаны в продакшене, либо не существуют

Вы можете подумать, что все эти проблемы специфичны для stateless JWT токенов, и по большей части будете правы. Однако, использование stateful токена эквивалентно использованию стандартной сессионной куки… за исключением использования зрелых, обкатанных в продакшене решений.

Существующие реализации сессий (такие, как express-session для Express, например), использовались в продакшене годами, и их безопасность вследствие этого значительно улучшилась. Вы не получите этого преимущества, если будете использовать JWT в качестве своеобразной замены сессионной куки — придется либо выкатывать свое собственное решение (со своими собственными уязвимостями), либо использовать сторонние решения, которые тоже не обкатаны.

Заключение

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

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

Так… для чего тогда подходят JWT?

В начале статьи я написал, что сценарии уместного использования JWT существуют, они всего лишь не подходят для обеспечения сессионности.

И это действительно так. Сценарии, в которых JWT особенно эффективны, это сценарии, в которых токены выступают как одноразовые авторизационные токены.

Выдержка из спецификации JSON Web Token:

JSON Web Token (JWT) — это компактный, безопасный для использования в URL способ представления заявок (claims), которые могут передаваться между двумя сторонами. […] позволяет этим заявкам быть подписанными цифровой подписью или защищенными по целостности с помощью кода аутентификации сообщения (MAC) и/или зашифрованными.

В этом контексте «заявки» могут быть чем-то вроде «команды», одноразовым авторизационным токеном, или любой другой вещью, которую можно выразить так:

«Привет, сервер Б, сервер А сказал мне, что я могу сделать <заявка>, и вот криптографическое доказательство».

Например, у вас может быть сервис, раздающий файлы, в котором пользователь должен аутентифицироваться, чтобы иметь возможность скачать свои файлы.
Но сами файлы хранятся на отдельном не имеющем состояния сервере.
В этом случае возможен сценарий, в котором ваше приложение (сервер А) выпускает одноразовый «токен загрузки», а клиент предъявляет этот токен серверу с файлами и получает возможность скачать нужный файл.

При таком использовании JWT соблюдаются следующие свойства:

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

  • Токены используются только один раз. Приложение выпускает новый токен на каждое новое скачивание, так что каждый токен используется для запроса ровно один раз. И здесь в принципе нет никакого постоянного состояния.

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

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

От переводчика

Считаю, что автор прав в главном: сессии на JWT превращаются в распределенное хранилище со своим букетом проблем. Игнорировать их невозможно, а исправление ведёт к собственной ad-hoc реализации сессионных хранилищ на сервере.

Буду рад обсудить статью здесь в комментариях, либо на полях моего уютненького телеграм-канала, в котором я пишу о своих находках в области Software Engineering.

Спасибо за внимание! Надеюсь, перевод получился цельным, а идея статьи была раскрыта.

© Habrahabr.ru