Skip to content

Instantly share code, notes, and snippets.

@zmts zmts/tokens.md
Last active Oct 16, 2019

Embed
What would you like to do?
Про токены, 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)

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

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

Поскольку токены это не зашифрованная информация крайне не рекомендуется хранить в них такую информацию как пароли.

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

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

  1. Пользователь логинится в приложении, передавая логин/пароль на сервер
  2. Сервер проверят подлинность логина/пароля, в случае удачи генерирует и отправляет клиенту два токена(access, refresh) и время смерти access token'а (expires_in поле, в unix timestamp). Также в payload refresh token'a добавляется user_id
"accessToken": "...",
"refreshToken": "...",
"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: https://github.com/zmts/beauty-vuejs-boilerplate/blob/master/src/services/http.init.js

Back-end: https://github.com/zmts/supra-api-nodejs/tree/master/actions/auth

Чтиво:

And why JWT is bad

@kshutkin

This comment has been minimized.

Copy link

commented Aug 28, 2017

Создается новая пара токенов >> пара токенов "от хакера" становится не валидна

Интересно, как это реализовано?

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Aug 28, 2017

@kshutkin
После логина юзера(владельца аккаунта), создается новый рефреш токен который и записывается в БД на место украденного. А access и так експайрится через 10-30 мин(как кто настроит)

@kodwi

This comment has been minimized.

Copy link

commented Nov 26, 2017

В базе можно хранить сколько угодно refresh токенов на один userId, т.е. на каждое устройство / браузер, с которого юзер залогинился. Тогда ему не придется каждый раз перелогиниваться на старом устройстве, если он заходил с какого-либо другого места.

В данном случае проблема с угоном refresh токена решается добавлением функционала "Выйти на всех устройствах" в профиле юзера, при котором из базы сносятся все связанные с ним токены.

Также проблема с забиванием базы устаревшими токенами решается написанием утилиты, которая раз в сутки / неделю / месяц чекает каждый токен на живучесть и сносит его, если помер.

Поправьте, если где-то не прав.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Jan 2, 2018

@kodwi Спасибо за идею, нужно будет опробовать

@qwertukg

This comment has been minimized.

Copy link

commented Feb 16, 2018

Зачем хранить рефрешь токент на сервере, если можно делать его верифай, когда клиент пришел за обновлением токенов?

@nikolaas

This comment has been minimized.

Copy link

commented Feb 22, 2018

@qwertukg Если хакер угонит ваш рефреш токен, то сможет использовать его до тех пор, пока не истечет его срок действия, потому что с точки зрения верификации это будет валидный токен. Сохраняя токен в базе, вы получаете возможность проверить стоит ли доверять токену. Таким образом вы сможете отличать валидные доверенные токены от валидных недоверенных токенов.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Feb 22, 2018

@qwertukg
В данной заметке рассматривается случай когда юзер имеет право логинится только с одного браузера. Аутентификация на другом устройстве/браузере инвалидирует предыдущий рефреш токен. В данной схеме если не хранить рефреш токен в БД мы не с можем контролировать его инвалидацию. Что приведет к возможности плодить неограниченное количество рефреш токенов. И как написал товарищ @nikolaas мы также защищаемся таким образом от хацкеров (перечитайте п. "В случае кражи").

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

API должна не только верифаить токены но и знать какие именно токены привязаны к какому юзеру.

@DmytroDiachuk

This comment has been minimized.

Copy link

commented Mar 1, 2018

часть header { «alg»: «HS256», «typ»: «JWT» } она фактично статичная. Почему ее так же как secret не хранить на серверах: отсекать при выдаче токена и приклеить при верификации?

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Mar 3, 2018

@vavilon2 Хозяин барин) 1) Овчинка не стоит выделки 2) Лишние манипуляции(зачем усложнять) 3) Не стоит перекручивать уже давно всем известный способ взаимодействия с JWT(подумайте о тех кто будет в последствии читать ваш код)

@zikas1997

This comment has been minimized.

Copy link

commented Mar 28, 2018

разаработке SPA - ошибка. Хорошая статья!
upd: ошибку исправил

@newsiberian

This comment has been minimized.

Copy link

commented Apr 4, 2018

@zmts, по какой схеме предлагаете хранить токены на стороне клиента? В статье на харбе в комментах активно приводят мнение, что рефреш токен вообще не стоит передавать клиенту, но не понятно, как это реализовывать в случае с простейшей реализацией spa - api server без всяких наворотов.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Apr 15, 2018

@newsiberian Я храню в localstorage. Предварительно обезопасив себя от XSS(HTML Escaping/Content Security Policy) на стороне сервера. Интересно глянуть как это можно реализовать без хранения рефреш токена на клиенте. В любом случае на клиенте будет лежать какой-то ID(session id) так что особого смысла не хранить рефреш токен на клиенте я не вижу. Есть линк на холивар ?

@hidemire

This comment has been minimized.

Copy link

commented May 15, 2018

@zmts Как вариант можно добавить поле prev_refresh_token. При рефреше в это поле записывается предыдущий рефреш токен. Если prev_refresh_token и refresh_token совпадают очевидно что парой пользуются 2+ клиента. Исходя из этого можно удалить из базы пару в целом.

@kkaugust

This comment has been minimized.

Copy link

commented May 23, 2018

@Fionor prev_refresh_token не пойдет, потому что хакер может рефрешнуть токен 2-3-4 раза и не будешь же ты всю цепочку отслеивать и потом последний отключать...
Самый эффективный вариант для мультисессий это показывать все залогиненые устройства в лич кабинете и дать возможность их отрубать. В гугле так...

@mvarakin

This comment has been minimized.

Copy link

commented May 29, 2018

какая общая практика, если клиент не проверит expiration у access-токена и пошлет его на сервер?
что ответит сервер?

@imShara

This comment has been minimized.

Copy link

commented May 30, 2018

Отсутствует механизм отзыва access токена. Если в данной схеме злоумышленник украдёт access токен, пользователь будет наблюдать как сливаются его данные пока не истечёт срок жизни токена и ничего с этим поделать не сможет.

Чтобы этого избежать, access токены тоже нужно хранить в базе и удалять в случае, если пользователь разлогинился на клиенте с client_id

@Claud

This comment has been minimized.

Copy link

commented May 30, 2018

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

Токен, который мы кидаем туда-сюда, слишком большой по размеру, плюс делаются дополнительные манипуляции для его создания проверки, но на деле выходит что мы как и прежде мудем проверять наличие токена в аккаунте пользователя, тоесть от запроса в базу мы не ушли засчет потписанного JWT, тогда накой он нам нужен, выглядит как работа ради работы?

Куда логичней он подходит для случая, когда вам нужно передать какие-то данные и быть уверенными что их где-то посередке ни кто не подменил. Ну или для частного случая access token из статьи, где не делается его проверка по базе данных.

P.S. Т.е. куда проще использовать просто обычные tokken в виде сгенеренной строки (даже можно два как в примере), и просто проверять их по базе.

@ivan-kleshnin

This comment has been minimized.

Copy link

commented May 30, 2018

@Claud 👍 всё так. Можно аргументировать что запрос к базе будет более быстрый (т.к. таблица/коллекция иная), но всё это не убеждает... По итогу JWT аутентификация получается намного сложнее, а часто и медленнее своих якобы устаревших альтернатив.

Обычные строковые токены в заголовке Authorization решают проблему с поддержкой доменов у кукисов и убирают лишний трафик из запросов статики. Почему-то никто не сравнивает JWT с ними... А сессия у вас появится в любом случае (ну, кроме домашних проектов).

@imShara

This comment has been minimized.

Copy link

commented May 31, 2018

Написал тут правильный workflow как я его представляю:

Database table fields

  • user id
  • client id
  • token string
  • token type [ refresh, access ]
  • expiration date

Create new token pair (login)

POST token

Required

  • login
  • password

Logic

IF received login/password pair is right

    IF client id received and there is refresh token for this client id

        - Update access token for received client id
        - Update refresh token for received client id

    IF client id not received or there is no refresh token for this client id

        IF there is so many refresh tokens for user id with received login/password pair

            - IT IS STRGANGE! ACHTUNG! ALARM!

        IF there is not much refresh tokens for user id with received login/password pair
            
            - Create client id
            - Create refresh token
            - Create access token

Return

  • client id
  • refresh token
  • refresh token expiration date
  • access token
  • access token expiration date
  • user data

Renew token pair

GET token

Maybe it should not use GET method because GET method should only return data by RFC. But in this place it so relevant: "Hey, I want to GET new access token! Take this beautiful refresh token!"

Required

  • client id
  • refresh token

Logic

IF received refresh token for client id is right
    - Update access token for received client id
    - Update refresh token for received client id

Return

  • access token
  • access token expiration date
  • refresh token
  • refresh token expiration date

Delete token pair (logoff)

DELETE token

Required

  • client id
  • refresh token

Logic

IF received refresh token for client id is right
    - Delete refresh token for received client id
    - Delete access token for received client id
@zmts

This comment has been minimized.

Copy link
Owner Author

commented Jun 19, 2018

@mvarakin

какая общая практика, если клиент не проверит expiration у access-токена и пошлет его на сервер?
что ответит сервер?

Токен будет инвалидным и сервер идентифицирует юзера как 'ROLE_ANONYMOUS' при условии что запрос к приватному ресурсу сервер ответит 403, в случае публичного запрос будет успешным.

@imShara

Отсутствует механизм отзыва access токена. Если в данной схеме злоумышленник украдёт access токен, пользователь будет наблюдать как сливаются его данные пока не истечёт срок жизни токена и ничего с этим поделать не сможет.
Чтобы этого избежать, access токены тоже нужно хранить в базе и удалять в случае, если пользователь разлогинился на клиенте с client_id

Поэтому access-токену не стоит давать длительный срок жизни. А еще хуже при краже рефреш токена, сливаться инфа будет до тех пор пока юзер не перелогинится, а когда сделает он это черт его знает, вот такая печаль и от нее никуда не денешься.

Хранить в БД access-токен, ради инвалидации особо смысла не вижу если у access-токена срок жизни 30 мин. Попробуйте поймать момент кражи вовремя. Тогда уж лучше обходится с одним долгоиграющим access-токеном да и все. Хранить его в БД и не парится с рефрешами.

Держать юзера залогиненым в безопасности на длительном промежутке времени это еще тот гемор. Если сильно нужна безопасность придется пожертвовать UX'ом и заставлять юзера каждый раз логинится. Украсть можно все. Жизнь боль.

@AntsiferovMaxim

This comment has been minimized.

Copy link

commented Jun 20, 2018

А как реализовать logout? Если удалить refresh_token из базы, то пользователь сможет заходить еще пока не истек acces_token, а это как-то хреново.

@AlexanderMint

This comment has been minimized.

Copy link

commented Jun 29, 2018

Расскажу о своем опыте использования JWT в нагруженном проекте, в котором более чем за год работы никаких проблем не произошло.

Вот примерная структура в базе данных

                                                         Table "public.sessions"
   Column   |            Type             |                       Modifiers                       | Storage  | Stats target | Description 
------------+-----------------------------+-------------------------------------------------------+----------+--------------+-------------
 id         | bigint                      | not null default nextval('sessions_id_seq'::regclass) | plain    |              | 
 user_id    | bigint                      | not null                                              | plain    |              | 
 ip         | cidr                        | not null                                              | main     |              | 
 os         | text                        |                                                       | extended |              | 
 browser    | text                        |                                                       | extended |              | 
 user_agent | text                        |                                                       | extended |              | 
 token      | character varying(36)       | not null                                              | extended |              | 
 expired_at | timestamp without time zone | not null                                              | plain    |              | 
 created_at | timestamp without time zone | not null                                              | plain    |              | 
 updated_at | timestamp without time zone | not null                                              | plain    |              | 

в ней хранятся refresh token-ы которые представляют из себя любую случайную уникальную строку, например 763675e5-f22a-4a51-bf9f-8ee784a1d500, различные данные об устройстве и expired_at (дата истечения refresh token-а)

Авторизация

Начну с простого (в надежде что кто то поймет меня, а то из меня такой себе писатель), пользователь ввел корректный логин/пароль и мы создаем запись в таблице, например такую:

-[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------------------------------
id         | 677
user_id    | 96
ip         | ::1/128
os         | Mac 10.13.4
browser    | Safari 11.1
user_agent | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15
token      | 6d36877d-6a5d-411d-85f7-9d68b37f6761
expired_at | 2018-06-19 09:21:34.099956
created_at | 2018-06-18 09:21:34.10338
updated_at | 2018-06-18 09:21:34.10338

Далее формируем ему JWT access token, который состоит например из:
(sub: user.id, jti: refresh_token, roles: user.roles) # refresh_token - это тот что выше из базы

и отправляем его клиенту тот в свою очередь записывает данный токен например в куку.

Работа на клиенте происходит следующим образом

Далее происходит интересное. Имеем клиент у которого есть кука с JWT токеном, теперь с каждым запросом мы:

  • отправляем JWT токен в заголовке запроса (не куку, это важно), например HTTP_AUTHORIZATION
  • с каждым запросом мы будем получать новый JWT, как он генерируется я опишу ниже. Но суть в том что мы записываем токен который к нам пришел в ответе и заменяем им старый, заголовок может называться как MyProject-Access-Token

Мы используем GraphQL и React.js с apollo-graphql на борту, так что если у кого похожий стэк, могу написать как это сделать на клиенте.

Работа на клиенте очень простая и не имеет никакой сложности в виде постоянного мониторинга актуальности токенов, каких то данных внутри них. Мы просто формируем заголовок для запроса и записываем токен из ответа.

Что происходит на сервере

Представим ситуацию что мы получили запрос на получение данный и JWT access token в заголовке запроса.

Чуть ниже я прикреплю скрин на блок схему написанную на коленке где наглядно показан режим работы, а пока я хочу обьяснить ключевые моменты работы.

И так, есть JWT, в которым первым делом мы проверяем его действительность, то что он подписан нашим ключом. Далее смотрим время его окончания, и тут 2 возможных исхода, опишу их подробнее.

В случае если время НЕ вышло и токен актуален, мы вообще не лезем в базу, нам даже для того что бы вытащить какие то данные пользователю достаточно будет использовать JWT user.id который мы получаем, так как мы можем гарантировать валидность этих данных.

В случае если время вышло, тут происходит интересное, мы делаем запрос к базе данных и просим вернуть строку в которой token == JWT.refresh_token (т.е. ищем строку ту самую что выше с нашим токеном, но не по id пользователя). В данной строке есть поле expired_at, в нем указанно время окончания refresh токена и если время вышло, значит данный токен не рабочий, можете считать что его просто не существует и возвращать пользователя на страницу авторизации. Если время все таки старше текущей даты (т.е. refresh токен актуальный), в таком случае есть 2 решения, объясню самый простой (а то и так много текста). Мы просто изменяем дату окончания, допустим на "текущее время + Х дней".

Как результат и в первом и втором случае мы формируем новый access JWT token с новой датой окончания.

вот схема работы что я обещал выше: https://ibb.co/hZ7mhJ

ладно, идем дальше, по ходу дела постараюсь ответить на ваши вопросы...

Безопасность

- Я потерял устройство с которого можно попасть в аккаунт...

У нас в системе была возможность у пользователя завершить сессию на Х устройстве или выйти из всех сессий кроме текущей. Как это работало, опять же, простыми словами (некоторые очевидные вещи я не озвучиваю):

  • мы получаем запрос с ID сессией из которой надо выйти
  • обновляем строку и устанавливаем expired_at в значение текущего времени
    аналогично обновляем список с массивом айдишников если нужно выйти из всех сервисов.

Но тут возникает проблема что злоумышленник у которого будет access токен, сможет им еще пользоваться до его окончания, а это может печально закончится, что бы этого не допустить, следующим шагом мы записываем в Redis индефикаторы refresh токенов которые заблокировал пользователь и устанавливаем эту запись на время access токена таким образом если access токен живет 10 минут, то и в редис мы вешаем блокировку на 10 минут.

- У меня украли куки с устройства

Тут 2 основных момента безопасности, первый заключается в том что когда человек нажимает кнопку "Выход" мы refresh token делаем не действительны т.е. баним его и меняем expired_at. Естественно кнопку выход никто не нажимает, по этому второй момент заключается в том на сколько вы хотите заморочиться, так как любые дополнительные ограничения накладывают и сложности для клиентов. Например

вы можете писать в access токен IP адрес пользователя и проверять его при каждом запросе и если он изменился просить пользователя ввести только пароль. Аналогичным образом можно мониторить user agent и все тому подобное.

вы можете указать refresh токен в рамках Х часов, и если это время сервисом не пользовались то пользователь вылетит.

вы можете контролировать что бы использовалось всегда только одно устройство

Проблемы от подобных вещей я думаю ясны всем.... Мы остановились на том что если у Х пользователя изменился IP и User Agent мы просим ввести его пароль, если происходят другие странные изменения мы просто уведомляем его, например на почту.

Писал просто свои мысли о JWT, многое не сказал, возможно где то опечатывался или что то забывал добавить, замечу поправлю. Надеюсь кому то будет полезно.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Jul 17, 2018

@AlexanderMint Спасибо за ваш пост. Будет время обязательно опробую на практике.

@xvonabur

This comment has been minimized.

Copy link

commented Jul 23, 2018

@AlexanderMint спасибо! Я правильно понимаю, что access и refresh токены вы всегда шлете вместе, наподобие того, как у вас в upment-hanami реализовано?

@AlexanderMint

This comment has been minimized.

Copy link

commented Jul 24, 2018

@xvonabur все верно, правда приложение о котором вы говорите слегка устарело ) Сейчас хочу новую версию выпустить, там многое доведено до ума. К слову, вот более безопасный вариант https://ibb.co/bJ32pT. Буду рад любой критике

@Mefistophell

This comment has been minimized.

Copy link

commented Sep 7, 2018

@AlexanderMint расскажите пожалуйста, что вы делаете на клиенте (Apollo Client) и как graphQL устанавливает куку с токеном.

@Mefistophell

This comment has been minimized.

Copy link

commented Sep 19, 2018

Что в JSONB выступает в качестве ключей (refreshTokenId) ?

@e-eki

This comment has been minimized.

Copy link

commented Oct 2, 2018

Спасибо за статью, очень помогла, но возникло несколько вопросов по вашей реализации токенов в проекте supra-api-nodejs.

  1. почему время жизни access и refresh токена пишется не в тело токена, в expiresIn, а передается отдельно, через '::'? ведь все равно при проверке приходится их друг от друга отделять с помощью split, разве не удобнее было бы сразу из токена брать после декодирования? иными словами, какой в этом профит и можно ли просто в payload в exp положить время жизни?

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

  3. зачем в payload пишете тип токена? фронт-энд же их все равно достает через response.data.accessToken и response.data.refreshToken?

  4. И еще - зачем сохранять refresh token в базу, если его "валидность и срок действия" можно проверить и не сравнивая его с лежащим в базе данных?

  5. В https://github.com/zmts/supra-api-nodejs/blob/master/services/auth/findAndVerifyRefreshToken.js вы сравниваете рефреш токен с токеном из базы вот так: (existingUserTokenFromDb !== reqRefreshToken)
    Я сохраняю токен в базу в виде строки, и у меня такой способ сравнения не работает, сравнить токены с помощью >, <, которые вроде бы со строками должны работать, тоже не удается. В каком формате у вас токены, когда вы их сравниваете?

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Oct 2, 2018

Что в JSONB выступает в качестве ключей (refreshTokenId) ?

timestamp создания токена
https://github.com/zmts/supra-api-nodejs/blob/master/services/auth/makeRefreshTokenService.js#L28

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Oct 3, 2018

  1. Время жизни пишется в options.expiresIn , все как положено. Перед :: пишется timestamp создания рефреш токена. Который в последствии используется как ключ в объекте(refreshTokensMap ) в котором хранится список рефреш токенов.
  2. Для симметричного алгоритма(HS256/HS384/HS512) секретным ключом может быть любая строка.
  3. Для наглядности, мне так нравится. Семантика =)
  4. Перечитайте заметку еще раз более внимательно. Там описано достаточно кейсов где необходим рефреш токен.
  5. String.
@e-eki

This comment has been minimized.

Copy link

commented Oct 4, 2018

Спасибо!

  1. В заметке два кейса вижу я:
  • для проверки рефреш токена сравнением с лежащим в БД (при рефреше токенов);
  • для отслеживания количества устройств, с которых залогинился юзер (при мульти сессии).

Почему и возник такой вопрос, в случае с проверкой, валидность и время жизни рефреша можно проверить, не сравнивая с лежащим в БД, а в случае с мульти сессиями, не принципиально, например, со скольких устройств юзер зайдет.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Oct 5, 2018

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

@e-eki

This comment has been minimized.

Copy link

commented Oct 6, 2018

Спасибо за развернутый ответ.

@sergeysova

This comment has been minimized.

Copy link

commented Oct 10, 2018

учитывая все комментарии и свой опыт, я делаю вывод, что для безопасной аутентификации JWT не нужен вовсе.
Гораздо проще и безопаснее использовать обычные сессионные токены в базе

@AlexanderBukhtaty

This comment has been minimized.

Copy link

commented Oct 22, 2018

после прочтения доки по jwt смутно представлял все последовательности.
после прочтения Вашей статьи все уложилось.
Спасибо большое автору, очень полезная и понятная статья.

@Delgus

This comment has been minimized.

Copy link

commented Dec 5, 2018

В чем смысл использовать для refresh_token JWT?

Я вот храню refresh_token в БД в таблице с полями: id, user_id. refresh_token,expired(время истечения срока действия токена)
Соответственно когда клиент присылает refresh_token я ищу его в БД, если не находит - возвращаю ошибку что клиент не авторизован.
Потом сверяю по дате истечения refresh_token. если срок действия истек - возвращаю ошибку что клиент не авторизован,
А если все хорошо - генерирую токены, удаляю старый refresh_token, сохраняю новый и возвращаю access_token и refresh_token клиенту.

JWT для этого не нужен совсем

@Dogrtt

This comment has been minimized.

Copy link

commented Jan 14, 2019

В чем смысл использовать для refresh_token JWT?

Я вот храню refresh_token в БД в таблице с полями: id, user_id. refresh_token,expired(время истечения срока действия токена)
Соответственно когда клиент присылает refresh_token я ищу его в БД, если не находит - возвращаю ошибку что клиент не авторизован.
Потом сверяю по дате истечения refresh_token. если срок действия истек - возвращаю ошибку что клиент не авторизован,
А если все хорошо - генерирую токены, удаляю старый refresh_token, сохраняю новый и возвращаю access_token и refresh_token клиенту.

JWT для этого не нужен совсем

Хранение токенов в базе - нарушения принципа Stateless.

@kolkov

This comment has been minimized.

Copy link

commented Jan 31, 2019

В базе можно хранить сколько угодно refresh токенов на один userId, т.е. на каждое устройство / браузер, с которого юзер залогинился. Тогда ему не придется каждый раз перелогиниваться на старом устройстве, если он заходил с какого-либо другого места.

В данном случае проблема с угоном refresh токена решается добавлением функционала "Выйти на всех устройствах" в профиле юзера, при котором из базы сносятся все связанные с ним токены.

Также проблема с забиванием базы устаревшими токенами решается написанием утилиты, которая раз в сутки / неделю / месяц чекает каждый токен на живучесть и сносит его, если помер.

Поправьте, если где-то не прав.

По идее можно не чистить, а инвалидировать (valid: {true, false}), а в записи хранить ip и время создания и последнего обновления. тогда будет некий аналог лога подключений...

@kolkov

This comment has been minimized.

Copy link

commented Jan 31, 2019

@Fionor prev_refresh_token не пойдет, потому что хакер может рефрешнуть токен 2-3-4 раза и не будешь же ты всю цепочку отслеивать и потом последний отключать...
Самый эффективный вариант для мультисессий это показывать все залогиненые устройства в лич кабинете и дать возможность их отрубать. В гугле так...

а как правильно устройства отслеживать? особенно браузеры? fingerprint?

@kolkov

This comment has been minimized.

Copy link

commented Jan 31, 2019

учитывая все комментарии и свой опыт, я делаю вывод, что для безопасной аутентификации JWT не нужен вовсе.
Гораздо проще и безопаснее использовать обычные сессионные токены в базе

Тут, наверное для того, что сервер аутентификации может быть другой и удобно выдать токен и потом чел им пользуется для доступа к ресурсу до момента его устаревания.

@kolkov

This comment has been minimized.

Copy link

commented Jan 31, 2019

В чем смысл использовать для refresh_token JWT?

Я вот храню refresh_token в БД в таблице с полями: id, user_id. refresh_token,expired(время истечения срока действия токена)
Соответственно когда клиент присылает refresh_token я ищу его в БД, если не находит - возвращаю ошибку что клиент не авторизован.
Потом сверяю по дате истечения refresh_token. если срок действия истек - возвращаю ошибку что клиент не авторизован,
А если все хорошо - генерирую токены, удаляю старый refresh_token, сохраняю новый и возвращаю access_token и refresh_token клиенту.

JWT для этого не нужен совсем

Лично я его использую еще и как userId токен. т.е. в него допинформацию пишу, не очень хорошо, но все же. Имя пользователя, его настройки для приложения и т.д. но лучше третий токен использовать просто.

@hound672

This comment has been minimized.

Copy link

commented Feb 3, 2019

Приветствую, а если добавляем сервис websockets? Какие варианты? Я вижу такой вариант:

  1. Юзер открывает сайт(SPA).
  2. Проверяем токен - отправляем запрос на какой-нибудь урл типа /check-token
  3. Там сервер согласно выше приведенным алгоритмам проверяет его валидность
  4. Клиент получает результат проверки токена и дальше, если токен не валидный, то перекидывает пользователя на страницу авторизации
  5. Если токен валидный, то продолжаем работу с пользователем, например, подключаемся к сервису websockets
  6. На сервис websockets после установления соединения отправляем токен и тут сервису достаточно будет просто проверить его валидность. В случае с сокетами нам не надо делать рефреш токена, т.к этот процесс происходит после подключения и сам коннект живёт до отключения клиента.
    Какие мысли могут на этот счёт?
@zmts

This comment has been minimized.

Copy link
Owner Author

commented Feb 11, 2019

В чем смысл использовать для refresh_token JWT?
JWT для этого не нужен совсем

В моем случае я не хотел делать еще одну таблицу(читай на один запрос в БД меньше) и решил хранить авторизационные данные в таблице users. У вас вполне рабочий вариант почему бы и нет =)

Хранение токенов в базе - нарушения принципа Stateless.

Это в случае с access токеном. Для рефреш токена это норма.

Приветствую, а если добавляем сервис websockets? Какие варианты? Я вижу такой вариант:
6. На сервис websockets после установления соединения отправляем токен и тут сервису достаточно будет просто проверить его валидность. В случае с сокетами нам не надо делать рефреш токена, т.к этот процесс происходит после подключения и сам коннект живёт до отключения клиента. Какие мысли могут на этот счёт?

Я с вебсокетом особенно не работал но как на меня звучит неплохо. Я бы тоже так сделал :)

@d1m96

This comment has been minimized.

Copy link

commented Feb 15, 2019

  • с каждым запросом мы будем получать новый JWT

@AlexanderMint Как клиент удостоверится что заново сгенерированный сервером JWT пришел от сервера?

@pp-chain

This comment has been minimized.

Copy link

commented Mar 6, 2019

и в первом и втором случае мы формируем новый access JWT token с новой датой окончания

Если просто рефрешить дату в JWT, то злоумышленник, однажды укравший JWT сможет по нему ходить пока не обновится строка refresh_token?

@xvonabur все верно, правда приложение о котором вы говорите слегка устарело ) Сейчас хочу новую версию выпустить, там многое доведено до ума. К слову, вот более безопасный вариант https://ibb.co/bJ32pT. Буду рад любой критике

Спасибо автору @zmts и вариант @AlexanderMint, думаю использовать именно его в своём проекте.
Если Вас не затруднит, подскажите пожалуйста:

  1. каков алгоритм работы при логине пользователя с нескольких устройств?
  2. как отслеживается ситуация, если с клиента пришло несколько запросов с устаревшим JWT - первый обновит токен, а другие будут ещё со старым - ведь они не будут найдены в базе и пользователь будет сброшен на 401?
  3. в более безопасном варианте схемы поиск идёт не по token-строке, а по id в базе, в итоге строку не используете вообще?
  4. также во втором варианте схемы идёт проверка времени сессии до времени JWT, этот момент бы прояснить.

Вообще, думаю многим бы помогло, если бы Вы также детально расписали обновлённый вариант, к которому пришли в итоге.

@Devoter

This comment has been minimized.

Copy link

commented Mar 29, 2019

Позволю нагло подключиться к беседе вставить свои пять копеек. Использую JWT сейчас, но проект на микросервисах, JWT удобен для сквозной аутентификации. То есть основных методов использования два (как по мне):

  1. Разные части системы расположены на разных серверах, для авторизации используется один общий сервер, после чего можно получать доступ ко всем удаленным ресурсам.
  2. Система использует общий сервер авторизации, но работает с разными интерфейсами (например, публичный - для клиентов и корпоративный - для сотрудников).

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

@AlexanderMint

This comment has been minimized.

Copy link

commented Apr 4, 2019

Возможно это кому то поможет:

Почему не стоит использовать JWT вместо сессий
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
http://cryto.net/%7Ejoepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/

@taburetkin

This comment has been minimized.

Copy link

commented Apr 28, 2019

ну а что если "солить" рефреш токен чем-нибудь из реквеста, чтобы было бесполезно угонять рефреш токен?

ну и почему бы не хранить валидацию токенов в редиске, чтобы не лазить в бд за этим делом?

@Rey8d01

This comment has been minimized.

Copy link

commented May 8, 2019

@AlexanderMint прочитал несколько раз, просмотрел обе схемы. правильно ли я понимаю что в случае если Access просрочен Refresh актуален то, помимо того что будет сгенерирован новый токен, все равно в рамках этого же запроса доступ к системе будет разрешен и данные будут получены? т.е. на самом деле со старым Access можно будет ходить до истечения срока Refresh? просто будут приходить новые токены которые, при такой схеме, не совсем уж и обязательно обновлять на клиенте.

@user4i

This comment has been minimized.

Copy link

commented May 20, 2019

1. Если установить время access token 30 секунд, это же только уменьшит риски угона информации при краже refresh token, а нагрузка существенно не увеличится, так?
2. Если при бездействии системы запустить периодическое обновление (setInterval, sockets), access token (с периодичностью = время жизни access token), то это уменьшит возможности применения украденного refresh token, правильно?
3. @zmts, если при нескольких устройствах в refresh token хранить время логина пользователя и в списке refresh token применять в роли ключа списка не refreshTokenTimestamp1 а время логина, то придется удалять только 1 refresh token а не всех логаутить. Так лучше?
И @zmts, спасибо за обширную статью по существу.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented May 20, 2019

  1. Уменьшит. Нагрузка увеличится пропорционально количеству запросов к API
  2. Уменьшит. Но лучше так не делать. Это не лучшая практика.
  3. При обновлении рефреш токена удаляется только предыдущий рефреш токен
@user4i

This comment has been minimized.

Copy link

commented May 20, 2019

К моему вопросу #1.

Не совсем пропорционально. Если взять время обновления access token 30сек (A) и 30минут (B) и пусть для обновления нам нужен 1 дополнительный запрос, то для разного количества запросов получим в сравнении следующее

100-запросов/30сек – в итоге за 30 минут:
А - 6060 запросов
В - 6001 запрос
разница неоощутима в соотношению к общему количеству

1-запрос/30сек – в итоге за 30 минут:
А - 120 запросов
В - 61 запрос
разница неоощутима из-за небольшого количества обращений за такое время.

К вопросу #2.

А как тогда не пропустить момент кражи refresh token-а во время "перерыва"? Хакер украл refresh token, уловил время бездействия и делает свое дело во время работы пользователя в других приложениях / страницах или в обеденный перерыв. А так мы ему помешаем, потому что периодически обновляем р-токен. Другое дело при закрытии вкладки или в конце рабочего дня, тут злоумышленник может до утра скачивать файлы, копировать всю важную информацию во всех доступных разделах – нужно искать другое решение.

К вопросу #3.

В случае кражи(обоих токенов)
Создается новая пара токенов >> пара токенов "от хакера" становится не валидна

Процитированный из заметки шаг из кражи токенов не подходит для нескольких устройств одного пользователя. Потому что для хакера получится параллельная ветка или цепочка refresh token-ов и хакер может хазяйничать немало времени, а реальный пользователь перелогинится и получит другую цепочку refresh token-ов. Как вариант, нужно разлогинивать все входы когда пришел несуществующий в списке refresh token (от реального пользователя, потому что хакер уже обновил).

Но, думаю, лучше хранить время логина или другой уникальный единый идентификатор цепочки всех refresh token-ов для данного устройства.
время логина -> refresh token -> refresh token -> refresh token -> refresh token
в конкретный момент в списке refresh token-ов может быть только 1 refresh token для конкретного времени логина, и поступление даже валидного refresh token-на должно вызвать тревогу, если для такого же времени логина в БД записан другой refresh token.

1. Хакер воспользовался access token'ом
2. Закончилось время жизни access token'на
3. Клиент хакера отправляет refresh token
4. Хакер получает новую пару токенов
5. На сервере создается новая пара токенов("от хакера")

6. Юзер пробует зайти на сервер >> обнаруживается что токены невалидны (т.к. для последнего времени логина лежит другой refresh token)
7. Сервер удаляет неверный refresh token с таким же последним временем логина (время логина == ключе в списке refresh token-ов + храним внутри token-а для понимания куда его относить) и перенаправляет юзера на форму аутентификации
8. Юзер вводит логин/пароль
9. Создается новая пара токенов >> пара токенов "от хакера" становится не валидна

Если ошибаюсь, поправте пожалуйста.

@zmts

This comment has been minimized.

Copy link
Owner Author

commented May 21, 2019

  1. Умножьте к своим расчетам количество одновременно работающих юзеров. Если вы уверенны что можете позволить такой оверхед на ровном месте (как по трафику так и по нагрузке), тогда дерзайте.
  2. Браузер юзера должен быть всегда в активном состоянии. Если система заснет запросы перестанут выполнятся.

не подходит для нескольких устройств одного пользователя. Потому что для хакера получится параллельная ветка или цепочка refresh token-ов и хакер может хазяйничать немало времени, а реальный пользователь перелогинится и получит другую цепочку refresh token-ов. Как вариант, нужно разлогинивать все входы когда пришел несуществующий в списке refresh token (от реального пользователя, потому что хакер уже обновил).

Вы правильно заметили. Этот шаг актуален только для одного устройства. Я обновил заметку: добавил кейс и для мульти сессий.

@user4i

This comment has been minimized.

Copy link

commented May 21, 2019

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

По IP вы предложили очень даже хорошее решение, но оно подходит для пользователей, которые сидят в одном месте (офис/дома и еще где). Если же я в течении дня поменял 5 раз точку доступа из-за разъездов по работе + менялся динамический IP, а Fingerprint нету, то каждый раз логинится не совсем удобно да и пароль нужно вспоминать где-то в дороге, в аэропорте и т.д.

Только что MongoDB Cloud прислал мне email из-за логина с того же устройства и точки ноступа, но возможно с нового дин. IP (не уверен), может кому пригодится пример. Так можно делать и при подозрительных изменениях при обновлении refresh token-а

We're verifying a recent sign-in for ...@gmail.com:

Timestamp: 2019-05-21 17:38:10 GMT
IP Address: 1.1.1.1
User agent: Mozilla Windows AppleWebKit Chrome

You're receiving this message because of a successful sign-in from a device that we didn’t recognize. If you believe that this sign-in is suspicious, please reset your password immediately.

If you're aware of this sign-in, please disregard this notice. This can happen when you use your browser's incognito or private browsing mode or clear your cookies.

Thanks,

MongoDB Team

@zmts

This comment has been minimized.

Copy link
Owner Author

commented May 21, 2019

Безопасность требует жертв в том числе и в удобстве UX.
Что бы отправить такое письмо им скорее всего нужен был отпечаток(Fingerprint) браузера или еще какой либо идентификатор пользователя.

@user4i

This comment has been minimized.

Copy link

commented May 22, 2019

Не сразу понял насчет Fingerprint, может стоит добавить пояснение в заметку для тех кто не знаком – "отпечаток браузера/устройства" или "browser/device fingerprint". Хорошее направление для исследования, потому что не помню, чтобы в приложениях при смене IP перенаправляло на форму аутентификации. Значит есть механизмы определения, когда стоит доверять таким изменениям IP.

@lesovsky

This comment has been minimized.

Copy link

commented Jul 11, 2019

Правильно ли я понимаю что в схеме рефреша токена, в п.1

Клиент проверяет перед запросом не истекло ли время жизни access token'на

под "клиентом" понимается условный фронтенд на который пришел юзер (браузер)?

Далее, там же но в п.6

В случае успеха сервер: ...

А в случае не успеха, просто отправляем юзера логиниться?

@zmts

This comment has been minimized.

Copy link
Owner Author

commented Jul 11, 2019

@lesovsky Все верно

@nodkz

This comment has been minimized.

Copy link

commented Jul 23, 2019

Начав читать, подумал о как классно щас узнаю как варить правильно JWT.

Но после прочтения,

  • что рефреш токен проверяет айпишник (упс, не мой вариант, я люблю через ВПНы прыгать)
  • используют хранилище рефрешей (ничем не лучше сессий)
  • нет возможности моментального выхода со всех устройств

Вобщем лучше старых добрых сессий и секурных хттпешных куках пока ничего нет. С сессиями код чище и проще и нет плясок с бубном по рефрешам. В сад JWT для индетификации.


JWT прикольно использовать например в письмах, для работы над каким-то одним объектом/ресурсом. К примеру, присылаешь письмо с редактированием объявы, и пофиг залогинен или нет – вот тебе права на изменение конкретно этой объявы.

@astranavt

This comment has been minimized.

Copy link

commented Aug 14, 2019

Расскажу о своем опыте использования JWT в нагруженном проекте, в котором более чем за год работы никаких проблем не произошло.

Вот примерная структура в базе данных

                                                         Table "public.sessions"
   Column   |            Type             |                       Modifiers                       | Storage  | Stats target | Description 
------------+-----------------------------+-------------------------------------------------------+----------+--------------+-------------
 id         | bigint                      | not null default nextval('sessions_id_seq'::regclass) | plain    |              | 
 user_id    | bigint                      | not null                                              | plain    |              | 
 ip         | cidr                        | not null                                              | main     |              | 
 os         | text                        |                                                       | extended |              | 
 browser    | text                        |                                                       | extended |              | 
 user_agent | text                        |                                                       | extended |              | 
 token      | character varying(36)       | not null                                              | extended |              | 
 expired_at | timestamp without time zone | not null                                              | plain    |              | 
 created_at | timestamp without time zone | not null                                              | plain    |              | 
 updated_at | timestamp without time zone | not null                                              | plain    |              | 

в ней хранятся refresh token-ы которые представляют из себя любую случайную уникальную строку, например 763675e5-f22a-4a51-bf9f-8ee784a1d500, различные данные об устройстве и expired_at (дата истечения refresh token-а)

Авторизация

Начну с простого (в надежде что кто то поймет меня, а то из меня такой себе писатель), пользователь ввел корректный логин/пароль и мы создаем запись в таблице, например такую:

-[ RECORD 1 ]--------------------------------------------------------------------------------------------------------------------------------------------
id         | 677
user_id    | 96
ip         | ::1/128
os         | Mac 10.13.4
browser    | Safari 11.1
user_agent | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15
token      | 6d36877d-6a5d-411d-85f7-9d68b37f6761
expired_at | 2018-06-19 09:21:34.099956
created_at | 2018-06-18 09:21:34.10338
updated_at | 2018-06-18 09:21:34.10338

Далее формируем ему JWT access token, который состоит например из:
(sub: user.id, jti: refresh_token, roles: user.roles) # refresh_token - это тот что выше из базы

и отправляем его клиенту тот в свою очередь записывает данный токен например в куку.

Работа на клиенте происходит следующим образом

Далее происходит интересное. Имеем клиент у которого есть кука с JWT токеном, теперь с каждым запросом мы:

  • отправляем JWT токен в заголовке запроса (не куку, это важно), например HTTP_AUTHORIZATION
  • с каждым запросом мы будем получать новый JWT, как он генерируется я опишу ниже. Но суть в том что мы записываем токен который к нам пришел в ответе и заменяем им старый, заголовок может называться как MyProject-Access-Token

Мы используем GraphQL и React.js с apollo-graphql на борту, так что если у кого похожий стэк, могу написать как это сделать на клиенте.

Работа на клиенте очень простая и не имеет никакой сложности в виде постоянного мониторинга актуальности токенов, каких то данных внутри них. Мы просто формируем заголовок для запроса и записываем токен из ответа.

Что происходит на сервере

Представим ситуацию что мы получили запрос на получение данный и JWT access token в заголовке запроса.

Чуть ниже я прикреплю скрин на блок схему написанную на коленке где наглядно показан режим работы, а пока я хочу обьяснить ключевые моменты работы.

И так, есть JWT, в которым первым делом мы проверяем его действительность, то что он подписан нашим ключом. Далее смотрим время его окончания, и тут 2 возможных исхода, опишу их подробнее.

В случае если время НЕ вышло и токен актуален, мы вообще не лезем в базу, нам даже для того что бы вытащить какие то данные пользователю достаточно будет использовать JWT user.id который мы получаем, так как мы можем гарантировать валидность этих данных.

В случае если время вышло, тут происходит интересное, мы делаем запрос к базе данных и просим вернуть строку в которой token == JWT.refresh_token (т.е. ищем строку ту самую что выше с нашим токеном, но не по id пользователя). В данной строке есть поле expired_at, в нем указанно время окончания refresh токена и если время вышло, значит данный токен не рабочий, можете считать что его просто не существует и возвращать пользователя на страницу авторизации. Если время все таки старше текущей даты (т.е. refresh токен актуальный), в таком случае есть 2 решения, объясню самый простой (а то и так много текста). Мы просто изменяем дату окончания, допустим на "текущее время + Х дней".

Как результат и в первом и втором случае мы формируем новый access JWT token с новой датой окончания.

вот схема работы что я обещал выше: https://ibb.co/hZ7mhJ

ладно, идем дальше, по ходу дела постараюсь ответить на ваши вопросы...

Безопасность

- Я потерял устройство с которого можно попасть в аккаунт...

У нас в системе была возможность у пользователя завершить сессию на Х устройстве или выйти из всех сессий кроме текущей. Как это работало, опять же, простыми словами (некоторые очевидные вещи я не озвучиваю):

  • мы получаем запрос с ID сессией из которой надо выйти
  • обновляем строку и устанавливаем expired_at в значение текущего времени
    аналогично обновляем список с массивом айдишников если нужно выйти из всех сервисов.

Но тут возникает проблема что злоумышленник у которого будет access токен, сможет им еще пользоваться до его окончания, а это может печально закончится, что бы этого не допустить, следующим шагом мы записываем в Redis индефикаторы refresh токенов которые заблокировал пользователь и устанавливаем эту запись на время access токена таким образом если access токен живет 10 минут, то и в редис мы вешаем блокировку на 10 минут.

- У меня украли куки с устройства

Тут 2 основных момента безопасности, первый заключается в том что когда человек нажимает кнопку "Выход" мы refresh token делаем не действительны т.е. баним его и меняем expired_at. Естественно кнопку выход никто не нажимает, по этому второй момент заключается в том на сколько вы хотите заморочиться, так как любые дополнительные ограничения накладывают и сложности для клиентов. Например

вы можете писать в access токен IP адрес пользователя и проверять его при каждом запросе и если он изменился просить пользователя ввести только пароль. Аналогичным образом можно мониторить user agent и все тому подобное.

вы можете указать refresh токен в рамках Х часов, и если это время сервисом не пользовались то пользователь вылетит.

вы можете контролировать что бы использовалось всегда только одно устройство

Проблемы от подобных вещей я думаю ясны всем.... Мы остановились на том что если у Х пользователя изменился IP и User Agent мы просим ввести его пароль, если происходят другие странные изменения мы просто уведомляем его, например на почту.

Писал просто свои мысли о JWT, многое не сказал, возможно где то опечатывался или что то забывал добавить, замечу поправлю. Надеюсь кому то будет полезно.

@AlexanderMint
А если с клиента пришло 2 запроса с одинаковым токеном, один отработал быстрее, и он оказался не актуальным, рефреш токен обновился, сработает ли второй запрос со старым токеном?

@Glacialix

This comment has been minimized.

Copy link

commented Sep 7, 2019

Начав читать, подумал о как классно щас узнаю как варить правильно JWT.

Но после прочтения,

  • что рефреш токен проверяет айпишник (упс, не мой вариант, я люблю через ВПНы прыгать)
  • используют хранилище рефрешей (ничем не лучше сессий)
  • нет возможности моментального выхода со всех устройств

Вобщем лучше старых добрых сессий и секурных хттпешных куках пока ничего нет. С сессиями код чище и проще и нет плясок с бубном по рефрешам. В сад JWT для индетификации.

JWT прикольно использовать например в письмах, для работы над каким-то одним объектом/ресурсом. К примеру, присылаешь письмо с редактированием объявы, и пофиг залогинен или нет – вот тебе права на изменение конкретно этой объявы.

я вот тоже подумал как вы - типа о, круто как. и блин сделал эту самую авторизацию.

Возможно это кому то поможет:

Почему не стоит использовать JWT вместо сессий
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
http://cryto.net/%7Ejoepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/

а потом я прочел это, и понял, что по ходу разработки столкнулся со всем что там написано )))
особенно диаграмма во второй части доставила )

@AlexanderMint

This comment has been minimized.

Copy link

commented Sep 7, 2019

@Glacialix спустя много лет мы остановились на том что используем JWT между микросервисами, авторизация через сессию, тем более все современные мобильные приложения их хорошо поддерживают и можно повесить HttpOnly что бы JS не имел доступ к значению. И рад что кому то помогла статья :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.