JWT авторизация и сессии
Во многих простых гайдах про реализацию JWT (JSON Web Token) и у опытных разработчиков возникает подмена понятий авторизации и сессии. В результате возникают неочевидные на первый взгляд проблемы безопасности и усложнения реализации.
Разберем подробнее суть авторизации с использованием JWT в реальных приложениях.
Описанное ниже релевантно для web-приложений, доступ к которым осуществляется через браузер, и может быть некорректно для мобильных приложений или межсервисного взаимодействия.
Stateful сессии
В классических web-приложениях авторизация реализуется через сессии. Пользователь вводит корректные логин и пароль, и получает идентификатор сессии.
На стороне приложения токен сессии сопоставляется с пользователем и любыми другими данными. При каждом запросе проверяется токен сессии.
Для реализации такой схемы нужно где-то хранить сессии и лучше всего в БД, чтобы переживать рестарты.
Дополнительно нужно заложить инфраструктуру, схемы для вспомогательных данных, не относящихся к бизнес-процессу, а также делать больше чтений из БД.
Описанная схема (с возможными вариациями) – stateful авторизация, так как серверу приходится хранить состояние.
Преимущество JWT – stateless сессия
В сравнении с stateful авторизацией и классическими сессиями, с JWT все проще. Токен уже хранит в себе минимальные данные о пользователе, а для проверки подлинности достаточно секретного ключа, который с большой вероятностью находится в оперативной памяти.
Использование stateless подхода позволяет без дополнительных запросов в БД реализовать более строгие, но гибкие механики авторизации. Например, в payload JWT можно сохранить IP-адрес пользователя и User-Agent и сравнивать их с параметрами запроса. При этом такой механизм может быть реализован с учетом настроек пользователя через отсутствие значений или явным флагом в payload, который не может быть подделан без знания секретного ключа.
Также JWT может быть удобен в микросервисной архитектуре, когда каждый отдельный сервис имеет доступ к данным о пользователе даже без знания секретного ключа (при условии, что проверка подлинности произведена в сервисе авторизации).
Удобства JWT – это плюс, но оно может создавать ложное ощущение безопасности. При неправильном хранении и проверке токена могут возникнуть сложности, которых нет в классических сессиях.
Проблема небезопасного хранения токена
Большинство вводных гайдов про JWT, которые я видел, перекладывают ответственность за хранение токена на клиента: на успешную аутентификацию сервер отвечает JSON со строкой токена, а клиент сохраняет это значение, например в LocalStorage.
Аналогичная ошибка встречается не только у начинающих разработчиков, но и более опытных. Однажды я спросил тимлида бэкенда «почему мы отправляем токен авторизации в заголовках, вынуждая клиента обрабатывать его», на что он ответил «так работают REST API». Позже я понял, что он рассматривает JWT как API-токен и смешивает состояние и способ его передачи.
Мы не доверяем клиенту, а также мы не всегда знаем какой код JS выполняется на его стороне, поэтому лучше, чтобы любой клиентский код не имел доступа к чувствительной информации.
Поэтому единственным правильным способом хранить токен авторизации в браузере является http-only cookie, которые устанавливает сервер и к которым не имеет доступа JS код.
Кстати, Cookie – это тоже заголовок, поэтому формально единственным отличием от Authorization: Bearer является необходимость распарсить куки, что должно быть везде «из коробки».
Иногда дополнительно используют refresh-токены, которые хранятся в http-only куках, в то время как сам JWT остается доступен JS коду. Это практически не влияет на безопасность, уязвимость доступа к авторизации остается.
Проблема компрометации пользователя
Если пользователь скомпрометирован, мы хотим как можно скорее предотвратить любые действия пользователя.
В stateful авторизации мы могли бы просто удалить все строки сессий пользователя из БД, но JWT самодостаточен и, не считая проверок payload, например через exp, мы можем инвалидировать его, только изменив секретный ключ, что повлияет на сессии всех остальных пользователей.
Из ситуации банально есть два выхода: использовать refresh-токены или blacklist для токенов доступа. Вместе с этими решениями система перестает быть полностью stateless, но преимущества для микросервисов все еще остаются.
Использование refresh-токенов
При аутентификации помимо токена доступа пользователю выдается stateful refresh-токен.
Время жизни JWT при этом максимально уменьшается, вплоть до минуты, в то время как refresh-токен имеет значительно большее время жизни. Когда срок жизни JWT истекает (или немного заранее), клиент получает новый JWT, если он имеет валидный refresh-токен.
Дополнительно можно сделать refresh-токены одноразовыми, тогда пользователь вместе с токеном доступа получит новый refresh, а использованный станет невалидным.
При таком подходе, когда пользователь скомпрометирован, мы можем просто удалить все его refresh-токены, оставив злоумышленнику небольшое временное окно и надеяться, что он не успеет сделать ничего критичного за это время.
Использование черного списка токенов
Чтобы инвалидировать JWT, нам надо знать его значение. Значение JWT мы можем сохранить в БД, когда отдаем его клиенту.
В таком случае, когда клиент скомпрометирован, мы просто добавляем все его токены в черный список.
Либо просто реализуем белый список, авторизуя только известные приложению токены. Иными словами реализуем классические stateful сессии.
Резюме
Мы прошли полный круг от современной и приятной реализации к скучной старой сессии, но добавили немного ментальных усложнений.
JWT часто подменяет собой понятие сессии, но фактически является удобным access-токеном.