Pull to refresh

Comments 53

Спасибо за пример, в принципе если не охота со всякими либами морочиться, то вроде как вполне рабочая тема в статье.

Токены стоит сохранять только в HttpOnly cookie.

Почему же? Разве хранение токена в local storage + защита от CSRF не является безопасным вариантом?

Извините, тут не выйдет использовать HttpOnly куки.
Но я про то, что любое расширение может читать обычные куки и localstorage. В xss внедренный код тоже.
CSRF спасает от выполнение со стороннего сайта, а не от кражи куки.

Какая еще защита от CSRF, если уже используется нестандартный заголовок Authorization? Зачем ??? Он уже сам по себе защита.
Я согласен что хранить токены в сторах небезопасно. Но их основное применение это ajax запросы. Что же делать? Я например храню в куки и потом получаю с сервера запросом который эту куклу мне расшифровует. Не знаю насколько это нормально.
Токены стоит сохранять только в HttpOnly cookie.

Это шутка такая? Чем он тогда будет отличаться от обычного идентификатора сессии?

Как же людям маркетологи голову запудрили с этими токенами. Сначала пихают их куда только возможно, а потом пытаются из них сделать более логически верное и безопасное решение — то есть сессию.
Токен токену рознь. Я общался с одним программистом который свою речь строит на комбинации слов токен и кейс. Такая вот двоичная арифметика. Поэтому я рассмотрю конкретно jwt. Jwt ниразу не сессия т.к. в общем случае выдается отдельным сервером авторизации и является неизменяемым. Поэтому jwt не является сессией. Насколько jwt более секьюрный чем традиционная сессия. Секьюрно от его лежит немного в другой плоскости. Его должен выдавать хорошо защищённый сервер основная функция которого выдавать токены. Топ есть не получится найти очередную лазейку в коде очередной cms чтобы получить доступ к сессии. А в остальном, особенно если тот же jwt выдает не специализированный сервис а то же самое веб приложение то конечно по секьюрности все один к одному.

Возникает вопрос а зачем же огород городить было. Опять же про jwt. Применение jwt позволяет масштабировать приложение т.к. учетные данные клиента не хранятся в сессии и не перезапрашиваются при каждом запросе в базе данных. Следовательно api можно рассшаривать на несколько физических серверов не боясь той проблемы что логин клиента будет на одном сервере а запрос к api придет на другой сервер.

Но согласен с Вами в той части что я часто встречаю реализацию jwt которая не использукт ни возможность секьюрности ни возможность масштабирования. То есть токены выдает тот же сервер на котором работает api и в токена хранится только id клиента по которому каждый раз запрашивается база данных.

Это уже чистыймаркетинг

Как минимум даже такое испольщование jwt это готовность к масштабированию без изменения фронта.

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

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

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

UFO just landed and posted this here
По идее отменяет. Jwt вроде не рефрешат с помощью рефреш токена

Ни в OIDC, ни в OAuth 2 нет ограничений на то, что лежит в access token, так что там может быть и JWT (и, например, IdentityServer так и делает, если поставить "значимые токены").

JWT был создан для stateless серверов, как более-менее доверенное хранилище информацию о пользователе/запросе. Иными словами суть использовани JWT — если сервер смог его расшифровать, то дополнительно валидировать его не надо. Также не надо лазить в базу проверять просрочен он или нет. Иначе использование JWT также теряет смысл.

Во-первых, то, что вы описали, не противоречит использованию refresh-токенов. Во-вторых, эта схема подразумевает, что у вас нет отзывов… что не всегда так.

Я писал чисто про JWT, как про криптографию для доверенного хранения данных на клиенте между запросами. Он особо не говорит как именно должен проходить процесс авторизации. С другой стороны, а как вы его будете отзывать? Можете рассказать подробнее? Да и какой смысл в JWT, если при каждом запросе все равно надо сверяться с базой?
Конечно отзыв JWT токена не прописан в стандарте. Но здесь схема лежит на поверхности. JWT токен как я уже говорил имеет смысл только при его постоянной ротации. И все же иногда для особо критических случаев важно мгновенно этот токен аннулировать. Ничего не поделаешь, придется составить «черный» список идентификаторов клиентов и моментов времени когда поменялись его учетные данные. Хранить запись в этом черном списке нужно только на время действия токена. То есть это список будет содержать не так уж много данных и его можно хранить в каком-то быстром хранилище в зависимости от потребностей — в кассандре или в редисе.
Ну то есть, получается для каждого авторизованного запроса нужно будет слазить таки в какое-то «быстрое» хранилище и весь наш заявленный stateless тут же исчезает. Получается JWT не решает проблему сессий и вообще ни чем не лучше. Сессию тоже можно положить в «быстрое» хранилище и ратировать чаще даже чем JWT. И без всякой крипты.

На самом деле, так как вы написали делать конечно не надо. Если мы «доверяем» JWT настолько, что готовы не лазить в хранилища и пользоваться всеми благами stateless серверов, то чтобы анулировать JWT достаточно в него же положить expired дату. После расшифровки ее прочитать и не пускать дальше. Ну а получить новый JWT юзер должен уметь в любой вообще момент времени, даже если его текущий токен не просрочился. И конечно же хранить сам JWT на серверах нельзя, stateless же.
Обратите внимание, что я написал об том что «в особо ответственных случаях» если критично что в течение продолжительности жизни (пусть это будет 5 минут) клиента нужно разлогинить. Например он за это время может снять деньги со счета который заблокирован или админ сайта хакнуть сайт после несправедливого с его точки зрения увольнения с работы. То да черный список должен быть. Ходить за ним в базу данных специально нет необходимости т.к. есть средства асинхронного обмена. Но в большинстве случаев этого совершенно справедливо делать не нужно.

Что касается реальных приложений я действительно пока еще не юзал токены со сроком действия (веренее юзал для временных токенов) А клиенту после логина сообщал клал в токен только его идентификатор. После чего чтобы не лазить в базу хранил юзера в редисе со сроком действия и перезапрашивал в базе по истечению срока. В основном это было сделано исключительно из за того что согласовать с мобильными разработчиками рефреш токена было достаточно неперспектовной идеей. Т.к. если бы я их даже уговорил, добиться беспроблемной ротации при отстутсвии хорошо отлаженной билиотек, которая делала бы примерно то о чем эта статья сверху — было бы очень долго и не факт что не было бы сбоев у клиентов.
Я писал чисто про JWT, как про криптографию для доверенного хранения данных на клиенте между запросами. Он особо не говорит как именно должен проходить процесс авторизации.

Вот именно поэтому утверждение "Jwt вроде не рефрешат с помощью рефреш токена" и ошибочно.


С другой стороны, а как вы его будете отзывать?

Как обычно — держать список валидных, и проверять в нем.


Да и какой смысл в JWT, если при каждом запросе все равно надо сверяться с базой?

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

Сппцификация jwt просто о другом. Она не определяет протокол получения и использования токена. Она содержит его статическое что ли определение как документа а не ка процедуры его. Издания и т.п. с точки зрения бэкэнда к нему приходит некоторый jwt. После этого проверяется его подпись, время создания и если все нормально он принимается в работу. В связии с этим возникает вопрос о том что если давать бессрочный jwt то мы лишается возможности Отозвать такой токен. Следовательно нужен сравнительно короткий срок его действия. Если у токена короткий срок действия то у клиента должен быть путь для обновления токена. Конечно совсем не обязательно что это рефреш токен. Может быть сертификат или ещё что то что будет использоваться клиентом в автоматическом режиме без участия пользователя. Как один из вариантов рефреш токен. То есть резюмируя jwt подразумевает постоянную ротацию токенов. Одним из вариантов такой ротации организуется при помощи рефреш токена.

Судя по характерным признакам, речь идет про OAuth 2. Речь именно о нем, или о каком-то своем велосипеде?

Доступ с двумя токенами это совсем не обязательно oauth2 если просто клиенту отдать после какого то подтвкрждения два токена например jwt то мы имеем два положительных результата
1 я могу не ходить в базу данных за учётной записью пользователя т.к. в токена пришла актуальная информация которая отстаёт максимум на время действия токена
2 многие утверждают что это более безопасно хотя я не уверен. Во всяком случае выглядит более секьюрно
Доступ с двумя токенами это совсем не обязательно oauth2

Я знаю, что это не обязательно oauth, поэтому и решил сначала уточнить, что же это.


если просто клиенту отдать после какого то подтвкрждения два токена например jwt то мы имеем два положительных результата

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

По поводу велосипеда. На стороне сервера все уже готово в технологии jwt. Теперь есть вопрос к клиентам. Насколко я понимаю общепринятого клиента для ротации тоаенов пока нет. Поэтому я собственно использую их только в том случае если пишу сам и бэк и фронт. Иначе будут одни траблы.
На стороне сервера все уже готово в технологии jwt.

Не, совсем не все. В "технологии JWT" описано, что делать в случае кражи токена, или это все-таки просто "a compact, URL-safe means of representing claims to be transferred between two parties"? А то там написано, знаете ли, что "The contents of a JWT cannot be relied upon in a trust decision unless its contents have been cryptographically secured and bound to the context necessary for the trust decision."


Насколко я понимаю общепринятого клиента для ротации тоаенов пока нет.

Если речь об OAuth 2, то клиентская часть там стандартизирована, и я встречал ее имплементации в виде промежуточных хэндлеров. Другое дело, что всегда есть вопрос "что делать, если рефреш тоже протух".

если рефреш протух то это как бы заново авторизоваться.

Спасибо, кэп. В зависимости от приложения это совершенно разный флоу.

А как он может протухнуть, если он не экспайрится? AcesssToken экспайрится по времени, RefreshToken одноразовый просто

Во-первых, он прекрасно экспайрится, он не бессрочный.
Во-вторых, он может быть отозван.
В-третьих, он может быть кем-то использован, и, как следствие, тоже перестать быть валидным.


(в-четвертых он не обязан быть одноразовым)

Во-первых, он прекрасно экспайрится, он не бессрочный.

С чего вы это взяли? Тогда он практически теряет свой смысл

Во-вторых, он может быть отозван.

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

В-третьих, он может быть кем-то использован, и, как следствие, тоже перестать быть валидным.

Одноразовость как раз и делает его практически бесполезным для кражи. Можно воспользоваться один раз и получить временный токен. Далее юзер опять авторизуется. Выданный рефреш обнуляется, а access отзывается.

(в-четвертых он не обязан быть одноразовым)

Я вас ни к чему не обязываю вообще. Просто это наиболее рабочая схема.

С чего вы это взяли?

RFC 6749 (OAuth 2) 5.2 Error Response: "invalid_grant [...] refresh token is invalid, expired, revoked".


На практике как минимум Azure AD выдает рефреш-токены со сроком жизни дней в 90.


Тогда он практически теряет свой смысл

Нет, не теряет. Просто мы контролируем доступ в долгосрочной перспективе.


Более того, он «отзывается» при каждом использовании.

Не обязательно. "The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client."


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

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


(это одна из причин, почему refresh token делают ограниченным по времени)

RFC 6749 (OAuth 2) 5.2 Error Response: «invalid_grant [...] refresh token is invalid, expired, revoked».

И что тут написано? Что так можно делать, да. Я не говорил что так нельзя делать. Я говорил не нужно.

На практике как минимум Azure AD выдает рефреш-токены со сроком жизни дней в 90.

Для вас MS — это образец как нужно? Это не тот ли Azure на котором еще 4 года назад было практически невозможно поднять linux-сервер?

Нет, не теряет. Просто мы контролируем доступ в долгосрочной перспективе.

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

Не обязательно. «The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client.»

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

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

Да, приложение потеряет доступ и будет показывать форму логина. Юзер введет логин, получит новые токены, а злоумышленник останется с носом. Именно то, что рефреш одноразовый делает очевидным факт его кражи. Если кто-то украл его и воспользовался, юзера сразу «выкинет» из системы, а не в тихую будет продолжать работать.
И что тут написано? Что так можно делать, да. Я не говорил что так нельзя делать. Я говорил не нужно.

Вы спросили, с чего я это взял. Вот с этого и взял.


Для вас MS — это образец как нужно?

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


Иными словами, если даже срок не вышел, но рефреш токен был использован, он автоматом должен анулироваться.

Как уже говорилось, на это есть разные взгляды.


Вы воспринимаете то, что я говорю слишком мандаторно. Я говорю как лучше делать, а не то, что сделать по-другому невозможно.

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


Юзер введет логин, получит новые токены, а злоумышленник останется с носом.

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

Да, кстати интересный вопрос. Как технически отзывать рефреш токену после использования. Если с акцес токенами все более или менее ванильно. Тк 1 отзываться они будут сравнительно редко и 2 хранить их в черном списке нужно на время действия. То с рефреш токенами все сложнее. Во-первых отзываться они будут все и хранить черный список нужно будет долго. Второй вариант который мне недавно озвучили это хранить таблицу идентификатор клиента плюс идентификатор валидного токена. Но тут мы приходим к тому что на каждом запросе обращаться в базу данных.

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

Вообще-то, для рефрешей это практически данность, потому что там надо очень желательно проверять валидность самого клиента, а это client credentials, и запрос, что клиент не заблокирован (и еще много интересного).

По поводу одноразового использования рефреш-токена все не так просто как хотелось бы. Я как раз на днях обсуждал эту тему. Если рисовать на бумаге то все выглядит просто и красиво. В реальной асинхронной и не надежной сетевой среде все получается очень сложно. Я перечислю только первые пришедшие в голову ситуации
1. Токен был запрошен с клиента и аннулирван сервером после чего запрос клиенту был отправлена новая пара токенов. Запрос до клиент ане дошел из-за сетевых проблем и теперь у клиента есть просроченный акцес с токен, аннулированный рефреш токен и нет возможности их обновить. Никакой
2. То же самое но теперь из-за медленной работы сети клиент после таймаута сделал дублирующий запрос (это например так устроен клиент REST на Андроиде) Первый из запросов клиент сбросил а второй запрос заканчивается с ошибкой т.к. идет повторное обращение с одним и тем же рефреш токеном.
3. Просто два конкурирующие запроса которые вычислили что нужно обновить токены и все отправили запросы — в результате токен обновился один первый а все остальные завершились с отказом и с ошибкой
Все эти проблемы могут быть в реальном мире и бывают. Решаются простым повторым входом юзера. Если рефреш токен будет не одноразовым, то его можно украсть, получить токен и без ведом юзера делать что хочешь. Если же рефреш будет одноразовым, то получение доступа злоумышленником автоматом разлогинит юзера и попросит залогинится снова. Повторный вход «выкинет» злоумышленника из системы.
Про пользу одноразового токена это все понятно. Но те которые приводят к нарушению красивой схемы это не такая уже большая редкость. В связи с чем разлогины будут не у одного пользователя пару раз в год, а у всех пользователей пару раз в день. Можно конечно придумать схему чтобы учесть такие случаи. Но она пожалуй становится слишком громоздкой.

Когда говорится о защищенности рефреш токена то имеется в виду то что Вы его не присылаете на сервер приложения а обмениваетесь им по гарантированно защищенному каналу с сервером токенов. Прямая аналогия это то что вы не присылаете в интернет-магазин номер своей платежной карточки. Если его кроме всего прочего не сохранять в сторах которые могут быть скомпрометированы или в глобальных переменных которые могут быть прочитаны то украсть его модно только вместе с устройством.
Про «пару раз в день» сказать не могу. Нет статистики на руках, хотя все же думаю, что не все так печально. Как миниму пару раз в день не получится, если aceessToken будет жить сутки, к примеру.
Идея была показать общий способ реализации авторизации на фронтенде, когда сервер отдает ответ вида: token и refresh token.

Гм. Это OAuth 2 Resource Server, или это какая-то своя авторизация?

Когда писал статью, да, основывался на OAuth

Если это OAuth 2, то есть несколько нюансов.


Во-первых, обновление токена может требовать (и часто требует) client credentials, которые у вас не фигурируют.


Во-вторых, в браузере намного чаще используется Implicit Grant, в котором refresh token не выдают (в частности, потому, что в браузере сложно выполнить требование "Refresh tokens MUST be kept confidential in transit and storage, and shared only among the authorization server and the client to whom the refresh tokens were issued.")

В этом способе есть минус — если вы сделаете несколько обращений к API подряд — то получите сразу несколько запросов на refresh.
Логичней будет ввести метод getToken, унести в него всю логику работы с токеном и использовать его в fetchWithAuth.
Если токен есть и он не expired — getToken вернёт уже resolved промис с токеном, а если токен надо обновлять — вернёт промис на refresh. Этот промис надо сохранить и возвращать его всегда если он в статусе pending — таким образом любое количество API-запросов инициирует только один запрос на refresh.
Только нативные промисы не дают узнать свой статус. Поэтому или колхозить некий свой статус промиса, или использовать drop-in альтернативы типа bluebird.
Спасибо за замечание, согласен, этот момент можно учесть и улучшить способ.
Функция для обновления токена
const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
saveToken(newToken);


По-моему кроме обновления самого токена, ваша функция должна ТАКЖЕ обновлять и refresh-token.

if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена

Разве можно полагаться на Date.now()? Если «хакер» подменит время в системе?

Хранить данные для авторизации мы будем в sessionStorage или localStorage, в зависимости от наших нужд. В первом случае данные хранятся до тех пор, пока пользователь не завершит сеанс или не закроет браузер

Какой смысл хранить токены в sessionStorage? Почему не в самом клиентском коде? Раз уж он будет доступен только на время жизни клиентского приложения.
По-моему кроме обновления самого токена, ваша функция должна ТАКЖЕ обновлять и refresh-token.

refreshToken() возвращает объект такого же формата, что и при авторизации по логину/паролю: с полями token, refresh_token и т. д.

Какой смысл хранить токены в sessionStorage? Почему не в самом клиентском коде? Раз уж он будет доступен только на время жизни клиентского приложения.


В статье указано, что можно хранить не только там, это зависит от наших нужд.

Проверка на клиенте как обычно лишь способ улучшить UX и снизить трафик и нагрузку на сервер.

А точно надо в сторейдж сохранять промис? Может лучше сами токену?
Sign up to leave a comment.

Articles