Skip to content

Instantly share code, notes, and snippets.

@zmts zmts/tokens.md

Last active Aug 11, 2020
Embed
What would you like to do?
Про токены, JSON Web Tokens (JWT), аутентификацию и авторизацию. Token-Based Authentication

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

Last major update: 20.04.2020

  • Что такое авторизация/аутентификация
  • Где хранить токены
  • Как ставить куки ?
  • Процесс логина
  • Процесс рефреш токенов
  • Кража токенов/Механизм контроля токенов
  • Зачем все это ? JWT vs Cookie sessions

Основа:

Аутентификация(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'е, токен станет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. А возможность сгенерировать новую сигнатуру у хакера отсутствует, поскольку секретный ключ для зашифровки лежит на сервере.

access token - используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload). Сам токен храним не в 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 на сервере ставим куку с такими опциями:

{
    domain: '.super.com',
    path: '/api/auth'
}

Таким образом кука установится в браузер и прийдет на все эндпоинты по адресу super.com/api/auth/<any-path>

Если у нас монолит и за аутентификацию отвечает один и тот-же API, тут проблем не должно быть. Но если за аутентификацию отвечает отдельный микросервис, прячем его средствами nginx по выше указанному пути (super.com/api/auth).

# пример настройки nginx конфига(только основые настройки)
server {
    listen 80;
    server_name super.com;
    # SPA/Front-end
    location / {
        try_files $uri /index.html;
        root /var/www/frontend/dist;
        index index.html;
    }
    # Main API
    location /api {
        proxy_pass http://111.111.111.111:7000;
    }
    # Auth API
    location /api/auth {
        proxy_redirect http://222.222.222.222:7000   /auth/;
        proxy_pass http://222.222.222.222:7000;
    }
}

Логин, создание сессии/токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль и fingerprint браузера (ну или некий иной уникальный идентификатор устройства если это не браузер)
  2. Сервер проверят подлинность логина/пароля
  3. В случае удачи создает и записывает сессию в БД { userId: uuid, refreshToken: uuid, expiresIn: int, fingerprint: string, ... } (схема таблицы ниже)
  4. Создает access token
  5. Отправляет клиенту access и refresh token uuid (взятый из выше созданной сессии)
Set-Cookie: refreshToken='c84f18a2-c6c7-4850-be15-93f9cbaef3b3'; HttpOnly // для браузера
{
  body: { 
    accessToken: 'eyJhbGciOiJIUzUxMiIsI...',
    refreshToken: 'c84f18a2-c6c7-4850-be15-93f9cbaef3b3' // для мобильных приложений
  }
}
  1. Клиент сохраняет токены(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 в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша.

В случае если клиент не браузер, а мобильное приложение, в качестве fingerprint используем любую уникальную строку(тот же uuid) персистентно хранящуюся на устройстве.

Рефреш токенов (api/auth/refresh-tokens):

Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице(а надо бы в Redis'е). В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией, так званая рефреш-сессия.

CREATE TABLE refreshSessions (
    "id" SERIAL PRIMARY KEY,
    "userId" uuid REFERENCES users(id) ON DELETE CASCADE,
    "refreshToken" uuid NOT NULL,
    "ua" character varying(200) NOT NULL, /* user-agent */
    "fingerprint" character varying(200) NOT NULL,
    "ip" character varying(15) NOT NULL,
    "expiresIn" bigint NOT NULL,
    "createdAt" timestamp with time zone NOT NULL DEFAULT now()
);
  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token'на
  2. Если истекло клиент делает запрос на POST auth/refresh-tokens { fingerprint: string } в body и соответственно refreshToken куку.
  3. Сервер получает запись рефреш-сессии по UUID'у рефреш токена
  4. Сохраняет текущую рефреш-сессию в переменную и удаляет ее из таблицы
  5. Проверяет текущую рефреш-сессию:
    1. Не истекло ли время жизни
    2. На соответствие старого fingerprint'a полученного из текущей рефреш-сессии с новым полученным из тела запроса
  6. В случае негативного результата бросает ошибку TOKEN_EXPIRED/INVALID_REFRESH_SESSION
  7. В случае успеха создает новую рефреш-сессию и записывает ее в БД
  8. Создает access token
  9. Отправляет клиенту access и refresh token uuid (взятый из выше созданной рефреш-сессии)
Set-Cookie: refreshToken='c84f18a2-c6c7-4850-be15-93f9cbaef3b3'; HttpOnly // для браузера
{
  body: { 
    accessToken: 'eyJhbGciOiJIUzUxMiIsI...',
    refreshToken: 'c84f18a2-c6c7-4850-be15-93f9cbaef3b3' // для мобильных приложений
  }
}

Tip: Для отправки запроса с куками для axios есть опция { withCredentials: true }

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

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

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

В случае кражи access токена и refresh куки:

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

В случае кражи access токена, refresh куки и fingerprint'а:

Стащить все авторизационные данные это не из легких задач, но все же допустим этот кейс как крайний.

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

Зачем все это ? JWT vs Cookie sessions

Зачем этот весь геморой ? Почему не юзать старые добрые cookie sessions ? Чем не угодили куки ?

  • Куки подвержены CSRF: https://habr.com/ru/company/oleg-bunin/blog/412855 https://www.youtube.com/watch?v=x5AuK_IbJlg
  • Нативыным приложениям для сматфонов удобнее работать с токенами. Да есть хаки для работы с куки, но это не нативная поддержка
  • Куки в микросерисной архитектуре использовать не вариант. Напомню зачастую микросервисы раскиданы на разных доменах, а куки не поддерживают кросc-доменные запросы
  • В микросерисной архитектуре JWT позволяет каждому сервису независимо от сервера авторизации верифицировать access токен (через публичный ключ)
  • При использовании cookie sessions программист зачастую надеется на то, что предоставил фреймворк и оставляет как есть
  • При использовании jwt мы видим проблему с безопасностью и стараемся предусмотреть механизмы контроля в случае каржи авторизационных данных. При использовании cookie сессий программист зачастую даже не задумывается что сессия может быть скомпрометирована
  • На каждом запросе использование JWT избавляет бекенд от одного запроса в БД(или кеш) за данными пользователя(userId, email, etc.)

В итоге:

  • access токены храним исключительно в памяти клиентского приложения. Не в глобально доступной переменной аля window.accessToken а в замыкании
  • refresh токен храним исключительно в httpOnly куке
  • Механизмы контроля при угоне sensitive data в наличии
  • Взяли лучшее из обеих технологий, максимально обезопасились от CSRF/XSS
  • Добавьте в компанию ко всему CSP заголовки и SameSite=Strict флаг для кук и ждите прихода злодеев

p.s. Каждой задаче свой подход. Юзайте в небольших/средних монолитах cookie sessions и не парьтесь. Ну или на ваш вкус :)


Имплементация:

Front-end:

Back-end:

Info:

And why JWT is bad


Комментарии периодически подчищаются

@kshutkin

This comment has been minimized.

Copy link

kshutkin commented Aug 28, 2017

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

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Aug 28, 2017

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

@kodwi

This comment has been minimized.

Copy link

kodwi commented Nov 26, 2017

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

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

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

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Jan 2, 2018

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

@qwertukg

This comment has been minimized.

Copy link

qwertukg commented Feb 16, 2018

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

@nikolaas

This comment has been minimized.

Copy link

nikolaas commented Feb 22, 2018

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Feb 22, 2018

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

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

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

@DmytroDiachuk

This comment has been minimized.

Copy link

DmytroDiachuk commented Mar 1, 2018

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Mar 3, 2018

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

@zikas1997

This comment has been minimized.

Copy link

zikas1997 commented Mar 28, 2018

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

@newsiberian

This comment has been minimized.

Copy link

newsiberian commented Apr 4, 2018

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Apr 15, 2018

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

@hidemire

This comment has been minimized.

Copy link

hidemire commented May 15, 2018

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

@kkaugust

This comment has been minimized.

Copy link

kkaugust commented May 23, 2018

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

@mvarakin

This comment has been minimized.

Copy link

mvarakin commented May 29, 2018

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

@imShara

This comment has been minimized.

Copy link

imShara commented May 30, 2018

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

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

@Claud

This comment has been minimized.

Copy link

Claud commented May 30, 2018

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

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

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

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

@ivan-kleshnin

This comment has been minimized.

Copy link

ivan-kleshnin commented May 30, 2018

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

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

@imShara

This comment has been minimized.

Copy link

imShara 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

zmts 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

AntsiferovMaxim commented Jun 20, 2018

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

@AlexanderMint

This comment has been minimized.

Copy link

AlexanderMint 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

zmts commented Jul 17, 2018

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

@xvonabur

This comment has been minimized.

Copy link

xvonabur commented Jul 23, 2018

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

@AlexanderMint

This comment has been minimized.

Copy link

AlexanderMint commented Jul 24, 2018

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

@Mefistophell

This comment has been minimized.

Copy link

Mefistophell commented Sep 7, 2018

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

@Mefistophell

This comment has been minimized.

Copy link

Mefistophell commented Sep 19, 2018

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

@e-eki

This comment has been minimized.

Copy link

e-eki 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

zmts 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

zmts 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

e-eki commented Oct 4, 2018

Спасибо!

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

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Oct 5, 2018

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

@e-eki

This comment has been minimized.

Copy link

e-eki commented Oct 6, 2018

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

@sergeysova

This comment has been minimized.

Copy link

sergeysova commented Oct 10, 2018

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

@AlexanderBukhtaty

This comment has been minimized.

Copy link

AlexanderBukhtaty commented Oct 22, 2018

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

@Delgus

This comment has been minimized.

Copy link

Delgus 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

Dogrtt 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

kolkov commented Jan 31, 2019

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

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

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

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

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

@kolkov

This comment has been minimized.

Copy link

kolkov commented Jan 31, 2019

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

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

@kolkov

This comment has been minimized.

Copy link

kolkov commented Jan 31, 2019

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

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

@kolkov

This comment has been minimized.

Copy link

kolkov 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

hound672 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

zmts commented Feb 11, 2019

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

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

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

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

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

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

@d1m96

This comment has been minimized.

Copy link

d1m96 commented Feb 15, 2019

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

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

@PoltP

This comment has been minimized.

Copy link

PoltP 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

Devoter commented Mar 29, 2019

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

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

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

@AlexanderMint

This comment has been minimized.

Copy link

AlexanderMint 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

taburetkin commented Apr 28, 2019

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

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

@Rey8d01

This comment has been minimized.

Copy link

Rey8d01 commented May 8, 2019

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

@user4i

This comment has been minimized.

Copy link

user4i 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

zmts commented May 20, 2019

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

This comment has been minimized.

Copy link

user4i 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

zmts commented May 21, 2019

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

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

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

@user4i

This comment has been minimized.

Copy link

user4i 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

zmts commented May 21, 2019

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

@user4i

This comment has been minimized.

Copy link

user4i commented May 22, 2019

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

@lesovsky

This comment has been minimized.

Copy link

lesovsky commented Jul 11, 2019

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

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

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

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

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

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Jul 11, 2019

@lesovsky Все верно

@nodkz

This comment has been minimized.

Copy link

nodkz commented Jul 23, 2019

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

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

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

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


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

@astranavt

This comment has been minimized.

Copy link

astranavt 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 запроса с одинаковым токеном, один отработал быстрее, и он оказался не актуальным, рефреш токен обновился, сработает ли второй запрос со старым токеном?

@GlacialDev

This comment has been minimized.

Copy link

GlacialDev 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

AlexanderMint commented Sep 7, 2019

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Oct 21, 2019

Внес в заметку мажорные апдейты:

  • Добавил сессии и объединил их с рефреш токеном
  • Добавил в методику fingerprint
  • Убрал привязку по IP, вместо этого используется привязка сессии к fingerprint
  • Убрал кейс когда юзер логинится только с одного устройства

Старая версия: https://gist.github.com/zmts/802dc9c3510d79fd40f9dc38a12bccfc/339778999d35a9c81f115a3755ea3035681f2702

@HiveTraum

This comment has been minimized.

Copy link

HiveTraum commented Nov 9, 2019

а необходимо ли делать что то с токенами при смене пароля? инвалидировать сразу все?

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Nov 9, 2019

@HiveTraum Можно(нужно) удалить все сессии юзера или все кроме текущей (а сними уйдут и рефреш токены). Инвалидация accessToken'a не предусмотрена, они должны жить мало и умирать быстро :)

@e-eki

This comment has been minimized.

Copy link

e-eki commented Feb 29, 2020

Возник вопрос по данной схеме. В частности, по этим моментам:
"В случае кражи токенов:
Хакер воспользовался access token'ом
Закончилось время жизни access token'на"

  1. До тех пор, пока время жизни access token'на не закончилось, и если, допустим, это не десять минут, а пара часов, хакер мог все это время отправлять запросы от имени юзера.

access token - используется для авторизации запросов и хранения дополнительной информации о пользователе (аля user_id, user_role или еще что либо, эту информацию также называет payload)

  1. Насколько удается понять, id/роль юзера хранятся в payload'е для того, чтобы при каждом запросе не лезть за ними в БД, а просто взять из аксесс токена. И вот, по аксесс токену получается, что пользователь имеет право все эти действия выполнять, а если, допустим, права были эксклюзивные, то хакер мог за все это время наворотить делов на ресурсе.

  2. Что делать, чтобы избежать такой ситуации?

Первое, что приходит в голову, при небезопасных запросах отправлять помимо аксесс токена еще и fingerprint, потом по userId из payload'а и по fingerprint'у искать сессию в БД, и если ее нет, то отправлять хакера лесом. Но это вроде бы противоречит логике использования аксесс токена, да и просто дольше. Может быть, подскажете более оптимальный вариант?

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Mar 13, 2020

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

Да, все верно

Что делать, чтобы избежать такой ситуации ?

Повысить политики безопасности:

  • Ставить короткий lifetime access_token (15 - 30min)
  • Засунуть IP/Subnet в payload (что приведет к необходимости логинится с каждого нового IP)
  • Хранить в БД белые списки IP c которых юзер логинился
  • Хранить черный список access_token'ов в редисе и делать проверки на каждый запрос. Что уже само по себе звучит дико ...

Безопасность это всегда компромисс между удобством пользовательского опыта и уровнем безопасности. Можно юзеру выдавать hardware токены и все равно со временем найдется способ их сломать.

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Apr 19, 2020

Внес в заметку мажорные апдейты:

  • Рефреш токен храним как httpOnly cookie
  • Добавил небольшой версус: JWT vs Cookie sessions
  • Описание: как ставить куки
  • Настойка nginx для микросервисов
@HiveTraum

This comment has been minimized.

Copy link

HiveTraum commented May 4, 2020

а что делать с fingerprint в случае не JS клиентов?

Вообще интересует вопрос если я хочу авторизовать клиента через телеграмм в своем сервисе - "куда" читать? Подходит ли JWT в таких случаях вообще?

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented May 4, 2020

а что делать с fingerprint в случае не JS клиентов?

В случае если клиент не браузер, а мобильное приложение, в качестве fingerprint используем любую уникальную строку(тот же uuid) персистентно хранящуюся на устройстве.

Вообще интересует вопрос если я хочу авторизовать клиента через телеграмм в своем сервисе - "куда" читать? Подходит ли JWT в таких случаях вообще?

@HiveTraum Больше деталей. Опишите конкретный флоу.

@HiveTraum

This comment has been minimized.

Copy link

HiveTraum commented May 5, 2020

В принципе я уже продумал как можно реализовать

Предположим у меня есть телеграмм бот с собственным бэкендом и хотелось бы авторизовать пользователей телеграмма в своем сервисе авторизации, корректно ли использовать для таких вещей JWT? Как я представляю себе мой backend для бота будет хранить accessToken'ы и refreshToken'ы где то у себя. Fingerprint можно будет так же использовать некий uuid

@zazanik

This comment has been minimized.

Copy link

zazanik commented May 7, 2020

Вроде всё перечитал - включая коментарии - но есть вопрос:

  • Что если получить access_token and refresh_token на одной вкладке - а потом очень быстро продублировать 5-10 вкладок - они все попытаются получить новую пару ключей по refresh_token при условии что refresh_token одноразовый - 1 получит - 9 - получат в ответ 401...
  • Можно сделать refresh_token многоразовым - и тогда 5 из 10 вкладок получат новые пары ключей - а 6 вкладка инвалидирует те 5 refresh_tokens что были выданы для 1-5 вкладок - так как у одного юзера больше 5 refresh_tokens в хранилище...
  • Можно при рефреше ещё валидным refresh_token возвращать новый access_token и старый refresh_token - но в таком случае клиент не получит новый refresh_token и рано или поздно по этому refresh_token нельзя будет получить access_token and refresh_token
@negezor

This comment has been minimized.

Copy link

negezor commented May 14, 2020

В SPA приложениях которые используют SSR, нужно хранить access_token в куках. В случае если токен стух (учитывая его время жизни 15~30 минут), мы вернём страницу для неавторизованного пользователя. Как результат — плохой UX, тут конечно после загрузки можно получить новый токен и сбросить стор.

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

В общем refresh_token в браузере как по мне спорное решение. Оно хорошо подходит только для приложений, которые имеют защищенное хранилище.

@pantuchy

This comment has been minimized.

Copy link

pantuchy commented May 23, 2020

Большое спасибо автору за подробную статью, пожалуй лучшее и подробное решение для JWT. У меня к Вам 2 вопроса, если позволите:

  1. У вас в статье написано, что Refresh Token мы храним на клиенте в Куке (HTTP Only), тут все понятно, а вот Access Token (JWT) мы храним в памяти приложения. Вроде бы тоже логично, в Locale Storage хранить его не хочется. Однако если хранить его в памяти клиентского приложения и предположим, что приложение SPA (Single Page Application), то все будет работать замечательно, до тех пор, пока я не начну судорожно делать обновление страницы, в таком случае приложение теряет Access Token (JWT) и ему необходимо запрашивать новую пару токенов (Access/Refresh), каждый раз, как я буду делать обновление страницы. Не будет ли это overhead на сервис авторизации и на базу данных, которая хранит refresh сессии, будь то Postgres или Redis.

  2. У вас в разделе, где указано, что Хакер украл оба токена и fingerprint, есть пункт: 9, где написано "Создается новый refresh токен >> refresh токен "от хакера" становится не валиден". Но ведь если пользователю разрешено иметь 5 сессий, для разных устройств, то получается только одна сессия (самая старая удалится), и пользователь зайдет с новой сессией, и сессия Хакера еще будет активна, так как она еще не самая старая. Либо если у пользователя всего 2 сессии из 5 разрешенных, значит будет еще одна новая, где 2 пользователя и одна Хакера. Или я не так понял статью.

Спасибо.

@pantuchy

This comment has been minimized.

Copy link

pantuchy commented May 24, 2020

Хочу добавить к тому, что написал выше, что отвечая на первый свой вопрос, я нашел решение для себя следующее:

Все сделать, как рекомендуется в данной статье, за исключением хранения Access Token (JWT) в памяти приложения, ибо приложение его теряет после перезагрузки страницы, и приходится все время делать новую пару токенов, новую запись в базе и т.д., по моему мнению это лишний overhead. Я предлагаю Access Token (JWT) на сервере разделить на 2 части, и передавать на клиент в двух куках. Одна кука будет состоять из JWT Header + JWT Signature и будет храниться на клиенте с флагами HttpOnly, Secure и SameSite=Lax. А вторую куку, которая будет состоять из JWT Payload хранить в обычной куке, которая будет доступна самому приложению на клиенте, например, чтобы делать проверки срока годности перед запросом. Соответственно при запросе на сервер будут отправляться как Refresh Token (HttpOnly), так и 2 куки Access Token (JWT), которые на сервере мы склеиваем и делаем дальнейшие проверки.

По моему мнению такой подход позволит избавиться от лишнего overhead, и использовать Access Token (JWT) по полной, пока он не истечет, допустим 15 минут, так как Access Token (JWT) никуда не пропадает после перезагрузки страницы в браузере.

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

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Jun 7, 2020

@pantuchy

Не будет ли это overhead на сервис авторизации и на базу данных, которая хранит refresh сессии, будь то Postgres или Redis.

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

У вас в разделе, где указано, что Хакер украл оба токена и fingerprint, есть пункт: 9, где написано "Создается новый refresh токен >> refresh токен "от хакера" становится не валиден"...

А вот тут то у меня в логике баг, спасибо. Сам как-то упустил этот момент. Действительно система выдаст юзеру новые токены и хакер останется со своей парой. Черт! Честно даже не вижу никакого другого способа(покаместь) кроме того как записывать в refreshSessions подсеть(или IP) юзера которому был выдан рефреш. Но если сервис требует максимального уровня безопасности, то без этого никак.

Ну скажем так вероятность того что сопрут все токены (access, refresh из куки и fingerprint) достаточно мала и рассматривать этот случай стоит только если у нас действительно чувствительный проект. Статью подправлю чуть позже.

Хочу добавить к тому, что написал выше, что отвечая на первый свой вопрос, я нашел решение для себя следующее...

Можете юзать если это работает в вашем кейсе. Но в случае если нам нужно использовать такую JWT куку в микросервисной архитектуре - это не прокатит. Юзайте для refreshSessions Redis и все будет пучком.

@Ridzhi

This comment has been minimized.

Copy link

Ridzhi commented Jul 19, 2020

@zmts А можете чуть подробнее про хранение access токена в памяти. Интересует кейс что происходит если я открываю новый таб в браузере, ведь access токена у меня больше нет...

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Jul 28, 2020

@Ridzhi SPA получит новый access токен. Это все :)

@bmind12

This comment has been minimized.

Copy link

bmind12 commented Jul 29, 2020

Сервер сгенерил юзеру токен с указанной ролью
...
Сервер смотрит на права(роль) юзера в токене и соответственно пропускает или отсекает запрос

Добавление роли это один из частных случаев. Наличие токена само по себе уже может быть достаточным фактом для авторизации.
Я бы не стал указывать частный случай в общем мануале, или как минимум уточнил, что это - частный случай и один из вариантов авторизации. Иначе может возникать ложное представление о том, что всегда должно быть поле с ролью, которая определяет права. На самом деле это может быть просто наличие токена или комбинация полей или любое другое поле (в зависимости от бизнес логики).

@zmts

This comment has been minimized.

Copy link
Owner Author

zmts commented Jul 30, 2020

@bmind12 Согласен, спасибо поправлю

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.