Where should you store access tokens?
![]()
OAuth, or token-based authentication is a blessing from a security perspective, but very frustrating from any other. In this article I’m sharing a few platform-agnostic ideas and thoughts on safely storing access tokens in your frontend, while compromising neither security nor user experience.
For kittens: What is OAuth?
Let’s say you have a JavaScript frontend and a REST backend. The backend requires authorization to access all, or most of its routes. That means you must send a bearer token, a secure identifier in the HTTP request header. The backend validates the token, and allows or denies serving the request accordingly.
The OAuth authentication flow is relatively simple. Your frontend requests credentials from the user, like an email address and a password. (Let’s just stick to this simple example: this article isn’t going to deal with borderline paranoid authentication methods.) These credentials are presented to an OAuth provider, a third party service. The provider returns an encrypted authentication token, basically a garbled string of around 1000 characters. When you make a call to your backend, you attach this to the HTTP request’s header:
The backend reads this token, and validates it with the OAuth provider. This can be done either by actually connecting to the provider (online authentication) or decrypting the token with a secret key obtained previously from the OAuth provider (offline authentication). At the end of the process, you’re either good or no good for a response. And this, kids, is how OAuth works, in case you didn’t know.
But nothing is ever simple
After your frontend received the token, it will be attached to every single HTTP request you make in the future. So you need to store it somewhere. The easiest is to put it into the application state. As it’s just a regular string, you can stash it into a variable, store it in your state manager, or add it directly to the Axios default header. Then just reuse it every time.
This works fine until the user suddenly decides to reload the page. Then your state disappears, and you have to send the user to the login form again. The user will get offended, give you a negative feedback, go riot, complain to his friends about the trauma, and may need therapy for years. Since there’s a regrettable lack of remote activated face punching devices (I would make them mandatory for anyone who uses a computer) you will have to address the problem.
Where to store a token?
If your first thought was localStorage , I will have to rain on your parade. Unfortunately, localStorage isn’t secure, and whatever you put there will stay there forever. It’s not that difficult to extract anything from localStorage , and if the token is still valid by the time an attacker finds it, then congratulations, you successfully compromised your system.
Be aware: VueX and vuex-persist may seem like a solution, but it isn’t, because it also uses localStorage . It can be configured to use cookies instead, but it’s still very accessible for anyone who somehow accesses your computer.
An additional problem with localStorage is that multiple browser tabs or windows share the same instance. So if you sign in in two tabs as different users, one will overwrite the other, and there’ll be all kinds of trouble.
The right solution, at least in my opinion, is to store it not in the frontend, but the backend, as a session variable. Add a route which the frontend calls upon startup, and checks if a token had been saved on the other side. If the backend recognizes the frontend client, it can give back the token. If not, then the user should log in.
If you want to take it to another level, you can extend your token validation routine to check if there’s a token in the session in case a HTTP request arrived without a valid one.
It’s a good idea to set your session expiry time the same as tokens’ expiry, or a little bit shorter. This way you can be certain that a frontend will never receive an expired token. And of course, you should delete the session when the user logs out.
JWT — как безопасный способ аутентификации и передачи данных
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.
В простом понимании — это строка в специальном формате, которая содержит данные, например, ID и имя зарегистрированного пользователя. Она передается при каждом запросе на сервер, когда необходимо идентифицировать и понять, кто прислал этот запрос.
В этой статье разберу, что такое Access токен, Refresh токен и как с ними работать.
Для дальнейших разборов будет использован токен:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0
После того, как посетитель прошел авторизацию в нашей системе, указав свой логин и пароль, система выдает ему 2 токена: access token и refresh токен.
После чего посетитель, когда хочет получить с сервера данные, например, свой профиль, вместе с запросом он передает Access токен, как на примере выше. Сервер, получив его проверяет, что он действительный (об этом чуть ниже), вычитывает полезные данные из него (тот же user_id) и, таким образом, может идентифицировать пользователя.
Токен разделен на три основные группы: заголовок, полезные данные и сигнатура, разделенные между собой точкой.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 — это первая часть токена — есть заголовок. Она закодирована в Base64 и если её раскодировать, получим строку:
Это можно проверить прям в браузере, выполнив в консоле или js коде:
typ — это наш тип токена JWT. Alg — алгоритм шифрования HMAC-SHA256. Их может быть несколько, но здесь буду говорить именно об этом алгоритме.
Вторым блоком идет eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
Это есть полезные данные, так же закодированные в Base64. После раскодирования получим:
Данные могут быть любыми. Главное, чтобы по ним можно было идентифицировать пользователя. В нашем случае — это user_id и exp — время окончания действия текущего токена.
Поскольку необходимо ограничивать токен по времени, поле exp обязательно. По нему можно проверить, актуален ли токен или нет.
Последняя часть токена — наиболее важная. У нас это E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0
Как вы уже могли заметить — первые данные передаются практически в открытом виде и раскодировать их может любой. Но шифровать их нет необходимости. Цель токена — подтвердить, что эти данные не были изменены. Вот для этих целей и выступает сигнатура. И чтобы её сгенерировать нужен приватный ключ. Ну или некая секретная фраза, которая находится только на сервере. Только с помощью этого ключа мы можем создать сигнатуру и проверить, что она была создана именно с помощью его.
Она получается примерно следующим образом:
Берем заголовок, например <"alg":"HS256","typ":"JWT">и кодируем его в base64, получаем ту самую часть eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Тоже самое проделываем с данными eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
После этого склеиваем их и получаем eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9
Далее эти данные шифруем с помощью нашего алгоритма HMAC-SHA256 и ключа.
Для проверка токена необходимо проделать ту же операцию.
Берем склейку заголовок + данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру с токена и сверяем с результатом кодирования. Если результаты совпадают — значит данные подтверждены и можно быть уверенным, что они не были подменены.
Основной токен, про который шла речь выше, обычно имеет короткий срок жизни — 15-30 минут. Больше давать не стоит.
Как только время выйдет, пользователю снова придется проходить авторизацию. Так вот чтобы этого избежать, существует Refresh токен. С помощью него можно продлить Access токен.
В действительности, Refresh токен обязательно должен быть одноразовым. Его задача — получить новую пару токенов. Как только это было сделано, предыдущий токен будет считаться недействительным. Срок жизни Refresh токена уже может быть большим — до года, а может даже и больше.
У него, обычно, нет какой-то структуры и это может быть некая случайная строка.
Для проекта odo24.ru я использовал следующий подход.
Генерируется Access токен и после случайная строка, например T6cjEbghMZmybUd_fhE
С нашего нового Access токена eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9.E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0 беру последние шесть знаков, получаю Ed4wF0
Склеиваю и получаю рефреш токен T6cjEbghMZmybUd_fhEEd4wF0
Это сделано для привязки Access токена к Refresh. Для получения новых токенов необходимо передать эти два токена. Делается проверка на их связку и только после валидируется Access токен. Если и второй этап прошел успешно, тогда получаем с базы данных по текущему user_id рефреш токен и сверяем с тем, что к нам пришел. Если они совпадают, тогда генерируются новые токены и в базе данных обновляется Refresh токен на новый.
В моем случае я разделил оба токена и храню в разных местах. Access токен нужен только для идентификации пользователя и на клиенте (JS) он не нужен, поэтому он передается в Cookie (http only).
Refresh токен хранится в LocalStorage и используется только когда Access токен перестал быть актуальным.
Представим ситуацию, когда у нас каким-то образом украли Access токен. Да, это уже плохо и где-то у нас брешь в безопасности. Злоумышленник в этом случае сможет им воспользоваться не более чем на 15-30 минут. После чего токен «протухнет» и перестанет быть актуальным. Ведь нужен второй токен для продления.
Если украли Refresh токен, то без Access токена (который недоступен в JS) продлить ничего нельзя и он оказывается просто бесполезным.
Самая неприятная ситуация — это когда удалось увести сразу 2 токена. В этом случае злоумышленник сможет пользоваться системой неограниченное время. Точнее когда пользователь попытается войти в систему, его не пустит, т.к. его Refresh токен уже будет неактуальным, и ему придется вводить логин и пароль. Только в этом случае злоумышленник потеряет контроль над чужой учетной записью.
В своей реализации Refresh токена использовал общую длину 24 знака. Первые 6 знаков — это дата его «протухания», следующие 12 знаков — случайно сгенерированные данные. И в конце 6 знаков — это часть Access токена последней части сигнатуры.
Дату протухания внедрил прям в токен с той целью, чтобы не хранить эту информацию где-то в другом месте, например, в базе данных.
Дата содержит год, месяц, день, час и минуты. Хранится в ASCII
Кодирование даты на Golang:
Всю реализацию на Go можно изучить на Github-е
В этой статье попытался рассказать о взаимодействии двух токенов и как ими пользоваться. В сети достаточно много информации о Access токенах, однако мало, как мне показалось, информации о Refresh токенах.
Where to save a JWT in a browser-based application and how to use it
I’m trying to implement JWT in my authentication system and I have a few questions. To store the token, I could use cookies but it’s also possible to use localStorage or sessionStorage .
Which would be the best choice?
I have read that JWT protects the site from CSRF. However, I can’t imagine how that would work assuming I save the JWT token in cookie storage.
How would it then protect from CSRF?
Update 1
I saw some usage samples like the following:
How can I implement that when I make a request to server from the browser? I also saw that some implement the token in the URL:
If I would make a request via AJAX then I could set an header like jwt: [token] and then I could read the token from header.
Update 2
I installed the Advanced REST Client Google Chrome extension and was able to pass the token as a custom header. Is it possible to set this header data via Javascript when making a GET request to the server?
5 Answers 5
Choosing the storage is more about trade-offs than trying to find a definitive best choice. Let’s go through a few options:
Option 1 — Web Storage ( localStorage or sessionStorage )
- The browser will not automatically include anything from Web storage into HTTP requests making it not vulnerable to CSRF
- Can only be accessed by Javascript running in the exact same domain that created the data
- Allows to use the most semantically correct approach to pass token authentication credentials in HTTP (the Authorization header with a Bearer scheme)
- It’s very easy to cherry pick the requests that should contain authentication
- Cannot be accessed by Javascript running in a sub-domain of the one that created the data (a value written by example.com cannot be read by sub.example.com )
- ⚠️ Is vulnerable to XSS
- In order to perform authenticated requests you can only use browser/library API’s that allow for you to customize the request (pass the token in the Authorization header)
Usage
You leverage the browser localStorage or sessionStorage API to store and then retrieve the token when performing requests.
Option 2 — HTTP-only cookie
- It’s not vulnerable to XSS
- The browser automatically includes the token in any request that meets the cookie specification (domain, path and lifetime)
- The cookie can be created at a top-level domain and used in requests performed by sub-domains
- ⚠️ It’s vulnerable to CSRF
- You need to be aware and always consider the possible usage of the cookies in sub-domains
- Cherry picking the requests that should include the cookie is doable but messier
- You may (still) hit some issues with small differences in how browsers deal with cookies
- ⚠️ If you’re not careful you may implement a CSRF mitigation strategy that is vulnerable to XSS
- The server-side needs to validate a cookie for authentication instead of the more appropriate Authorization header
Usage
You don’t need to do anything client-side as the browser will automatically take care of things for you.
Option 3 — Javascript accessible cookie ignored by server-side
- It’s not vulnerable to CSRF (because it’s ignored by the server)
- The cookie can be created at a top-level domain and used in requests performed by sub-domains
- Allows to use the most semantically correct approach to pass token authentication credentials in HTTP (the Authorization header with a Bearer scheme)
- It’s somewhat easy to cherry pick the requests that should contain authentication
- ⚠️ It’s vulnerable to XSS
- If you’re not careful with the path where you set the cookie then the cookie is included automatically by the browser in requests which will add unnecessary overhead
- In order to perform authenticated requests you can only use browser/library API’s that allow for you to customize the request (pass the token in the Authorization header)
Usage
You leverage the browser document.cookie API to store and then retrieve the token when performing requests. This API is not as fine-grained as the Web storage (you get all the cookies) so you need extra work to parse the information you need.
Additional Notes
This may seem a weird option, but it does has the nice benefit that you can have storage available to a top-level domain and all sub-domains which is something Web storage won’t give you. However, it’s more complex to implement.
Conclusion — Final Notes
My recommendation for most common scenarios would be to go with Option 1, mostly because:
- If you create a Web application you need to deal with XSS; always, independently of where you store your tokens
- If you don’t use cookie-based authentication CSRF should not even pop up on your radar so it’s one less thing to worry about
Also note that the cookie based options are also quite different, for Option 3 cookies are used purely as a storage mechanism so it’s almost as if it was an implementation detail of the client-side. However, Option 2 means a more traditional way of dealing with authentication; for a further read on this cookies vs token thing you may find this article interesting: Cookies vs Tokens: The Definitive Guide.
Finally, none of the options mention it, but use of HTTPS is mandatory of course, which would mean cookies should be created appropriately to take that in consideration.
zmts / tokens.md
Аутентификация(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 блоков):
Токены предоставляют собой средство авторизации для каждого запроса от клиента к серверу. Токены(и соответственно сигнатура токена) генерируются на сервере основываясь на секретном ключе(который хранится на сервере) и payload’e. Токен в итоге хранится на клиенте и используется при необходимости авторизации какого-либо запроса. Такое решение отлично подходит при разработке SPA.
При попытке хакером подменить данные в header’ре или payload’е, токен станет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.
access token — используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload). Все поля в payload это свободный набор полей необходимый для реализации вашей частной бизнес логики. То бишь user_id и user_role не являются требованием и представляют собой исключительно частный случай. Сам токен храним не в localStorage как это обычно делают, а в памяти клиентского приложения.
refresh token — выдается сервером по результам успешной аутентификации и используется для получения новой пары access/refresh токенов. Храним исключительно в httpOnly куке.
Каждый токен имеет свой срок жизни, например access: 30 мин, refresh: 60 дней
Поскольку токены(а данном случае access) это не зашифрованная информация крайне не рекомендуется хранить в них какую либо sensitive data (passwords, payment credentials, etc. )
Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам придется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).
Как ставить куки ?
Для того что бы refreshToken кука была успешно уставленна и отправлена браузером, адреса эндпоинтов аутентификации( /api/auth/login , /api/auth/refresh-tokens , /api/auth/logout ) должны располагася в доменном пространстве сайта. Тоесть для домена super.com на сервере ставим куку с такими опциями:
Таким образом кука установится в браузер и прийдет на все эндпоинты по адресу super.com/api/auth/<any-path>
Если у нас монолит и за аутентификацию отвечает один и тот-же API, тут проблем не должно быть. Но если за аутентификацию отвечает отдельный микросервис, прячем его средствами nginx по выше указанному пути ( super.com/api/auth ).
Логин, создание сессии/токенов (api/auth/login):
- Пользователь логинится в приложении, передавая логин/пароль и fingerprint браузера (ну или некий иной уникальный идентификатор устройства если это не браузер)
- Сервер проверят подлинность логина/пароля
- В случае удачи создает и записывает сессию в БД < userId: uuid, refreshToken: uuid, expiresIn: int, fingerprint: string, . >(схема таблицы ниже)
- Создает access token
- Отправляет клиенту access и refresh token uuid (взятый из выше созданной сессии)
- Клиент сохраняет токены(access в памяти приложения, refresh сетится как кука автоматом)
На что нужно обратить внимание при установке refresh куки:
- maxAge куки ставим равную expiresIn из выше созданной сессии
- В path ставим корневой роут auth контроллера ( /api/auth ) это важно, таким образом токен получат только те хендлеры которым он нужен( /api/auth/logout и /api/auth/rerfesh-tokens ), остальные обойдутся(нечего зря почём отправлять sensitive data).
Стоит заметить, что процесс добавления сессии в таблицу должен имеет свои меры безопасности. При добавлении стоит проверять сколько рефреш-сессий всего есть у юзера и, если их слишком много или юзер конектится одновременно из нескольких подсетей, стоит предпринять меры. Имплементируя данную проверку, я проверяю только что бы юзер имел максимум до 5 одновременных рефреш-сессий максимум, и при попытке установить следующую удаляю предыдущие. Все остальные проверки на ваше усмотрение в зависимости от задачи.
Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновляться и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 5’ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.
Перед каждым запросом клиент предварительно проверяет время жизни access token’а (да берем expiresIn прямо из JWT в клиентском приложении) и если оно истекло шлет запрос на обновление токенов. Для большей уверенности можем обновлять токены на несколько секунд раньше. То есть кейс когда API получит истекший access токен практически исключен.
Что такое fingerprint ? Это инструмент отслеживания браузера вне зависимости от желания пользователя быть идентифицированным. Это хеш сгенерированный js’ом на базе неких уникальных параметров/компонентов браузера. Преимущество fingerprint’a в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша.
- Библиотека для хеширования: https://github.com/Valve/fingerprintjs2
- Более подробно: https://player.vimeo.com/video/151208427
- Пример ф-ции получения такого хеша: https://gist.github.com/zmts/b26ba9a61aa0b93126fc6979e7338ca3
В случае если клиент не браузер, а мобильное приложение, в качестве fingerprint используем любую уникальную строку(тот же uuid ) персистентно хранящуюся на устройстве.
Рефреш токенов (api/auth/refresh-tokens):
Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице(а надо бы в Redis’е). В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией, так званая рефреш-сессия.
- Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
- Если истекло клиент делает запрос на POST auth/refresh-tokens < fingerprint: string >в body и соответственно refreshToken куку.
- Сервер получает запись рефреш-сессии по UUID’у рефреш токена
- Сохраняет текущую рефреш-сессию в переменную и удаляет ее из таблицы
- Проверяет текущую рефреш-сессию:
- Не истекло ли время жизни
- На соответствие старого fingerprint’a полученного из текущей рефреш-сессии с новым полученным из тела запроса
Tip: Для отправки запроса с куками для axios есть опция
В момент рефреша то есть обновления access token’a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аутентификации ? refresh token в момент рефреша сравнивает себя с тем refresh token’ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены.
Вопрос зачем refresh token’y срок жизни, если он обновляется каждый раз при обновлении access token’a ? Это сделано на случай, если юзер будет в офлайне более 60 дней, тогда придется заново вбить логин/пароль.
- Front-end делает кол POST: api/auth/logout c refreshToken в куке или бади (лучше в куки)
- Front-end удаляет локально сохраненный в памяти accessToken
- Back-end удаляет запись из таблицы refreshSessions по refreshToken
accessToken умирает по истечению строка его жизни. Руками банить, удалять, хранить accessToken не нужно, это нарушает всю суть эксесс токена.
В случае кражи access токена и refresh куки:
- Хакер воспользовался access token’ом
- Закончилось время жизни access token’на
- Клиент хакера отправляет refresh token и fingerprint
- Сервер смотрит fingerprint хакера
- Сервер не находит fingerprint хакера в рефреш-сессии и удаляет ее из БД
- Сервер логирует попытку несанкционированного обновления токенов
- Сервер перенаправляет хакера на станицу логина. Хакер идет лесом
- Юзер пробует зайти на сервер >> обнаруживается что refresh token отсутствует
- Сервер перенаправляет юзера на форму аутентификации
- Юзер вводит логин/пароль
В случае кражи access токена, refresh куки и fingerprint’а:
Стащить все авторизационные данные это не из легких задач, но все же допустим этот кейс как крайний и наиболее неудобный с точки зрения UX (без примера в кодовой базе supra-api-nodejs ).
Предложу несколько вариантов решения данной проблемы:
- Хранить IP или Subnet залогиненного клиента
- Хакер воспользовался access token’ом
- Закончилось время жизни access token’на
- Хакер отправляет refresh куку и fingerprint
- Сервер проверяет IP хакера, хакер идет лесом
UX минус: нужно логинится с каждого нового IP.
- Удалять все сессии в случае если refresh токен не найден
- Хакер воспользовался access token’ом
- Закончилось время жизни access token’на
- Хакер отправляет refresh куку и fingerprint
- На сервере создается новый refresh токен («от хакера»)
- Хакер получает новую пару токенов
- Юзер пробует отправить запрос на сервер >> обнаруживается что refresh токен не валиден
- Сервер удаляет все сессии юзера, в последствии чего хакер больше не сможет обновлять access токен
- Сервер создает новую сессию для пользователя
UX минус: в каждом случае когда сервер не будет находить рефреш токен — будут сбрасиватся все сессии юзера на всех устройствах.
Зачем все это ? JWT vs Cookie sessions
Зачем этот весь геморой ? Почему не юзать старые добрые cookie sessions ? Чем не угодили куки ?