Skip to content

Instantly share code, notes, and snippets.

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

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

Last major update: 25.08.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). Все поля в payload это свободный набор полей необходимый для реализации вашей частной бизнес логики. То бишь user_id и user_role не являются требованием и представляют собой исключительно частный случай. Сам токен храним не в localStorage как это обычно делают, а в памяти клиентского приложения (что приводит к необходимости при каждом обновлении странички браузера запрашивать новую пару токенов).

refresh token - выдается сервером по результам успешной аутентификации и используется для получения новой пары access/refresh токенов. Храним исключительно в httpOnly куке.

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

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

Роль рефреш токенов и зачем их хранить в БД ?

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

Как ставить куки ?

Для того что бы refreshToken кука была успешно уставленна и отправлена браузером, адреса эндпоинтов аутентификации(/api/auth/login, /api/auth/refresh-tokens, /api/auth/logout) должны располагася в доменном пространстве сайта. Тоесть для домена super.com на сервере ставим куку с такими опциями:

{
    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 дней, тогда придется заново вбить логин/пароль.

Logout (api/auth/logout)

  1. Front-end делает кол POST: api/auth/logout c refreshToken в куке или бади (лучше в куки)
  2. Front-end удаляет локально сохраненный в памяти accessToken
  3. Back-end удаляет запись из таблицы refreshSessions по refreshToken

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

В случае кражи 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'а:

Стащить все авторизационные данные это не из легких задач, но все же допустим этот кейс как крайний и наиболее неудобный с точки зрения UX (без примера в кодовой базе supra-api-nodejs).

Предложу несколько вариантов решения данной проблемы:

  • Хранить IP или Subnet залогиненного клиента
  1. Хакер воспользовался access token'ом
  2. Закончилось время жизни access token'на
  3. Хакер отправляет refresh куку и fingerprint
  4. Сервер проверяет IP хакера, хакер идет лесом

UX минус: нужно логинится с каждого нового IP.

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

UX минус: в каждом случае когда сервер не будет находить рефреш токен - будут сбрасиватся все сессии юзера на всех устройствах.

Зачем все это ? 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


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

@fls-dev
Copy link

fls-dev commented Jan 9, 2024

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

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

@Devoter
Copy link

Devoter commented Jan 10, 2024

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

@neverovski
Copy link

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

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

@zakirovtech
Copy link

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

@khokm
Copy link

khokm commented Mar 23, 2024

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

@khokm
Copy link

khokm commented Mar 23, 2024

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

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

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

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

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

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

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

@Mixaqi
Copy link

Mixaqi commented May 17, 2024

А как вообще формируется путь, например, для личного кабинета при jwt аутентификации? Декодировать же токен на фронте нельзя, если я не ошибаюсь. Тащить каждый раз user_id из стора, но стор обнуляется после перезагрузки или записать айди из стора в хранилище и чистить его при выходе юзера из аккаунта?

@Tsyklop
Copy link

Tsyklop commented May 17, 2024

@Mixaqi Можно декодировать первые две части jwt токена - это обычный base64. После декодирования JSON.parse.

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

@Mixaqi
Copy link

Mixaqi commented May 17, 2024

@Tsyklop А если сохранять айди прямо из этого ответа сервера в локалсторедж и по нему делать навигацию?

{
    "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcxNjAyMDQ0NiwiaWF0IjoxNzE1OTM0MDQ2LCJqdGkiOiI4YjYxYTQxMWEyYTA0NDBmYjM5ZDVmY2Y4NmRkYWE5YiIsInVzZXJfaWQiOjF9.T56IleWdqbMHIkY7ysf9HKIK-bIf96zbpgGcsWOB_lE",
    "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzE1OTM0MzQ2LCJpYXQiOjE3MTU5MzQwNDYsImp0aSI6ImRhNGJiMDJlNGIzODRlODFiYTk5MjFiOTdjZmIwOWRmIiwidXNlcl9pZCI6MX0.NYRSsaGFq4RzA0iXSlfZh7rNRDBCeIOtJc7ZLsY0O9w",
    "user": {
        "id": 1,
        "username": "Mixaqi",
        "email": "mixaqimixaqi@mail.ru",
        "is_active": true,
        "created_at": "2024-03-22T12:48:27.258541+03:00",
        "updated_at": "2024-03-22T12:48:27.261546+03:00"
    }
}

@Tsyklop
Copy link

Tsyklop commented May 17, 2024

@Mixaqi Зачем его хранить в LS? храните в памяти и все. Импортируйте куда надо. При перезагрузке страницы все равно новая пара токенов, а с ними и данные пользователя возвращаются (как я понял в вашем случае).
Та и я не особо понимаю такую привязку к id пользователя. По идее сервак должен и так знать id юзера после проверки jwt токена.

@Mixaqi
Copy link

Mixaqi commented May 17, 2024

@Tsyklop Не совсем понимаю, что значит в памяти: опыта маловато еще. У меня есть в сторе этот id, но стор же чистится после перезагрузки. Это как-то кешировать надо?

@Semmy99
Copy link

Semmy99 commented May 23, 2024

А что если хранить оба токена в куках с http only и прочими настройками Для монолитных приложений ?

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

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

Хотя наверно смысла мало тогда от акссесса.
Подскажите чем плох такой вариант ?

@LuchkinDS
Copy link

LuchkinDS commented May 23, 2024

@Semmy99 если jwt-токен хранится в httponly куках, что предпочтительно для web приложения, то смысл во втором токене (refresh) пропадает и можно использовать только один (access). практически все можно хранить в памяти, спокойно досылая нужные данные при перезагрузке страницы, смысл токена в сохранении состояния backend "без состояния")

@fls-dev
Copy link

fls-dev commented May 23, 2024

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

@LuchkinDS обычно refresh оставляют в httponly только для роута /refresh

@LuchkinDS
Copy link

@e-motionless, это не то что возможный, это наиболее правильный способ взаимодействия апишки с web-приложением. в таком виде, от refresh-токена вообще можно отказаться, access-токен, например, вообще можно генерить новый для каждого запроса.

@asminog
Copy link

asminog commented May 27, 2024

Из минусов нельзя вылогиниться если сервер лежит

@shert391
Copy link

shert391 commented Jun 19, 2024

Здравствуйте. К сожалению, статья максимально соответствует стандартным догмам реализации аутентификации, что не ново. Но почему-то никто не объясняет мотивов. В частности, так и не удалось понять, для чего использовать ДВА токена, а не обойтись одним , аргумент - один для обновления другой для авторизации - не исчерпывающий. Почему не использовать ОДИН токен с достаточно продолжительным сроком жизни? Аргумент - его своруют - тоже не котируется. Ведь вы сами предложили метод с созданием таблицы сессий, где находятся uuid и , возможно, Ip, а значит, очень легко пресечь не санкционированный вход даже с ОДНИМ токеном. Зачем нагружать тело запроса лишним объёмом в виде бесполезного второго токена.

Также почему в подавляющем большинстве статей постулируют хранить access токен в localStorage, либо в памяти приложения?
Разве не лучше использовать куки? LocalStorage: чтобы положить данные в хранилище, нужно писать кодец - дергать api браузера, а значит, есть риск, что токен будет скомпрометирован XSS-атакой. В тот же момент куки: есть все необходимые атрибуты заголовка, что нативно, без заморочек, позволяет защитится от XSS и CSRF; также токен браузер подставляет в каждый запрос самостоятельно и не надо описывать никакой процесс ручками.

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

@Tsyklop
Copy link

Tsyklop commented Jul 1, 2024

@shert391 Предложите свой вариант, раз этот не такой хороший.

@AntsiferovMaxim
Copy link

@shert391 в этом обсуждении никто не объясняет мотивы, потому что сюда люди приходят в поиске конкретной реализации. Если вам интересно интересно разобраться в мотивах, то можно чуток погуглить и найти первоисточник:
https://datatracker.ietf.org/doc/html/rfc6749
https://www.rfc-editor.org/rfc/rfc7523.html
https://datatracker.ietf.org/doc/html/rfc7519

Или на том же stackoverflow есть прекрасный ответ jwt vs sessions
https://stackoverflow.com/a/45214431

@shert391
Copy link

shert391 commented Jul 3, 2024

@AntsiferovMaxim
Это хорошо конечно, что навыки пользоваться гуглом присуще вам, но, к сожалению, си-я навык не всегда помогает находить правильный ответ на конкретно поставленный вопрос или вы не разобрались с поставленными мною вопросами?
Когда фундаментальные тех.доки (в частности RFC) стали отождествляться с поисками мотивов и распутывания спагетти в голове людей (зашедших в этот тред), которым также, как и мне будет интересно узнать ответы. RFC, увы - это лишь стандарт, тех.дока, которая описывает КАК НАДО делать, чтобы СООТВЕТСВОВАТЬ стандарту, но никак не пособие по мотивам. Возможно, помимо тех реализации вы увидели изюм, тогда пожалуйста предоставьте цитирование - уверен, многие отблагодарят вас, кто задаться такими же вопросами позднее.

По поводу отличий sessions от jwt - спасибо большое, освежил память. Но совсем не понял, как сессии относятся к контексту вопросов выше?

@Tsyklop Спасибо за ответ. Вы очень развернуто и профессионально помогли разобраться

@zmts
Copy link
Author

zmts commented Jul 3, 2024

@Tsyklop
Copy link

Tsyklop commented Jul 3, 2024

@zmts Я пересмотрел то что по ссылкам и у меня уже возникли вопросы.

По большому счету все плюсы JWT сводятся к микросервисам. Если не используются микросервисы - jwt не особо приносят пользу.
На практике приходится делать запрос в бд за данными пользователя при каждом запросе.

@zmts
Copy link
Author

zmts commented Jul 3, 2024

Делайте выводы и выбирайте инструмент под свои задачи. В конце статьи есть постскриптум по этому поводу ;)

@qFamouse
Copy link

@zmts Максимально исчерпывающее объяснение. Получил эйфорию от прочтения. Большое спасибо) 😇

@SciBourne
Copy link

@zmts Очень признателен за статью)

У меня крайне мало опыта, но мне пришла мысль - не будет ли более простым в реализации и эффективным, с точки зрения кол-ва запросов, решением для web-приложения следующая схема:

  1. Пользователь логинится и сервер генерирует refresh и access токены
  2. Кладёт refresh-токен в БД
  3. Пишет оба токена в http-only куки
  4. Клиент, ничего не подозревая об их существовании, отдаёт их только на защищённые маршруты (например, .../orders)
  5. Middleware, висящее на таких маршрутах сервера, при получении запроса верифицирует как сам access-токен, так и его свежесть
  6. В случае его протухания берёт refresh-токен и сравнивает его с тем, что лежит в БД
  7. При успешной верификации генерирует новую пару токенов, обновляет запись в БД и пишет новые значения в куки

Что получаем:

  1. Отсутствие частых запросов к БД за счёт access-токена (как и предполагается)
  2. Отсутствие расходов на отдельные запросы обновления токенов и авторизацию
  3. Упрощение реализации клиента (кроме интерфейса он ничем не заведует)
  4. Упрощение реализации сервера (одна middleware на все защищённые маршруты)

@nickname-is
Copy link

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

@AlexanderMint
А как вы объединили два подхода? Я смотрел ваши комментарии, поскольку привлекла реализация и вот последнее сообщение ввело в тупик. С микросервисами обычно ведь используются токены, собственно это и описано, однако также упоминаются сессии в httpOnly cookie, которые обычно не особо дружат с микросервисами, так как нарушается изоляция между сервисами. Я просто начинающий разработчик и мне сложно представить реальные production решения, которые используемы на практике и в довольно нагруженных проектах. Можете объяснить, для чего и каким образом сочетаются два подхода, а также где вы храните сессии: в ОЗУ через, допустим, Redis с бэкапами или просто на накопителях через СУБД, которые сохраняют данные в ПЗУ?

@qFamouse
Copy link

qFamouse commented Sep 23, 2024

@zmts @nickname-is Присоединяюсь к вопросу о местах хранений сессий. Каков идеальный подход?
В статье упоминается:

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

Мол лучшее место для хранения — это ОЗУ, в частности Redis, что, в принципе, логично. Вопросов по этому поводу нет. Однако, возникает вопрос: как именно реализовать хранение? Нужно ли хранить данные одновременно в Redis и в обычной базе данных, используя Redis как кэш? Или можно ограничиться только Redis для хранения сессий? Если да, то в каком виде должны храниться данные в Redis? Например, следует ли сохранять объекты в виде строк и сериализовать их при каждом обращении?

Также для полноты картины стоит рассмотреть механизмы сохранения данных, такие как RDB и AOF. Какой план действий в случае сбоя или перезапуска Redis? Следует ли вести журнал операций (AOF), делать снапшоты (RDB), или можно отказаться от этих механизмов?

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

@khokm
Copy link

khokm commented Nov 19, 2024

Не знаю насчет крупных компаний, но для стартапов, которые вечно озабочены проблемой auth, токенов и иже с ним, вместо решения действительно нужных задач, решение простое:

Да сделайте вы просто JWT, с привязкой к IP, чтобы его не стащили. Ротация с TTL в 24 часа - это абсолютно нормально. Короткий TTL абсолютно никак не поможет - взломщик, если украдет токен, легко получит все что нужно и за стандартные 20 минут. У ротации всего две причины: сменившийся IP, либо необходимость отозвать права. Ну проходит юзер еще один день админом, ничего страшного не произойдет - такие вещи планируют заранее. А чтобы добавить прав, достаточно попросить его перелогиниться.

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

Храните в хттп only куках - но не для безопасности, а чтобы просто чтобы не полоскать мозги фронтам. Фронт вообще никак на хттп-куки влиять не может; соответственно, он о них вообще не думает. Безопасность здесь идет как бонус. Слово localStorage забудьте; можете там просто флаг хранить, залогинен или нет. А лучше при запуске приложения получать инфу с сервера, валидна ли текущая сессия.

Никто у вашего стартапа этот токен не потырит. Да и не сможет. Хватит дурью маяться; лучше сделайте уже работу и идите нормально отдыхать.
Безопасность - это супер важно, но когда вот эти стартапы на самом важном своем этапе тратят время какие-то схемы с треми кербрер серверами (ну и в этом роде) - это не адекватный подход, а просто наружу вылезает О.К.Р лида (и у меня оно было).

Могу конечно и ошибаться, да и понимаю, что такой вариант может ломать множество устоявшихся решений;
но пока все кому его предлагал, устраивало.

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

@ninjakuro
Copy link

ninjakuro commented Nov 19, 2024

Не знаю насчет крупных компаний, но для стартапов, которые вечно озабочены проблемой auth, токенов и иже с ним, вместо решения действительно нужных задач, решение простое:

Да сделайте вы просто JWT, с привязкой к IP, чтобы его не стащили. Ротация с TTL в 24 часа - это абсолютно нормально. Короткий TTL абсолютно никак не поможет - взломщик, если украдет токен, легко получит все что нужно и за стандартные 20 минут. У ротации всего две причины: сменившийся IP, либо необходимость отозвать права. Ну проходит юзер еще один день админом, ничего страшного не произойдет - такие вещи планируют заранее. А чтобы добавить прав, достаточно попросить его перелогиниться.

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

Храните в хттп only куках - но не для безопасности, а чтобы просто чтобы не полоскать мозги фронтам. Фронт вообще никак на хттп-куки влиять не может; соответственно, он о них вообще не думает. Безопасность здесь идет как бонус. Слово localStorage забудьте; можете там просто флаг хранить, залогинен или нет. А лучше при запуске приложения получать инфу с сервера, валидна ли текущая сессия.

Никто у вашего стартапа этот токен не потырит. Да и не сможет. Хватит дурью маяться; лучше сделайте уже работу и идите нормально отдыхать. Безопасность - это супер важно, но когда вот эти стартапы на самом важном своем этапе тратят время какие-то схемы с треми кербрер серверами (ну и в этом роде) - это не адекватный подход, а просто наружу вылезает О.К.Р лида (и у меня оно было).

Могу конечно и ошибаться, да и понимаю, что такой вариант может ломать множество устоявшихся решений; но пока все кому его предлагал, устраивало.

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

Я бы добавил, что зачастую достаточно одной httpOnly-куки, а вся эта история с JWT-токенами не нужна! Она имеет место быть в очень редких случаях, например в случае с микро-сервисами, и то не всегда.

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