Skip to content

Instantly share code, notes, and snippets.

@zmts
Last active April 30, 2024 15:26
Show Gist options
  • Save zmts/802dc9c3510d79fd40f9dc38a12bccfc to your computer and use it in GitHub Desktop.
Save zmts/802dc9c3510d79fd40f9dc38a12bccfc to your computer and use it in GitHub Desktop.
Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

Основы:

Аутентификация(authentication, от греч. αὐθεντικός [authentikos] – реальный, подлинный; от αὐθέντης [authentes] – автор) - это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя путём сравнения введённого им логина/пароля с данными сохранёнными в базе данных.

Авторизация(authorization — разрешение, уполномочивание) - это проверка прав пользователя на доступ к определенным ресурсам.

Например после аутентификации юзер sasha получает право обращатся и получать от ресурса "super.com/vip" некие данные. Во время обращения юзера sasha к ресурсу vip система авторизации проверит имеет ли право юзер обращатся к этому ресурсу (проще говоря переходить по неким разрешенным ссылкам)

  1. Юзер c емайлом sasha_gmail.com успешно прошел аутентификацию
  2. Сервер посмотрел в БД какая роль у юзера
  3. Сервер сгенерил юзеру токен с указанной ролью
  4. Юзер заходит на некий ресурс используя полученный токен
  5. Сервер смотрит на права(роль) юзера в токене и соотвественно пропускает или отсекает запрос

Собственно п.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). Сам токен храним не в localStorage как это обычно делают, а в памяти клиентского приложения.

refresh token - выдается сервером по результам успешной аутентификации и используется для получения нового access token'a и обновления refresh token'a

Каждый токен имеет свой срок жизни, например access: 30мин, refresh: 60дней

Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них какую либо sensitive data (passwords, payment credentials, etc...)

Роль рефреш токенов и зачем их хранить в БД. Рефреш на сервере хранится для учета доступа и инвалидации краденых токенов. Таким образом сервер наверняка знает о клиентах которым стоит доверять(кому позволено авторизоваться). Если не хранить рефреш токен в БД то велика вероятность того что токены будут бесконтрольно гулять по рукам злоумышленников. Для отслеживания которых нам прийдется заводить черный список и периодически чистить его от просроченных. В место этого мы храним лимитированный список белых токенов для каждого юзера отдельно и в случае кражи у нас уже есть механизм противодействия(описано ниже).

Схема создания/использования токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль на сервер
  2. Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token'а (expires_in поле, в unix timestamp). Также в payload refresh token'a добавляется user_id
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "9f34dd3a-ff8d-43aa-b286-9f22555319f6",
"expires_in": 1502305985425
  1. Клиент сохраняет токены и время смерти access token'а, используя access token для последующей авторизации запросов
  2. Перед каждым запросом клиент предварительно проверяет время жизни access token'а (из expires_in)и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token

Схема рефреша токенов (одна сессия/устройство, api/auth/refresh-tokens):

  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token'на
  2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
  3. Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера и достает из него refresh token
  4. Сравнивает refresh token клиента с refresh token'ом найденным в БД
  5. Проверяет валидность и срок действия refresh token'а
  6. В случае успеха сервер:
    1. Создает и перезаписывает refresh token в БД
    2. Создает новый access token
    3. Отправляет оба токена и новый expires_in access token'а клиенту
  7. Клиент повторяет запрос к API c новым 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']
-------------------------------------------------------------------------------------------------
  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token'на
  2. Если истекло клиент отправляет на auth/refresh-token URL refresh token
  3. Сервер берет user_id из payload'a refresh token'a по нему ищет в БД запись данного юзера
    1. Проверяет IP юзера запрашиваемого обновление токенов с белым списком, если все успешно достает refresh token из записи в refreshTokensMap
    2. Если IP юзера отсутствует в белом списке, редиректит на страницу логина
  4. Сравнивает refresh token клиента с refresh token'ом найденным в refreshTokensMap
  5. Проверяет валидность и срок действия refresh token'а (но если токен не валиден удаляет его сразу)
  6. В случае успеха сервер:
    1. Удаляет старый рефреш токен
    2. Проверяет количество уже существующих решфреш токенов.
    3. Если их больше 10, удаляет все токены, создает новый и запиывает его в БД.
    4. Если их меньше 10 просто создает и записывает новый в БД.
    5. Создает новый access token
    6. Отправляет оба токена и новый expires_in access token'а клиенту
  7. Клиент повторяет запрос к API c новым access token'ом

Таким образом если юзер залогинился на пяти устройствах, рефреш токены будут постоянно обновлятся и все счастливы. Но если с аккаунтом юзера начнут производить подозрительные действия(попытаются залогинится более чем на 10ти устройствах) система сбросит все сессии(рефреш токены) кроме последней.

Как дополнительная мера можно вообще заблокировать данного юзера при попытке залогинится более чем на 10ти устройствах. С возможностью разблокировки только через email. Но в этом случае нам необходимо будет во время каждого рефреша проверять список токенов на наличие мертвых(не валидных).

Ключевой момент:

В момент рефреша то есть обновления access token'a обновляются ОБА токена. Но как же refresh token может сам себя обновить, он ведь создается только после успешной аунтефикации ? refresh token в момент рефреша сравнивает себя с тем refresh token'ом который лежит в БД и вслучае успеха, а также если у него не истек срок, система рефрешит токены. Внимание при обновлении refresh token'a продливается также и его срок жизни.

Возникает вопрос зачем refresh token'y срок жизни, если он обновляется каждый раз при обновлении access token'a ? Это сделано на случай если юзер будет в офлайне более 60 дней, тогда прийдется заново вбить логин/пароль.

В случае кражи токенов (когда когда юзер логинится только с одного устройства: одна сессия):

  1. Хакер воспользовался access token'ом
  2. Закончилось время жизни access token'на
  3. Клиент хакера отправляет refresh token
  4. Хакер получает новую пару токенов
  5. На сервере создается новая пара токенов("от хакера")
  6. Юзер пробует зайти на сервер >> обнаруживается что токены невалидны
  7. Сервер перенаправляет юзера на форму аутентификации
  8. Юзер вводит логин/пароль
  9. Создается новая пара токенов >> пара токенов "от хакера" становится не валидна

Проблема: Поскольку refresh token продлевает срок своей жизни каждый раз при рефреше токенов >> хакер пользуется токенами до тех пор пока юзер не залогинится.

В случае кражи токенов (когда когда юзер логинится с нескольких устройства: мульти сессии):

Во время кажого процесса логина необходимо добавлять IP/Fingerprint пользователя-владельца логина/пароля в белый список. Таким образом при каждой попытке зайти с новой точки доступа придется перелогиниватся.

  1. Хакер воспользовался access token'ом
  2. Закончилось время жизни access token'на
  3. Клиент хакера отправляет refresh token
  4. Сервер смотрит IP адрес хакера
  5. Сервер не находит IP адрес хакера в белом списке и удаляет refresh token из БД (можно так же забанить этот IP)
  6. Сервер логирует попытку несанкционированного обновления токенов
  7. Сервер перенапрявляет харека на станицу логина. Хакер идет лесом
  8. Юзер пробует зайти на сервер >> обнаруживается что refresh token отсутствует
  9. Сервер перенаправляет юзера на форму аутентификации
  10. Юзер вводит логин/пароль

Пример имплементации:

Front-end:

Back-end:

Чтиво:

And why JWT is bad

@surebrec
Copy link

surebrec commented Apr 7, 2023

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

@leviskay
Copy link

leviskay commented Apr 8, 2023

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

@surebrec , Использование токенов - это по прежнему аутентификация. Просто до этого для аутентификации использовалась связка логин/пароль, так как ей может обладать только определенный человек. Теперь же используется токен, которым тоже может обладать только кто-то конкретный. Всего лишь способ аутентификации.

@oroojtatiev
Copy link

При хранении токена в памяти приложения, токен будет теряться по обновлению страницы.
Ребята пишут про передачу ID токена клиенту. Возникает вопрос:
У redis нет ID записи, когда мы записываем refreshToken в redis. Прибегать к рандомно генерированным ID?

ОК, допустим передали tokenId при аутентификации, этот tokenId фронтенд ведь тоже должен хранить где то в localStorage, чтобы не потерять. Но это опять приведет к уязвимости

@reslear
Copy link

reslear commented Jun 26, 2023

При хранении токена в памяти приложения, токен будет теряться по обновлению страницы. Ребята пишут про передачу ID токена клиенту. Возникает вопрос: У redis нет ID записи, когда мы записываем refreshToken в redis. Прибегать к рандомно генерированным ID?

ОК, допустим передали tokenId при аутентификации, этот tokenId фронтенд ведь тоже должен хранить где то в localStorage, чтобы не потерять. Но это опять приведет к уязвимости

  1. в этом то и прикол что токен должен каждый раз когда теряется из памяти, через refresh получать новый access и даже в новых запросах если уже expired делать форк и ещё один запрос на получение нового access (супер готовое решение для axios https://github.com/Flyrell/axios-auth-refresh ).
  2. в Redis, да генерируй UUID, можно и сессию конкретному пользователю сделать с его ключами вот пример на одном из моих проектов

Screenshot 2023-06-26 at 15 45 10

  1. Да должен но не access, а refresh, поэтому в статье есть пункт по дополнительным мерам безопасности в идеале использовать cookies, но к сожалению не очень юзабельно с capacitor ionic (хотя я ещё не пробовал https://capacitorjs.com/docs/apis/cookies ) В целом по поводу уязвимостей если попадется какой нибуть хацкер то ничего не поможет, а это покрывает как минимум 94% кейсов.

@oroojtatiev
Copy link

oroojtatiev commented Jun 26, 2023

@reslear

  1. В принципе, верно, даже у Google Firebase Authorization реализован такой же подход: каждый раз запускается метод getIdToken(forceRefresh) при обновлении страницы (и accessToken каждый раз записывать в память на клиенте). Просто гугл не генерит новый токен, если он не истек (Returns the current token if it has not expired. Otherwise, this will refresh the token and return a new one)

  2. По-ходу refreshTokenId из сессии фронтенд может доставать медотодом sessionStorage.getItem('whoami')?
    Сессия ведь встраивается в браузер в Application -> Session Storage. Этот айди думаю нужно отправлять в запросе рефреша, чтобы бек знал какой именно refreshToken следует обновить.
    Да токен ID можно вообще вроде хранить в самом jwt токене и потом тупо декодить его вместе с expirationAt на фронте

  3. RefreshToken не в localStorage хранить, а в Cookies sameSite, по статье

  4. Правильно ли я понимаю, что в редисе следует хранить список accessToken-ов с пометкой на их актуальность, а также хранить refreshToken на их актуальность тоже?
    И при logout и accessToken и refreshToken делать в редисе не валидными (добавлять в черный список).

@reslear
Copy link

reslear commented Jun 26, 2023

@reslear

  1. В принципе, верно, даже у Google Firebase Authorization реализован такой же подход: каждый раз запускается метод getIdToken(forceRefresh) при обновлении страницы (и accessToken каждый раз записывать в память на клиенте). Просто гугл не генерит новый токен, если он не истек (Returns the current token if it has not expired. Otherwise, this will refresh the token and return a new one)
  2. По-ходу refreshTokenId из сессии фронтенд может доставать медотодом sessionStorage.getItem('whoami')?
    Сессия ведь встраивается в браузер в Application -> Session Storage. Этот айди думаю нужно отправлять в запросе рефреша, чтобы бек знал какой именно refreshToken следует обновить.
    Да токен ID можно вообще вроде хранить в самом jwt токене и потом тупо декодить его вместе с expirationAt на фронте
  3. RefreshToken не в localStorage хранить, а в Cookies sameSite, по статье
  4. Правильно ли я понимаю, что в редисе следует хранить список accessToken-ов с пометкой на их актуальность, а также хранить refreshToken на их актуальность тоже?
    И при logout и accessToken и refreshToken делать в редисе не валидными (добавлять в черный список).
  1. Я думал об этом, но мне показалось проще удалять сесию и ставить на неё на всякий случай EX, и при logout просто удалять эти сессии, потом проверка нет сесси нет входа, но это не уместно если тебе нужно как телеграме/инст/vk - log сессий где когда с каго устройства и ручная кнопка удаления, тогда да - маркер.

@zardan4
Copy link

zardan4 commented Aug 6, 2023

If anyone is interested, I tried to make a scheme for this article. I'm not guru in creating something like this, but I did it to better understand JWT concept.
Link: https://gist.github.com/zardan4/48c00e31e732f465ca34173b703ff1a6

@reslear
Copy link

reslear commented Sep 29, 2023

кстати жаль что я уже все реализовал как потом нашел готовое решение https://github.com/lucia-auth/lucia

@Miskler
Copy link

Miskler commented Dec 10, 2023

Огромное уважение автору! Мало что есть по этой теме, тем более так структурированно(

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

Скажите. А как жить с таким подходом, если позволяется работа с более чем одной вкладкой? Ведь access token хранится в памяти и у каждой страницы он свой (так будет получаться) + при refresh-е токенов мы сам refresh token тоже обновляем, но с ним проблем +- не будет ибо он в куке лежит и его браузер синхронизирует. Но вопрос с access token-ом остается. Ведь в него вшивается refresh token, что бы находить сессию в бд и если refresh token обновился (и выдали новый access), то старый access token со старым refresh-ем уже не прокатит.

@zmts
Copy link
Author

zmts commented Jan 9, 2024

@Tsyklop Я храню refresh токен в пейлоаде access токена для необходимости обеспечить инвалидацию акксес токена в том случае если рефтокен(по которому акксес был выдан) был удален.

  1. Если у вас сервис не так требователен к безопасности удалите рефтокен из ексеса и возможно если данные не секретные по положите их в пейлоад еккеса
  2. Или как вариант положите все что нужно в access токен и зашифруйте его еще раз каким-то аля aes-256-cbc алгоритмом
  • Для обоих вариантов увеличьте количество одновременных сессий

@Devoter
Copy link

Devoter commented Jan 9, 2024

@zmts А в чем безопасность? Разве смысл не в том, чтобы refresh токен передавать только в момент обновления access токен? Да и время жизни refresh токена должно быть больше. Подпись JWT не обеспечивает безопасности и шифрования, она лишь обеспечивает возможность проверки корректности токена.

@Devoter
Copy link

Devoter commented Jan 9, 2024

@zmts Все равно странно. Access и refresh выдаются вместе, а не один от другого. Должен быть какой-то приватный ключ (общий для всех пользователей или отдельный для каждого), по которому генерируются токены. Причем, при обновлении access токена выдаётся и новый refresh. Понятно, что реализации разными могут быть - это здесь и обсуждается. Также можно добавлять в список вручную отозванных токенов банально по времени валидации и пользователю, тогда не придется хранить список сессий на сервере. То есть, прилетает запрос, смотрим начало действия и время жизни токена и сверяемся с базой отозванных токенов, можно ещё какой-нибудь уникальный идентификатор добавлять, дабы можно было инвалидировать отдельные устройства пользования, но тут уже детали.

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

@zmts Не совсем понял. То бишь что бы данный подход работал с несколькими вкладками, то нужно допустить несколько сессия с одинаковым fingerprint-ом? Если 10 вкладок открыл - 10 записей? особенность в том что refresh токен общий для всех. И это проблема. Ведь в бд храним refresh token.

@LuchkinDS
Copy link

@Tsyklop а как у вас обычная сессия работает, для каждой вкладки отдельная? токен хранится не памяти, а в localStorage или в куке.

@berdos1989
Copy link

berdos1989 commented Jan 9, 2024

Я храню refresh токен в пейлоаде access токена

@zmts

Зачем? Чтобы если кто то получит твой access token он получит и refresh token? Зачем тогда вообще иметь 2 токена? хватит 1 access.

Присоединюсь к вопросам @Devoter

Вообще вы ребята куда то не туда свернули. Храните токен в куки http only (защитит от xss) и не придумываете глупости)
Топ уязвимость это все равно использование секретного ключа "secret" для подписи hmac256)

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

@LuchkinDS refresh токен хранится в куке, а access токен в памяти. Даже если хранить в localStorage проблемы нескольких вкладок это не решает.

@Devoter
Copy link

Devoter commented Jan 9, 2024

@Tsyklop Решали подобный вопрос в рамках приложения. Если токены не в куках, то хранили токены в local storage. Нужно делать немного нетривиально. Добавляем в обработку запросов перехват ошибки 401 и проверяем - совпадают ли текущие токены с токенами в local storage. Если нет - берём оттуда и пробуем заново, если да - делаем refresh-запрос и обновляем данные в local storage. Казалось бы, все просто, но нужно как-то синхронизировать это дело между вкладками. И здесь на помощь приходить очередь из обещаний. Если получили ошибку 401, то вызываем функцию-перехватчик, назовем ее refresh. В ней проверяем длину очереди, если очередь пуста, то создаем создаём промис с запросом на обновление токенов и кладём ждуна в очередь. По кончанию запроса данные обновляются в local storege. Очередь должна быть обернута в промис. Таким образом, если послали несколько запросов подряд и получили ошибку, то рефреш выполнится только один раз, остальнве запросы на вкладке будут ждать результата. Можно, конечно, класть перед запросом в тот же local storage какой-нибудь флаг а-ля refreshing, но с учётом крайне низкой вероятности того, что запрос refresh будет выполняться одновременно на нескольких вкладках, этими заморочками можно пренебречь. Также не стоит забывать и о том, что в случае неудачного рефреша нужно всё-таки выполнить разлогинивание. В сухом остатке становится очевидно, что реализовывать JWT в браузере через куки для разных доменов гораздо проще и веселесее, чем вручную разруливатт все возможные варианты коллизий при обработке множества запросов во множестве вкладок. А вот для приложения (мобильного, к примеру), ручное хранение токенов на уровне клиента как раз удобно.

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

@LuchkinDS До этого не парился по этому поводу. Но ввели вход с одного устройства и теперь надо парится.

@zmts
Copy link
Author

zmts commented Jan 9, 2024

Я храню refresh токен в пейлоаде access токена

@zmts

Зачем? Чтобы если кто то получит твой access token он получит и refresh token? Зачем тогда вообще иметь 2 токена? хватит 1 access.

Все правильно говорите. Я сделал это для решения проблемы с инвалидацией акксес токена. Решение не фонтан, но проблему закрыло + не зря я написал юзать aes-256-cbc в дополнение

@zmts
Copy link
Author

zmts commented Jan 9, 2024

Даже если хранить в localStorage проблемы нескольких вкладок это не решает.

Если аксесс токен хранить в localStorage клиентское-JS приложение сможет достать его из любой вкладки. Но мы опять приходим к то му что это не безопасно.

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

@Devoter А у вас при refresh запросе сам refresh token оставался старым или создавался новый? Очень сложно такую логику понять со слов. Код бы облегчил задачу.

Но я вот смотрю в сторону одного токена (jwt или нет) в куку httpOnly. Это универсально для любого кол-ва вкладок. Но вот вопрос в обновлении такой куки - при каких условиях? можно (и нужно мне кажется) использовать fingerprint (или аналог). Но в плане безопасности меня настараживает что если токен угонят из кук - то это доступ к приложению без проблем вообще.

@Devoter
Copy link

Devoter commented Jan 9, 2024

@zmts А в чем небезопасность-то? local storage не шарится между сайтами, да, в рамках одного сайта разные вкладки будут иметь доступ к общему хранилищу, так в том и смысл. О какой безопасности мы говорим, если значения хранятся на клиенте? Вы хотите изолировать каждую из вкладок одного сайта, создавая отдельную сессию под каждую? Если так, что это какой-то крайне специфичный кейс, так как лично я часто разные страницы одного сайта в рамках одной сессии в разных вкладках открываю.

@Tsyklop При реыреше возвращается новая пара, но refresh отдельно не инвалидировался на сервере, варианты реализации для сервера я описал выше. Смысл был как раз показать - насколько муторно разруливать подобные вещи браузерным JS. Для примера потребуется целый проект написать демонстрационный, может, руки и дойдут когда - тогда у ну ссылку на хабр, но как по мне - лучше написать пример реализации через две http-only куки.

Если использовать только одну печеньку, то смысла в JWT и нет, это обычная сессия с автопродлением, которую нужно хранить в базе и как-то иметь возможность отзывать, в противном случае, разовый перехват токена становится большой проблемой, о чем вы сами и написали. Мое мнение такое: для приложений используем пару токенов, передаваемых в теле запроса или в заголовке, управляемых приложением, а в браузере - http-only куки, и пусть он сам разруливает между вкладками.

@Tsyklop
Copy link

Tsyklop commented Jan 9, 2024

@Devoter Две куки это access и refresh токены (jwt)?

@fls-dev
Copy link

fls-dev commented Jan 9, 2024

@Devoter Две куки это access и refresh токены (jwt)?

Вообще обычно так и делают, только как правило refresh только для определенного url фиксируется и уже на стороне сервера проверяется.
А про одно печенье слышал только для защиты от CSRF

@Devoter
Copy link

Devoter commented Jan 10, 2024

@Tsyklop все так. Причем, как сказали выше, refresh фиксируется для отдельного url или домена/поддомене, таким образом, он не отправляется вместе с access при каждом запросе, а лишь при refresh-запросе.

@neverovski
Copy link

@Tsyklop все так. Причем, как сказали выше, refresh фиксируется для отдельного url или домена/поддомене, таким образом, он не отправляется вместе с access при каждом запросе, а лишь при refresh-запросе.

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

@zakirovio
Copy link

добрый день. почему в таблице с рефреш сессией добавляется refreshToken в виде uuid, а не сам refresh_token в виде последовательности ASCII?

@khokm
Copy link

khokm commented Mar 23, 2024

@zakirovio нет необходимости хранить в базе весь токен, со всеми его заголовками, payload и подписью - достаточно хранить его идентификатор.

@khokm
Copy link

khokm commented Mar 23, 2024

@Devoter если интересно, проблему с одновременными запросами на Refresh я решаю так:
В базе добавили поле prevRefreshToken и prevRefreshExpiredAt . При выполнении рефреша сюда записывается предыдущий UUID, выглядит это как-то так:

UPDATE session 
SET refreshToken = {newRefreshToken},
prevRefreshToken = {currentRefreshToken}
prevRefreshExpiredAt = {currentDate + 1 minute}
WHERE
refreshToken = {currentRefreshToken}

UPDATE прошел? Отвечаем 200 и возвращаем новую пару токенов - клиент их получит и сохранит.
UPDATE не прошел? Выполняем такой запрос:

SELECT 1 FROM session 
WHERE
prevRefreshToken = {currentRefreshToken}
prevRefreshExpiredAt > {curDate}

Если ОК, то возвращаем просто 204. Клиент здесь просто ничего не делает.
А вот если даже этот запрос не прошел - то только тогда уже 401.

Подводя итог: при отправке 3 одновременных запросов на рефреш:
Первый - получит новые токены;
Второй, третий - не получат ничего

При этом до бесконечности пользоваться предыдущим токеном тоже нельзя - prevRefreshExpiredAt ограничивает это время до минуты (хотя это тоже много, это время должно быть не больше среднего RTT).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment