О хранении JWT токенов в браузерах

Открытый стандарт JWT официально появился в 2015 (rfc7519) обещая интересные особенности и широкие перспективы. Правильное хранение Access токена является жизненно важным вопросов при построении системы авторизации и аутентификации в современном Web, где становятся все популярнее сайты, построенные по технологии SPA.
Неправильное хранение токенов ведет к их краже и переиспользованию злоумышленниками.
Так и где хранить?
Рассмотрим основные варианты хранения JWT Access токена в браузере:
- Local Storage/Session Storage – метод небезопасный и подвержен атакам типа XSS, особенно если Вы подключаете скрипты из сторонних CDN (добавление integrity атрибута не может гарантировать 100% безопасность), либо не уверены что подключаемые Вами скрипты не имеют возможности «слить» данные из хранилищ на сторону. Более того если Local Storage доступен между табами то Session Storage доступен только в одной вкладке и открытие сайта в новой вкладке лишь вызовет новый раунд авторизации/рефреша Access токена.
- Хранение токена в локальной переменной внутри замыкания тоже не обеспечивает должной безопасности потому что атакующий может, например, проксировать функцию fetch и отправить токен на левый сайт. Также это не решает проблему двух вкладок – нет безопасного способа передать токен из одной вкладки в другую.
- Cookies. Вот мы вернулись к старым «печенькам» которые использовались для хранения cookie sessions. Простое хранения Access токена в cookie чревато атакой CSRF. Более того оно не защищает от XSS атак. Для защиты от CSRF нужно ставить параметр Cookie SameSite в режим Strict– этим можно добиться того что все запросы, которые идут с других сайтов, не будут содержать Ваши credentials, что автоматически лишит атакующего возможности произвести CSRF атаку.
В отличии от первых двух вариантов здесь есть и плюс – Access токен невозможно получить через JS если использовать флаг httpOnly, добавление Secure также усилит защиту от сниффинга.
Что в итоге?
Cookies при правильном использовании являются адекватным и наиболее безопасным на данный момент решением для хранения JWT Access токена и должны следовать следующим правилам:
- Быть установленными для API домена/пути чтобы избежать оверхеда при запросах к статичным файлам (публичным картинкам/стилям/js файлам).
- Иметь флаг Secure (для передачи только по https).
- Иметь флаг httpOnly (для невозможности получения доступа из JavaScript).
- Атрибут SameSite должен быть Strict для защиты от CSRF аттак, запретит передачу Cookie файлов если переход к вашему API был не с установленого в Cookie домена.
- Content-Security-Policy – ограничение доверенных доменов для предотвращения возможных XSS атак
- Заголовок X-Frame-Options для защиты от атак типа clickjacking.
- X-XSS-Protection – принудительно включить встроенный механизм защиты браузера от XSS атак.
- X-Content-Type-Options – для защиты от подмены MIME типов.
Ограничения
Не смотря на то что атрибут SameSite поддерживается во многих популярных браузерах , существуют также браузеры которые не поддерживают его или поддерживают частично (привет IE и Safari для мака). Для этих случаев нужен fallback к CSRF токенам. В этом случае вместе с запросами к API надо передавать и CSRF токен. Правильный CSRF токен должен генерироваться сервером с учетом Fingerprint’a пользователя дабы минимизировать вероятность его подмены.
- Информационная безопасность
- Веб-разработка
- JavaScript
- API
Как на сервере хранить токен стороннего сервиса?
Коллеги, подскажите, как в бэкенде хранить токен? У меня spring boot приложение пользуется сервисом с курсами валют, от которого был получен токен. Какие стандартные практики хранения здесь, если требуется безопасность?
Отслеживать
задан 22 янв 2023 в 7:07
313 1 1 серебряный знак 7 7 бронзовых знаков
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
рекомендую хранить токен в безопасном месте на стороне сервера, используя шифрование и хранение его в базе данных. Это позволяет вам контролировать доступ к токену и защитить его от несанкционированного доступа.
import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.security.crypto.keygen.KeyGenerators; // создание соли String salt = KeyGenerators.string().generateKey(); // шифрование токена TextEncryptor encryptor = Encryptors.text("password", salt); String tokenEncrypted = encryptor.encrypt(token); // сохранение токена в базе данных tokenRepository.save(new Token(tokenEncrypted, salt)); // при извлечении токена из базы данных Token tokenFromDb = tokenRepository.findById(id); String salt = tokenFromDb.getSalt(); // дешифровка токена TextEncryptor decryptor = Encryptors.text("password", salt); String tokenDecrypted = decryptor.decrypt(tokenFromDb.getTokenEncrypted());
Отслеживать
ответ дан 22 янв 2023 в 9:47
versetty777 versetty777
1,026 1 1 золотой знак 2 2 серебряных знака 12 12 бронзовых знаков
зачем все эти танцы с бубном? какой смысл хранить токен в базе данных, заморачиваясь с криптованием? токен — не пароль, он не действует условно бесконечно. его время жизни ограничено. кроме того, даже если что-то произойдет (например, перезапуск сервера) у вас всегда есть механизм авторизации, позволяющий получить новый токен. а вы тратите системные ресурсы на обращение к баще и на расшифровку токена каждый раз, когда он необходим (а это с высокой вероятностью часто).
22 янв 2023 в 12:17
У вас под капотом спринг. Посему создайте простой интерфейс, имеющий методы:
public String authorize(); public String getToken();
Для каждого внешнего ресурса, который требует авторизации, вы пишите отдельный сервис, который имплементирует данный интерфейс. Переопределяя данный методы, вы пишите логику авторизации на данном ресурсе (получение токена) для метода public String authorize(). В данном сервисе заводите потокобезопасную переменную , к примеру, таким образом:
AtomicReference token = new AtomicReference();
Теперь переопределяете метод String getToken(), возвращая токен, спрятанный в AtomicReference. кроме того, для предыдущего метода String authorize() перед возвращением полученного при авторизации токена, вы пишите его сюда (в AtomicReference).
Здесь Можно создавать пару для токена и рефреш токена, можно коллекцию, в т.ч. на уровне супер класса (все зависит от вашей задачи)
Данный сервис добавляете на уровень отправки запроса (это может быть отдельный бин ресттемплейта, дополнительно сконфигурированный на добавление хедера авторизации, или что-либо другое), где отлавливаете исключения авторизации , связанные с истечением срока службы токена. Теперь при отправке каждого запроса (на уровне конфигурации ресттемплейта) вы дергаете метод getToken(), а при исключении авторизации — метод authorize() и повторяете запрос с новым токеном.
Все, теперь все запросы будут авторизироваться автоматически, подставляя и обновляя нужный токен
Защита JWT для аутентификации, httpOnly cookies, CSRF-токены
Часто говорят: Не храните токены в локальном хранилище (или хранилище сессий). Если какой-либо сторонний скрипт, который вы включаете в свою страницу, будет взломан, он сможет получить доступ ко всем токенам ваших пользователей.
localStorage действительно небезопасен. Но если не в localStorage, то где хранить токены пользователя?
Некоторые добавляют: JWT нужно хранить внутри httpOnly cookie — особого типа cookie, который отправляется только в HTTP-запросах к серверу и никогда не доступен (как для чтения, так и для записи) из JavaScript, запущенного в браузере.
Хорошая идея. Смотрим раздел использования HTTP cookies в MDN , чтобы узнать, что такое httpOnly cookie. httpOnly — это атрибут, добавляемый к cookies, который делает их недоступными на стороне клиента.
Хорошо. Как хранить JWT в куках httpOnly? Поиск в Google выдал эту статью Райана Ченки .
Он говорит, что существует два варианта безопасного хранения JWT:
- Память браузера (состояние React) — супербезопасно. Однако, если пользователь обновит браузер, JWT будет потерян, и вход потребуется снова. Не лучший пользовательский опыт.
- httpOnly cookie.
Конечная точка логина должена сгенерировать JWT и сохранить его в cookie:
res.cookie('token', token, < httpOnly: true >);
token предварительно генерируется в коде библиотекой (например jsonwebtoken ). httpOnly: true — это как раз то, что делает cookie невидимым для клиента. Когда httpOnly был установлен на false , мы можем получить доступ к содержимому куки в консоли с помощью document.cookie . Установка httpOnly: true предотвращает это.
Проблема в том, что клиент и сервер могут работать на разных портах, к примеру на localhost:3000 и localhost:5000 при разработке. Не существует такого понятия, как кросс-доменные куки — куки могут быть установлены только в том же домене, что и сервер. Как это обойти?
Если создать своего клиента с помощью Create-React-App, то мы можем использовать проксирование . Можно добавить например «proxy»: «http://localhost:4000» в package.json, таким образом мы ставим URL для API, к которому необходимо делать вызовы. Теперь путь будет относительным (т.е. вместо $/auth/login мы используем /auth/login ), этого достаточно.
После этого ответы от сервера станут приходить с заголовком Set-cookie , и можно будет увидеть Cookie в Chrome Dev Tools.
Как говорит Райан, Теперь, когда JWT находится в cookie, он будет автоматически отправляться API при любом обращении к нему. Так браузер ведет себя по умолчанию.
Следующий вопрос: как защитить маршруты, когда маркер хранится в cookie?
По определению, куки httpOnly не могут быть доступны клиенту, так как же мы можем защитить маршруты после того, как пользователь вошел в систему? Кто-то предложил идею в этом вопросе на StackOverflow . По сути, вы продолжаете генерировать httpOnly: true cookie, содержащий токен, и генерируете еще один, httpOnly: false , на этот раз без конфиденциальной информации, который только информирует о том, что пользователь вошел в систему. Следуя этой логике, вам даже не нужен cookie: получив успешный ответ API на вход, вы можете сохранить loggedIn: true в localStorage .
Итак, вы можете проверить куки httpOnly: false (или localStorage) и определить, вошел ли пользователь в систему или нет. Если нет, перенаправить на страницу авторизации.
А как получить доступ к cookie в React?
Конечно, есть два пути: воспользоваться библиотекой (например js-cookie ) или сделать это самостоятельно.
Ок, нам необходимо защитить маршруты, чтобы доступ к ним могли получить только те пользователи, которые вошли в систему (у которых cookie isLoggedIn имеет значение true ).
Думаю, многие понимают как создать , можно почитать статью Тайлера МакГинниса , она идеально подходит в качестве пошагового руководства.
const PrivateRoute = (< render: Component, . rest >) => ( render= Cookie.get('isLoggedIn') === 'true' ? ( /> ) : ( ) > /> );
Можно использовать PrivateRoute для защиты своего роута:
( shortUrl= setShortUrl= /> )> />
render: Component изнально был component: Component , потому что именно такой синтаксис пишут в туториалах. Однако это не работает, и может ввести в ступор. Можно прочитать этот ответ и понять, что ключ должен соответствовать атрибуту, который вы передаете в Route. Так, если вы передаете component= , то Private Route должен иметь component: Component . Если мой маршрут имеет render= , приватный маршрут должен иметь render: Component .
Следующий вопрос: как выйти из системы?
Поскольку cookie с маркером имеет значение httpOnly: true , он не будет доступен на клиенте, поэтому нам нужно, чтобы сервер удалил его. Как кто-то указал в этом вопросе на StackOverflow , вы можете обновить cookie на стороне сервера, просто оставив его пустым или забив его каким-нибудь мусором, также стоит не забыть поставить прошедшую дату экспирации.
В итоге, получаем на сервере что-то вроде:
res.cookie('token', 'deleted', < httpOnly: true >); res.cookie('isLoggedIn', false);
Есть что-то еще?
Боюсь, что да. Райан говорит о добавлении защиты от подделки межсайтовых запросов и добавлении анти-CSRF токена.
Что такое атака Cross Site Request Forgery
Здесь можно почитать на эту тему. Смысл таков: злоумышленник создает HTTP-запрос к некоторому сервису (например, к вашему счету в ebank), который спрятан внутри вредоносного сайта. Вас могут обманом заманить на этот сайт, и этот сайт может отправить HTTP-запрос. Суть атаки в том, что, поскольку вы аутентифицированы, вместе с запросом передаются куки аутентификации, и для сервера запрос является валидным.
Как известно, существуют меры защиты, которые должен предпринять сервер для защиты от этих атак: строгая политика CORS (при необходимости разрешая запросы только от определенных источников) и CSRF-токены.
Что такое токен CSRF
Здесь и здесь можно прочитать про этот токен. CSRF-токен на стороне сервера можно сгенерировать с помощью библиотеки csurf , и, после передачи клиенту в поле ответа, он устанавливается в качестве заголовка к каждому AJAX-запросу, который вы делаете к вашему серверу. Вы должны генерировать маркер как можно раньше в вашем приложении на сервере, потому что проверка CSRF происходит в middleware, которое размещается как можно раньше на сервере. Рекомендуют делать это следующим образом:
- useEffect в React App обращается к серверу для получения CSRF-токена по определенному пути. Этот токен генерируется библиотекой (например csurf ).
- Токен возвращается в поле ответа, а секретная часть, для проверки того что токен не был подделан, возвращается в виде cookie. Первый должен быть установлен в качестве заголовка к каждому последующему AJAX-запросу с помощью axios.default.headers.post[‘X-CSRF-Token] ‘. Второй должен быть возвращен клиенту как httpOnly и securecookie . Отправляется это дело в заголовке Set-cookie , и куки должны быть добавлены в каждый последующий запрос клиента.
Вроде все, но есть еще одна проблема. Предлагается создать конечную точку (endpoint), которая отправляет токен клиенту. Однако, если вы перейдете на страницу npm библиотеки csurf , там есть заголовок со ссылкой на эту страницу: Понимание CSRF, раздел о CSRF-токенах . Они говорят Не создавайте маршрут /csrf только для того, чтобы получить токен, и особенно не поддерживайте CORS на этом маршруте!
Очевидно, многие задаются этим вопросом — см. примеры здесь или здесь . У каждого программиста, похоже, есть свой способ. Многие согласны с тем, что не существует идеального решения.
Есть интересная статья Харлина Манна, где он объясняет, как снизить риски при использовании куки для хранения JWT, советую почитать.
Однако это еще не все!
И Райан , и Харлин говорят, что самым безопасным методом является хранение JWT в памяти и использование маркеров обновления.
Если есть возможность, храните JWT в состоянии приложения и обновляйте их либо через центральный сервер авторизации, либо с помощью refresh-токен в cookie.
Локальное хранилище или куки? Безопасное хранение JWT на клиенте
JWT (JSON Web Token) — это замечательный стандарт, основанный на формате JSON, позволяющий создавать токены доступа, обычно используемые для аутентификации в клиент-серверных приложениях. При использовании этих токенов возникает вопрос о том, как безопасно хранить их во фронтенд-части приложения. Этот вопрос нужно решить сразу же после того, как токен сгенерирован на сервере и передан клиентской части приложения.

Материал, перевод которого мы сегодня публикуем, посвящён разбору плюсов и минусов использования локального хранилища браузера ( localStorage ) и куки-файлов для хранения JWT.
Виды токенов
- Токены доступа (access tokens) обычно представляют собой короткоживущие JWT, подписанные сервером. Они включаются в каждый HTTP-запрос, выполняемый клиентом к серверу. Токены используются для авторизации запросов.
- Токены обновления (refresh tokens) обычно представлены долгоживущими токенами, хранящимися в базе данных и используемыми для получения нового токена доступа при истечении срока действия предыдущего токена.
Где именно следует хранить токены на клиенте?
Существует 2 распространённых способа хранения токенов на клиенте: локальное хранилище браузера и куки-файлы. О том, какой способ лучше, много спорят. Большинство людей склоняется в сторону куки-файлов из-за их лучшей защищённости.
Давайте сравним локальное хранилище и куки-файлы. Наше сравнение основано, преимущественно, на этом материале и на комментариях к нему.
Локальное хранилище
▍Преимущества
Основное преимущество локального хранилища заключается в том, что им удобно пользоваться.
- Работа с локальным хранилищем организована очень удобно, тут используется чистый JavaScript. Если у вашего приложения нет бэкенда, и вы полагаетесь на чужие API, не всегда можно запросить у этих API установку особых куки-файлов для вашего сайта.
- Используя локальное хранилище, удобно работать с API, которые требуют размещать токен доступа в заголовок запроса. Например — так: Authorization Bearer $ .
▍Недостатки
Главный недостаток локального хранилища — это его уязвимость к XSS-атакам.
- При выполнении XSS-атаки злоумышленник может запустить свой JavaScript-код на вашем сайте. Это означает, что атакующий может получить доступ к токену доступа, сохранённому в localStorage .
- Источником XSS-атаки может быть сторонний JavaScript-код, включённый в состав вашего сайта. Это может быть что-то вроде React, Vue, jQuery, скрипта Google Analytics и так далее. В современных условиях почти невозможно разработать сайт, в состав которого не входят библиотеки сторонних разработчиков.
Куки-файлы
▍Преимущества
Главное преимущество куки-файлов заключается в том, что они недоступны из JavaScript. В результате они не так уязвимы к XSS-атакам, как локальное хранилище.
- Если вы используете флаг HttpOnly и защищённые куки-файлы, это означает, что из JavaScript нельзя получить доступ к этим файлам. То есть, если даже атакующий сможет запустить свой код на вашей странице, ему не удастся прочитать токен доступа из куки-файла.
- Куки автоматически отправляются в каждом HTTP-запросе к серверу.
▍Недостатки
В зависимости от конкретных обстоятельств может случиться так, что токены в куки-файлах сохранить не удастся.
- Размер куки-файлов ограничен 4 Кб. Поэтому, если вы используете большие JWT, хранение их в куки-файлах вам не подойдёт.
- Существуют сценарии, при реализации которых вы не можете передавать куки своему API-серверу. Возможно и то, что какой-то API требует размещения токена в заголовке Authorization . В таком случае вы не сможете хранить токены в куки-файлах.
XSS-атаки
Локальное хранилище уязвимо к XSS-атакам из-за того, что с ним очень легко работать, используя JavaScript. Поэтому злоумышленник может получить доступ к токену и воспользоваться им в своих интересах. Однако, хотя HttpOnly-куки и недостижимы из JavaScript, это не означает, что вы, используя куки, защищены от XSS-атак, направленных на кражу токена доступа.
Если атакующий может запускать свой JS-код в вашем приложении, это значит, что он может просто отправить вашему серверу запрос, а токен будет включён в этот запрос автоматически. Такая схема работы просто не так удобна для атакующего, так как он не может прочитать содержимое токена. Но подобное нужно атакующим нечасто. Кроме того, при такой схеме работы злоумышленнику может быть выгоднее атаковать сервер, пользуясь компьютером жертвы, а не собственным.
Куки-файлы и CSRF-атаки
CSRF-атаки — это атаки, в ходе которых пользователя каким-то образом принуждают к выполнению особого запроса. Например, сайт принимает запросы на изменение адреса электронной почты:
POST /email/change HTTP/1.1 Host: site.com Content-Type: application/x-www-form-urlencoded Content-Length: 50 Cookie: session=abcdefghijklmnopqrstu email=myemail.example.com
В такой ситуации атакующий может создать форму со скрытым полем для ввода адреса электронной почты, которая отправляет POST-запрос на https://site.com/email/change . При этом сессионные куки автоматически будут включены в такой запрос.
Правда, от этой угрозы можно легко защититься, использовав атрибут SameSite в заголовке ответа и анти-CSRF токены.
Промежуточные итоги
Хотя и куки-файлы не отличаются полной неуязвимостью к атакам, для хранения токенов лучше всего, всегда, когда это возможно, выбирать именно их, а не localStorage . Почему?
- И локальное хранилище, и куки уязвимы к XSS-атакам, но злоумышленнику будет сложнее совершить атаку в том случае, если используются HttpOnly-куки.
- Куки уязвимы к CSRF-атакам, но риск таких атак можно смягчить, используя атрибут SameSite и анти-CSRF токены.
Использование куки-файлов для хранения токенов OAuth 2.0
Давайте кратко перечислим способы хранения токенов:
- Способ 1: хранение токенов в локальном хранилище. Этот способ подвержен XSS-атакам.
- Способ 2: хранение токенов в HttpOnly-куки. Этот способ подвержен CSRF-атакам, но риск подобных атак может быть смягчён. От XSS-атак этот вариант хранения токенов защищён немного лучше первого.
- Способ 3: хранение токенов обновления в HttpOnly-куки, а токенов доступа — в памяти. Этот способ хранения токенов безопаснее в плане CSRF-атак и немного лучше защищён от XSS-атак.
Почему хранение токена обновления в HttpOnly-куки безопаснее с точки зрения CSRF-атак?
Злоумышленник может создать форму, которая обращается к /refresh_token . В ответ на этот запрос возвращается новый токен доступа. Но атакующий не может прочитать ответ в том случае, если он использует HTML-форму. Для того чтобы не дать атакующему успешно выполнять fetch- или AJAX-запросы и читать ответы, нужно, чтобы CORS-политика сервера авторизации была бы настроена правильно, а именно — так, чтобы сервер не реагировал бы на запросы от неавторизованных веб-сайтов.
Как всё это настроить?
Шаг 1: возврат токена доступа и токена обновления при аутентификации пользователя
После того, как пользователь аутентифицируется, сервер аутентификации возвращает access_token (токен доступа) и refresh_token (токен обновления). Токен доступа будет включён в тело ответа, а токен обновления — в куки.
Вот что нужно использовать для настройки куки-файлов, предназначенных для хранения токенов обновления:
- Флаг HttpOnly — чтобы не дать прочесть токен из JavaScript.
- Флаг secure=true , что приведёт к тому, что данные будут передаваться только по HTTPS.
- Флаг SameSite=strict нужно использовать всегда, когда это возможно, что позволит защититься от CSRF-атак. Этот подход может использоваться только в том случае, если сервер авторизации относится к тому же сайту, что и фронтенд системы. Если это не так, тогда сервер авторизации должен устанавливать CORS-заголовки на бэкенде, или использовать другие методы для того чтобы убедиться в том, что запрос с токеном обновления может быть выполнен только авторизованным веб-сайтом.
Шаг 2: сохранение токена доступа в памяти
Хранение токена доступа в памяти означает, что токен, в коде фронтенда, записывают в переменную. Это, конечно, означает, что токен будет утерян в том случае, если пользователь закроет вкладку, на которой открыт сайт, или обновит страницу. Именно поэтому у нас имеется токен обновления.
Шаг 3: получение нового токена доступа с использованием токена обновления
Если токен доступа оказывается утраченным или недействительным, нужно обратиться к конечной точке /refresh_token . При этом токен обновления, который, на шаге 1, был сохранён в куки-файле, будет включён в запрос. После этого вы получите новый токен доступа, который сможете использовать для выполнения запросов к API.
Всё это значит, что JWT могут быть больше 4 Кб, и то, что их можно помещать в заголовок Authorization .
Итоги
То, о чём мы тут рассказали, должно дать вам базовую информацию о хранении JWT на клиенте, и о том, как сделать ваш проект безопаснее.
Как вы храните JWT на клиенте?

- безопасность
- разработка