Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication
Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор) — это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.
Авторизация(authorization — разрешение, уполномочивание) — это проверка прав пользователя на доступ к определенным ресурсам.
Например после аутентификации юзер sasha получает право обращатся и получать от ресурса «super.com/vip» некие данные. Во время обращения юзера sasha к ресурсу vip система авторизации проверит имеет ли право юзер обращатся к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)
- Юзер c емайлом sasha_gmail.com успешно прошел аутентификацию
- Сервер посмотрел в БД какая роль у юзера
- Сервер сгенерил юзеру токен с указанной ролью
- Юзер заходит на некий ресурс используя полученный токен
- Сервер смотрит на права(роль) юзера в токене и соотвественно пропускает или отсекает запрос
Собственно п.5 и есть процесс авторизации.
Дабы не путатся с понятиями Authentication/Authorization можно использовать псевдонимы checkPassword/checkAccess(я так сделал в своей API)
JSON Web Token (JWT) — содержит три блока, разделенных точками: заголовок(header), набор полей (payload) и сигнатуру. Первые два блока представлены в JSON-формате и дополнительно закодированы в формат base64. Набор полей содержит произвольные пары имя/значения, притом стандарт JWT определяет несколько зарезервированных имен (iss, aud, exp и другие). Сигнатура может генерироваться при помощи и симметричных алгоритмов шифрования, и асимметричных. Кроме того, существует отдельный стандарт, отписывающий формат зашифрованного JWT-токена.
Пример подписанного JWT токена (после декодирования 1 и 2 блоков):
< alg: "HS256", typ: "JWT" >.< iss: "auth.myservice.com", aud: "myservice.com", exp: 1435937883, userName: "John Smith", userRole: "Admin" >.S9Zs/8/uEGGTVVtLggFTizCsMtwOJnRhjaQ2BMUQhcY
Токены предоставляют собой средство авторизации для каждого запроса от клиента к серверу. Токены(и соотвественно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и payload’e. Токен в итоге хранится на клиенте и используется при необходимости авторизации како-го либо запроса. Такое решение отлично подходит при разработке SPA.
При попытке хакером подменить данные в header’ре или payload’е, токен cтанет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.
access token — используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload)
refresh token — выдается сервером по результам успешной аутентификации и используется для получения нового access token’a и обновления refresh token’a
Каждый токен имеет свой срок жизни, например access: 30мин, refresh: 60дней
Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них такую информацию как пароли.
Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам прийдется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).
Схема создания/использования токенов (api/auth/login):
- Пользователь логинится в приложении, передавая логин/пароль на сервер
- Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token’а ( expires_in поле, в unix timestamp). Также в payloadrefresh token’a добавляется user_id
"accessToken": ". ", "refreshToken": ". ", "expires_in": 1502305985425
- Клиент сохраняет токены и время смерти access token’а, используя access token для последующей авторизации запросов
- Перед каждым запросом клиент предварительно проверяет время жизни access token’а (из expires_in )и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token
Схема рефреша токенов (одна сессия/устройство, api/auth/refresh-tokens):
- Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
- Если истекло клиент отправляет на auth/refresh-token URL refresh token
- Сервер берет user_id из payload’arefresh token’a по нему ищет в БД запись данного юзера и достает из него refresh token
- Сравнивает refresh token клиента с refresh token’ом найденным в БД
- Проверяет валидность и срок действия refresh token’а
- В случае успеха сервер:
- Создает и перезаписывает refresh token в БД
- Создает новый access token
- Отправляет оба токена и новый expires_in access token’а клиенту
С такой схемой юзер сможет быть залогинен только на одном устройстве. Тоесть в любом случае при смене устройства ему придется логинится заново.
Если рассматривать возможность аутентификации на более чем одном девайсе/браузере(мульти сессии): необходимо хранить весь список валидных рефреш токенов юзера. Если юзер авторизовался более чем на ±10ти устройствах(что есть весьма подозрительно), автоматически инвалидоровать все рефреш токены кроме текущего и отправлять email с security уведомлением. Как вариант список токенов можно хранить в jsonb(если используется PostgreSQL).
Схема рефреша токенов (мульти сессии/несколько устройств, api/auth/refresh-tokens):
Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я этот список храню в записи юзера в виде JSONB. Во время кажого процесса логина необходимо добавлять IP/Fingerprint пользователя-владельца логина/пароля в белый список.
------------------------------------------------------------------------------------------------- | id | username | refreshTokensMap | whitelistIP ------------------------------------------------------------------------------------------------- | 1 | alex | < refreshTokenTimestamp1: 'refreshTokenBody1', refreshTokenTimestamp2: 'refreshTokenBody2'>| ['111.111.111.111', '222.222.222.222'] -------------------------------------------------------------------------------------------------- Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
- Если истекло клиент отправляет на auth/refresh-token URL refresh token
- Сервер берет user_id из payload’arefresh token’a по нему ищет в БД запись данного юзера
- Проверяет IP юзера запрашиваемого обновление токенов с белым списком, если все успешно достает refresh token из записи в refreshTokensMap
- Если IP юзера отсутствует в белом списке, редиректит на страницу логина
- Удаляет старый рефреш токен
- Проверяет количество уже существующих решфреш токенов.
- Если их больше 10, удаляет все токены, создает новый и запиывает его в БД.
- Если их меньше 10 просто создает и записывает новый в БД.
- Создает новый access token
- Отправляет оба токена и новый expires_in access token’а клиенту
Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновлятся и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 10ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.
Как дополнительная мера можно вообще заблокировать данного юзера при попытке залогинится более чем на 10ти устройствах. С возможностью разблокировки только через email. Но в этом случае нам необходимо будет во время каждого рефреша проверять список токенов на наличие мертвых(не валидных).
Ключевой момент:
В момент рефреша то есть обновления access token’a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аунтефикации ? refresh token в момент рефреша сравнивает себя с тем refresh token’ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Внимание при обновлении refresh token’a продливается также и его срок жизни.
Возникает вопрос зачем refresh token’y срок жизни, если он обновляется каждый раз при обновлении access token’a ? Это сделано на случай если юзер будет в офлайне более 60 дней, тогда прийдется заново вбить логин/пароль.
В случае кражи токенов (когда когда юзер логинится только с одного устройства: одна сессия):
- Хакер воспользовался access token’ом
- Закончилось время жизни access token’на
- Клиент хакера отправляет refresh token
- Хакер получает новую пару токенов
- На сервере создается новая пара токенов(«от хакера»)
- Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
- Сервер перенаправляет юзера на форму аутентификации
- Юзер вводит логин/пароль
- Создается новая пара токенов >> пара токенов «от хакера» становится не валидна
Проблема: Поскольку refresh token продлевает срок своей жизни каждый раз при рефреше токенов >> хакер пользуется токенами до тех пор пока юзер не залогинится.
В случае кражи токенов (когда когда юзер логинится с нескольких устройства: мульти сессии):
Во время кажого процесса логина необходимо добавлять IP/Fingerprint пользователя-владельца логина/пароля в белый список. Таким образом при каждой попытке зайти с новой точки доступа придется перелогиниватся.
- Хакер воспользовался access token’ом
- Закончилось время жизни access token’на
- Клиент хакера отправляет refresh token
- Сервер смотрит IP адрес хакера
- Сервер не находит IP адрес хакера в белом списке и удаляет refresh token из БД (можно так же забанить этот IP)
- Сервер логирует попытку несанкционированного обновления токенов
- Сервер перенапрявляет харека на станицу логина. Хакер идет лесом
- Юзер пробует зайти на сервер >> обнаруживается что refresh token отсутствует
- Сервер перенаправляет юзера на форму аутентификации
- Юзер вводит логин/пароль
Пример имплементации:
Чтиво:
- Заметка базируется на: https://habrahabr.ru/company/Voximplant/blog/323160/
- https://tools.ietf.org/html/rfc6749
- https://www.digitalocean.com/community/tutorials/oauth-2-ru
- https://jwt.io/introduction/
- https://auth0.com/blog/using-json-web-tokens-as-api-keys/
- https://auth0.com/blog/cookies-vs-tokens-definitive-guide/
- https://auth0.com/blog/ten-things-you-should-know-about-tokens-and-cookies/
- https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
- https://habr.com/company/dataart/blog/262817/
- https://habr.com/post/340146/
- https://habr.com/company/mailru/blog/115163/
- https://scotch.io/tutorials/authenticate-a-node-js-api-with-json-web-tokens
- https://www.youtube.com/watch?v=Ngh3KZcGNaU
- https://www.youtube.com/playlist?list=PLvTBThJr861y60LQrUGpJNPu3Nt2EeQsP
- https://egghead.io/courses/json-web-token-jwt-authentication-with-node-js
- https://www.digitalocean.com/community/tutorials/oauth-2-ru
- https://github.com/shieldfy/API-Security-Checklist/blob/master/README-ru.md
And why JWT is bad
- http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
- http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/
- https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-1-8f7616113c14
- https://medium.com/@cjainn/anatomy-of-a-jwt-token-part-2-c12888abc1a2
- https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens
- https://t.me/why_jwt_is_bad
Как на сервере хранить токен стороннего сервиса?
Коллеги, подскажите, как в бэкенде хранить токен? У меня spring boot приложение пользуется сервисом с курсами валют, от которого был получен токен. Какие стандартные практики хранения здесь, если требуется безопасность?
Отслеживать
задан 22 янв в 7:07
303 1 1 серебряный знак 6 6 бронзовых знаков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 янв в 9:47
versetty777 versetty777
1,026 1 1 золотой знак 1 1 серебряный знак 12 12 бронзовых знаковзачем все эти танцы с бубном? какой смысл хранить токен в базе данных, заморачиваясь с криптованием? токен — не пароль, он не действует условно бесконечно. его время жизни ограничено. кроме того, даже если что-то произойдет (например, перезапуск сервера) у вас всегда есть механизм авторизации, позволяющий получить новый токен. а вы тратите системные ресурсы на обращение к баще и на расшифровку токена каждый раз, когда он необходим (а это с высокой вероятностью часто).
22 янв в 12:17
У вас под капотом спринг. Посему создайте простой интерфейс, имеющий методы:
public String authorize(); public String getToken();Для каждого внешнего ресурса, который требует авторизации, вы пишите отдельный сервис, который имплементирует данный интерфейс. Переопределяя данный методы, вы пишите логику авторизации на данном ресурсе (получение токена) для метода public String authorize(). В данном сервисе заводите потокобезопасную переменную , к примеру, таким образом:
AtomicReference token = new AtomicReference();Теперь переопределяете метод String getToken(), возвращая токен, спрятанный в AtomicReference. кроме того, для предыдущего метода String authorize() перед возвращением полученного при авторизации токена, вы пишите его сюда (в AtomicReference).
Здесь Можно создавать пару для токена и рефреш токена, можно коллекцию, в т.ч. на уровне супер класса (все зависит от вашей задачи)
Данный сервис добавляете на уровень отправки запроса (это может быть отдельный бин ресттемплейта, дополнительно сконфигурированный на добавление хедера авторизации, или что-либо другое), где отлавливаете исключения авторизации , связанные с истечением срока службы токена. Теперь при отправке каждого запроса (на уровне конфигурации ресттемплейта) вы дергаете метод getToken(), а при исключении авторизации — метод authorize() и повторяете запрос с новым токеном.
Все, теперь все запросы будут авторизироваться автоматически, подставляя и обновляя нужный токен
Локальное хранилище или куки? Безопасное хранение 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 на клиенте?

- безопасность
- разработка
LocalStorage vs Cookies: все, что нужно знать о безопасном хранении токенов JWT во Front-End

В переводе статьи LocalStorage vs Cookies: All You Need To Know About Storing JWT Tokens Securely in The Front-End, опубликованном webdevblog.ru, даются рекомендации по хранению и использованию JWT-токенов. Если кратко резюмировать статью, то в ней рекомендуется полученные от бекенда access токены хранить в приложение, а refresh токены — в куках. А вот если хотите узнать, почему именно такие рекомендации — читайте статью.
Токены JWT замечательны, но как их надежно хранить на фронтенде? Мы рассмотрим плюсы и минусы LocalStorage и cookie для хранения JWT.
Краткий обзор Access token и Refresh token
Токены доступа (Access token) обычно являются недолговечными токенами JWT, подписанными сервером и включенными в каждый HTTP-запрос к серверу для авторизации запроса.
Токены обновления (Refresh token) обычно представляют собой долговечные зашифрованные токенами JWT, хранящиеся в базе данных, которые используются для получения нового токена доступа по истечении срока его действия.
Где лучше хранить свои токены?
Существует два распространенных способа хранения токенов: в localStorage и в cookie. Есть много споров о том, какой из них лучше, но большинство людей склоняются к cookie из-за большей безопасности.
Давайте рассмотрим это сравнение чуть более подробнее.
Local Storage
Плюсы: Это удобно.
- Это чистый JavaScript и с ним удобней работать. Если у вас нет бэкэнда и вы полагаетесь на стороннее API, вы не всегда сможете установить определенные cookie для вашего приложения.
- Работает с API-интерфейсами, которые требуют, чтобы вы поместили свой токен доступа в заголовок запроса, типа такого Authorization Bearer $ .
Минусы: Такой способ уязвим к XSS атакам.
Атака XSS происходит, когда злоумышленник может запустить свой скрипт JavaScript на вашем сайте во время посещения его другим пользователем. Это означает, что злоумышленник сможет получит доступ к токену доступа, который вы сохранили в localStorage.
Cookies
Плюсы: Файл cookie может быть не доступен через JavaScript, поэтому он не так уязвим для атак XSS, как localStorage.
- Если вы используете флаги httpOnly, то cookie будут не доступны из JavaScript. Это означает, что даже если злоумышленник сможет запустить JS на сайте, он не сможет прочитать ваш токен доступа.
- Cookie автоматически отправляется в каждом HTTP-запросе на ваш сервер.
Минусы: В зависимости от варианта использования иногда вы не сможете хранить свои токены в файлах cookie..
- Размер файлов cookie ограничен 4 КБ, поэтому, если вы используете большие токены JWT, сохранение в файле cookie станет не возможным.
- Существуют сценарии, когда вы не можете использовать cookie напрямую для доступа к серверному API. Например когда требуется наличие токена в заголовке запроса.
Кратко о XSS атаках
Итак мы уже сказали что local storage уязвим, так как он легко доступен с помощью JavaScript, и злоумышленник может получить access token и использовать его. Однако, хотя cookie с httpOnly недоступны из JavaScript, это не означает, что с помощью cookie вы полностью защищены от XSS атак.
Если злоумышленник может запустить JavaScript в вашем приложении, он все равно сможет отправить HTTP-запрос на ваш сервер получив таким образом все токены. Но для злоумышленника это менее удобно, потому что он не сможет прочитать содержимое токена, хотя это и не всегда нужно.

Cookies и CSRF Атаки
CSRF Attack — это атака, которая позволяет сделать запрос от имени пользователя но без его ведома. Например, если веб-сайт принимает запрос на изменение электронной почты через такой запрос:POST /email/change HTTP/1.1Host: site.comContent-Type: application/x-www-form-urlencodedContent-Length: 50Cookie: session=abcdefghijklmnopqrstuemail=myemail.example.com
То злоумышленник может легко создать форму на вредоносном веб-сайте, которая отправит POST запрос на https://site.com/email/change со скрытым полем электронной почты, и cookie с токенами будут автоматически включены в этот запрос.
Однако это можно легко исправить, используя флаг cookie sameSite и добавив anti-CSRF token.
Вывод
Хотя куки все еще имеют некоторые уязвимости, они предпочтительнее по сравнению с localStorage, когда их применение возможно. И вот почему?
- Как localStorage, так и cookie уязвимы для атак XSS, но злоумышленнику сложнее выполнить эту атаку, когда вы используете cookie с httpOnly.
- Cookie уязвимы для атак CSRF, но их вероятность можно уменьшить с помощью флага sameSite и токенов защиты от CSRF (anti-CSRF tokens).
- Вы все еще можете использовать cookie, даже если вам нужно использовать заголовок Authorization: Bearer или ваш JWT больше 4 КБ. Это также согласуется с рекомендацией сообщества OWASP:
Итак, как мне использовать куки для сохранения моих токенов OAuth 2.0?
Напомним, вот несколько способов хранения ваших токенов:
- Вариант 1: Хранение Access Token в localStorage: но это выбор уязвим к XSS атакам.
- Вариант 2: Хранение Access Token в cookie с httpOnly: но этот выбор будет уязвим к CSRF атакам, хотя его можно уменьшить, что лучше чем XSS.
- Вариант 3: Хранение Refresh Token в cookie c httpOnly: это будет безопасно от CSRF атак, и немного лучше от XSS.
Мы рассмотрим, Вариант 3, так как он предлагает лучшие преимущества из трех вариантов.
Сохранять Access Token в памяти приложения а Refresh Token в cookie.
Шаг 1: Пользователь получает Access Token и Refresh Token когда проходит аутентификацию.
После аутентификации пользователя Сервер авторизации отдает пользователю access_token и refresh_token. Access_token может быть включен в тело ответа, refresh_token должен быть включен только в cookie.
Свойства токена в cookie:
- Используйте флаг httpOnly, чтобы из JavaScript не было возможности прочитать его.
- Используйте флаг secure = true, чтобы его можно было отправлять только через HTTPS.
- По возможности используйте флаг SameSite = strict, чтобы предотвратить CSRF атаки. Но это можно использовать только в том случае, если у сервера авторизации тот же домен, что и у клиентского приложения. Если это не так, ваш Сервер авторизации должен установить нужные заголовки CORS в серверной части или использовать другие методы, чтобы гарантировать, что запрос на обновление токена может быть выполнен только авторизованными веб-сайтами.
Шаг 2: Сохраните Access Token в памяти
Хранение токена в памяти означает, что вы поместили этот токен доступа в соответствующую переменную в своем приложение. Да, это означает, что access token исчезнет, если пользователь переключит вкладки или обновит страницу. Вот зачем у нас есть refresh token.
Шаг 3: Обновляйте access token, используя refresh token
Когда access token token пропадет или срок его действия истечет, используйте серверное API для получения нового токена используя refresh token, который был сохранен в cookie в шаге 1, и будет включен в каждый запрос (то есть сервер получит его через cookie ). После получения нового access token сможете использовать его для своих запросов API.
Это означает, что ваш токен JWT может быть больше 4 КБ, и вы также можете поместить его в заголовок авторизации.
Заключение
Если вы создаете механизм авторизации для своего веб-сайта или мобильного приложения, эти статьи могут помочь:
- What On Earth Is OAuth? A Super Simple Intro to OAuth 2.0, Access Tokens, and How to Implement it in your Site
- Passwordless Login with Email and JSON Web Token (JWT) Authentication using Next.js
- Here’s How to Integrate Cotter’s Magic Link to Your Webflow Site in Less Than 15 minutes!