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

В статье рассмотрим причины необходимости работы с токеном на клиенте веб-приложений, узнаем ,что лучше для хранения токена: localStorage, sessionStorage или cookie без флага HttpOnly (спойлер, ничего из этого), а также посмотрим на меры воздействия, которые можно использовать для снижения риска утечки токена посредством различных уязвимостей.

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

Введение

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

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

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

Access token (токеном доступа) здесь и далее я буду называть некую строку, используемою для доступа к защищенному ресурсу (protected resource). Сам токен может иметь совершенно разные форматы и имплементации.

Также в статье будем оперировать понятиями XSS и CSRF.

Проблематика

Неподходящая для конкретного веб-приложения реализация аутентификации и работы с токенами на клиенте приводит к повышению уровня критичностей уязвимостей в нем. Самый простой пример: если у нас access token для доступа к ресурсам доступен в клиентской части, то любая XSS автоматически дает кражу учетной записи пользователя, поскольку внедряя и выполняя код на клиенте в контексте страницы мы можем получить доступ к тем же данным, к которым обращаются легитимные скрипты.

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

Кто-то скажет здесь:

Ну так это пример в вакууме, мы делаем наши приложения без XSS-уязвимостей, у нас есть %framework_name% или %library_name%, там это все учтено. Зачем нам что-то додумывать, если мы сфокусируемся на том, чтобы не допускать такие уязвимости вообще?

Звучит резонно, не правда ли? Однако реальность, к сожалению, не так проста. Практика и подходы показывают нам, что гарантию такую дать в большинстве случаев почти нереально. К тому же запросто может быть случай, где угрозы безопасности нашим веб-приложениям приходят совсем из других мест: например, токен мы положили в какую-нибудь wildcard и не-HttpOnly-cookie, обезопасили свое приложение от XSS до зубов, но вот незадача, на одном из поддоменов обнаружился старый дырявый проект, который свел на нет все наши старания.

Чем опасен украденный access token? С ним злоумышленник сможет выполнять запросы к ресурсам API от нашего лица или же попросту имперсонализироваться под нашей учетной записью у себя в браузере.

Примеры (или набор вредных советов)

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

Access token в localStorage

В данном приложении access token после получения с бэкенда помещается в локальное хранилище localStorage.

В таком случае при наличии XSS-уязвимости токен может быть получен злоумышленником. Однако у токена указан срок жизни - 15 минут. Допустим ли риск того, что в течение N (N<=15) минут токеном пользователя может воспользоваться злоумышленник, - вопрос над которым при применении такого решения следует подумать.

Пара access и refresh token в localStorage

В этом примере приложение помещает в localStorage пару: access token и refresh token. Access token имеет срок жизни - 60 минут. Refresh token - при исследовании был валиден и спустя более чем 12 часов.

Здесь проблема из первого примера расширяется. Access token имеет уже куда больший срок жизни - целый час. Но также мы имеем и долгоживущий refresh token. Refresh token - строка, используемая для получения access token. В то время как access token используется для доступа к защищенному ресурсу refresh token позволяет обратиться за получением нового (или дополнительного) access token.

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

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

Access token в wildcard не-HttpOnly cookie

Напоследок рассмотрим самый интересный вариант.

В примере мы видим, что токен помещается в сookie со сроком жизни - 1 год. Сама cookie при этом не имеет установленного значения флагов HttpOnly, Secure, SameSite. Кроме этого, видно, что домен начинается с точки: .%sitename%.com. Значит, данная cookie является wildcard, то есть будет доступна на всех поддоменах, удовлетворяющих такому условию.

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

  • Во-вторых, здесь токен помещен в cookie, которая доступна на всех поддоменах сайта. Это означает, что даже если в самом основном приложении XSS злоумышленник и не найдет, то среди содержимого наших поддоменов запросто может оказаться уязвимый сервис. Поиск поддоменов - один из типовых приемов, используемых при пентесте. На них может найтись много интересного: например, какой-то старый сайт или тестовый проект - потенциально уязвимый сервис. Также встречаются и атаки вида subdomain takeover (захват поддомена). В этом случае тоже злоумышленник сможет произвести кражу токена аналогичным способом.

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

Почему так, зачем нужно работать с токеном на клиенте?

Логичный вопрос: а зачем вообще нам тогда делать токен доступным на клиенте и работать с ним там? Почему бы не положить его просто в сессионную hardened-cookie? Кроме допущенных недостатков проектирования, я вижу для этого несколько предпосылок.

1. Использование stateless-токенов

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

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

Пример структуры JWT-токена
Пример структуры JWT-токена

За счет наличия блока signature токен может подписывать свое содержимое, и валидность подписи может быть проверена при получении запроса с данным токеном: приватным ключом при симметричном или публичным ключом при ассиметричном шифровании. Так валидация JWT может происходить без участия выдавшего его сервера и, соответственно, без необходимости обращения в БД для каждой проверки.

Подход позволяет как раз уйти от хранения токенов на сервере (по крайней мере, в его "ванильной" имплементации), отдавать такой JWT-токен после аутентификации и хранить его уже на том, что для нас является клиентом.

2. Использование OpenID Connect (OIDC) для аутентификации в своем же приложении

Не будем путать OpenID Connect с OAuth 2.0. OIDC расширяет возможности OAuth 2.0 и представляет собой протокол аутентификации, в то время как OAuth 2.0 является протоколом авторизации.

Причину я вижу в том, что by default реализации метода получения токена в OIDC возвращают токен в application/json теле ответа с типом Bearer. Пример ответа такого метода какой-нибудь типовой имплементации OIDC:

{
    "access_token": "<token>",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "openid"
}

Тип токена Bearer предполагается к использованию самой спецификацией OIDC. Следствие кроется в самом названии типа. Bearer-токен - токен на предъявителя.

Соответственно, кто его предъявит, тот и является авторизованным пользователем. Поскольку токен доступен на клиенте, это и используется: обычно клиент отправляет к серверу запросы с заголовком Authorization: Bearer <token>.

Ну и поскольку мы получаем токен в JSON-ответе от сервера, то на клиенте у нас не так много опций, куда этот токен можно безопасно положить.

Пару слов про OIDC и PKCE

Отдельное внимание хочу уделить использованию механизма PKCE в authorization code flow. PKCE рекомендован к использованию для public clients - клиентов, которые не имеют возможности конфиденциального хранения client secret, таким как SPA , однако применяется также и для confidential clients. Важно понимать, что PKCE не предоставляет абсолютно никакой защиты для токена на клиенте, он вообще не про это. Корректно реализованный PKCE дает возможность убедиться, что за получением токена обращается тот же субъект, который обращался за получением authorization code и все. Помните про это и не смешивайте эти понятия.

3. Использование Single-page applications (SPA) без бэкенда

Также это касается и использования Single-page applications (SPA) без бэкенда. Не используя бэкенд, разработчики вынуждены искать способы работы с токеном на клиенте.

Что можно сделать, варианты мер и решений

В интернете раньше были популярны споры, где лучше хранить токен для работы с ним: в localStorage, sessionStorage или сookies (естественно, без HttpOnly). На самом деле, с точки зрения безопасности разницы практически нет. И вот почему:

localStorage

sessionStorage

Cookies без флага HttpOnly

Доступно с клиента

Да

Да

Да

Привязка к конкретному домену

Да

Да

Да (и шире, см. wildcard-случаи)

Контекст

Синхронизируется между вкладками

Ограничен пределами вкладки

Синхронизируется между вкладками

Персистентность

Сохраняет состояние после закрытия браузера

Сохраняет состояние при обновлении вкладки, теряет при ее закрытии

Сохраняет состояние после закрытия браузера

А как же IndexedDB?

С рассматриваемых в сравнении точек зрения IndexedDB не отличается от localStorage, за исключением одного нюанса: доступ к ней есть также и у service workers.

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

А что же тогда делать? Как и везде, серебряной пули не существует, разные подходы имеют свои особенности, свои pros и cons - достоинства и недостатки. И здесь мы попробуем рассмотреть такие подходы независимо, тогда как выбор конкретного - индивидуальная задача, которую необходимо решить при проектировании.

Подход 1. Не работать с токеном на клиенте вообще

Именно так, первый и самый простой способ избежать рисков - отказаться от работы с токеном на клиенте. Использовать stateful-подход к аутентификации вместо stateless. Тогда мы можем использовать сессионную cookie для хранения нашего токена. Важно помнить, что для такой cookie потребуется корректная установка атрибутов HttpOnly, Secure, SameSite, Path. Также подчеркну, что не рекомендуется бездумно использовать wildcard cookies (причины были рассмотрены выше), поэтому внимание стоить уделить и атрибуту Domain.

Однако есть нюанс. В таком случае, поскольку данная cookie будет отправляться на сервер в запросах к указанным Domain и Path, необходимо предусмотреть защиту от CSRF-атаки. Сделать это можно, например, согласно рекомендациям OWASP.

Такой подход лишает нас возможностей, предоставляемых stateless-токенами, однако он делает наши риски более прозрачными и может быть проще в реализации.

Подход 2. Проксирующий бэкенд

А что если мы хотим оставить возможность работы со stateless-токенами? Здесь возможно использование middleware-слоя, который будет обеспечивать безопасность хранения токена.

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

Пару слов про Authorization server

По RFC 6749 под Authorization server понимается сервер, выпускающий access-токены для клиента после успешной аутентификации. Поэтому в данном случае Authorization server выполняет аутентификацию.

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

Имеем все те проблемы, о которых упоминали выше. Теперь добавим наш промежуточный слой:

Выглядит сложнее, давайте разбираться. Здесь Backend proxy уже выступает не public-, а confidential-клиентом (RFC 6749, п. 2.1), и должен выполнять обращение за получением токена со своими client id и client secret.

В процессе аутентификации и получения токена мы обращаемся не напрямую к Authorization server, а через Backend proxy, что показано в [1]. Соответственно и access token также будет получать он ([4]). Затем сервер генерирует некую cookie (подробнее рассмотрим ниже) c флагом HttpOnly, которую и отправляет на клиент ([6]). Клиент далее в [7] обращается к API ресурса, но делает это также не напрямую (поскольку он не владеет токеном доступа), а через наш Backend proxy. Здесь происходит аутентификация клиента и "размен" значения из cookie на легитимный access_token, с которым идет обращение к Resource server ([9]), а полученный ответ проксируется обратно на клиент ([12]).

Встает вопрос: какую cookie может выдавать наш Backend proxy? Здесь вижу несколько подходов.

  1. Обыкновенная сессионная cookie, которая идет в пару полученному токену. В таком случае в нашем компоненте нам придется реализовать работу с сессиями и хранить их. Однако так мы заодно получим способ управления инвалидацией доступа.

  2. Шифрование полученных от Authorization server значений. В таком случае мы избегаем необходимости хранить сессию у себя. При получении запроса на обращение к ресурсу мы производим расшифровку значения из cookie (ключ для этого у нас есть) и проксируем запрос далее.

Сроком действия данных cookie также необходимо управлять самостоятельно, также необходимо помнить про установку значений других их атрибутов. Отдельно отмечу, что такая реализация несет еще ряд тонкостей, таких как подбор времени жизни access_token, организация его кэширования в Backend proxy, механизм обновления access_token и т.д. Вследствие наличия и так большого объема информации, оставим эти вопросы за рамками данной статьи.

Подобный подход может быть применим и к SPA, поскольку, делая Single-page application, совсем не обязательно полностью отказываться от бэкенда, такая тонкая прослойка может быть вполне используема. В микросервисной архитектуре роль Backend proxy может выполнять API gateway или Backend-for-frontend (BFF).

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

Подход 3. Добавление пользовательского контекста в токен

До этого мы использовали подходы только с HttpOnly cookies, но что если есть и иной путь? Добавление пользовательского контекста в токен предполагает использование вместе как HttpOnly cookie, так и части, доступной на клиенте. Метод проще всего объясним на примере JWT-токена.

Пользовательский контекст может состоять из следующей информации:

  • Случайная строка, сгенерированная сервисом в процессе аутентификации. Передается на клиент в HttpOnly cookie (помним и про другие атрибуты).

  • SHA256-хэш от случайной строки, помещенный в payload JWT-токена.

Очень упрощенно это можно изобразить так:

Таким образом для авторизации, помимо проверки JWT-токена, необходимо еще и сравнение хэша от случайной строки, полученной в hardened-cookie, с хэшированным значением в самом токене. Токен при этом может быть сохранен как в сookies, так и в localStorage или sessionStorage - сути это не меняет, поскольку токен сам по себе становится недостаточным для доступа к Resource server.

Существует также еще интересная вариация, которую я тоже отнесу к данному подходу из-за схожести исполнения - Two Cookie JWT Approach. Здесь мы аналогично используем HttpOnly и не-HttpOnly cookie, получаемые с сервера, но принцип разделения информации несколько отличается.

В HttpOnly cookie здесь мы помещаем подпись JWT-токена, в то время как header и payload находятся в доступной на клиенте cookie. Тогда наше обращение к API будет выглядеть так:

При этом важно не забыть о грамотном выставлении атрибута Max-Age у cookies. Таким образом, авторизацию мы все еще выполняем на основе проверки JWT-токена, однако полная его "версия" может быть получена только путем совмещения двух частей, что снижает последствия эксплуатации XSS-уязвимости, поскольку подпись нам с клиента недоступна. При использовании одних только cookies здесь, как и ранее, тоже следует принять меры защиты от CSRF-атак.

Подход 4. Использование service worker

Service worker - скрипт, который браузер запускает в фоновом режиме, выполняющий роль прокси-сервера для взаимодействия между веб-приложением, браузером и сетью. Service worker запускается в отдельном контексте, работает в отдельном потоке, не имеет доступа к DOM, и соответственно клиент также не имеет доступа к service worker и хранимым там данным. Этой его особенностью мы и воспользуемся, чтобы обезопасить access token от утечки при XSS.

В этом случае service worker отвечает за получение токена от Authorization server и выполнение запросов к Request server. Запросы с клиента в данном случае проксируются service worker, он как бы перехватывает их. Следовательно вызов метода получения токена и сам токен полностью изолированы, поскольку контекст service worker недоступен для прочих JavaScript-контекстов.

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

У использования service worker существует еще одна важная деталь: его регистрация должна происходить в самом начале загрузки клиентской части приложения, в противном случае при эксплуатации XSS злоумышленник может инициировать новый flow аутентификации (до регистрации service worker) и получить токен в обход него.

В данном случае CSRF-атака неприменима, поскольку токен доступен только для service worker, а также исключается возможность кражи токена посредством XSS, однако реализация и эксплуатация такой схемы будут сложнее. Технология поддерживается современными браузерами, но, если требуется поддержка Internet Explorer, то такой подход не подойдет.

Подход 5. Хранение токена в памяти

JavaScript предоставляет возможность хранения полученного значения токена в памяти. Для этого используется имитация приватного свойства класса через "closure variable" - локальную переменную внутри замыкания. Тогда мы можем создать некий token-manager-class, который будет хранить значение токена и выполнять самостоятельно все обращения к Resource server, не допуская доступности токена снаружи.

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


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

Подчеркну, что мы говорили здесь только про кражу токена, в большинстве случая при наличии XSS злоумышленник все еще сможет отправить легитимные запросы к вашему API непосредственно из браузера жертвы, однако меры для предовтращения завладения токеном все равно важны.

Заключение

Тема безопасности аутентификации и авторизации достаточно обширна, и ее не охватить одной статьей. Здесь постарался рассмотреть вопрос хранения и работы с access token в клиентской части веб-приложений, какие риски он может нести и какие меры для их снижения существуют. Я намеренно использую слово "риски" - поскольку степень угрозы для каждого приложения может быть своя, но понимать ее важно.

Также для интересующихся привожу список статей по теме.

Список релевантного чтива

Помните о безопасности, проверяйте свои приложения и оценивайте риски.

Комментарии (36)


  1. savostin
    23.01.2023 21:31
    +15

    Посетив страницу workers в Chrome ужаснулся количеству. Удалил все, отключил создание новых. Теперь 10% сайтов просто не грузится. "Спасибо" за такие технологии. Имхо, если worker не критически необходим, не стоит забивать браузер клиентам. Разве что очень хочется отслеживать что-нибудь.

    Так и не понял как backend proxy что-то улучшает. Если злоумышленник может выполнить код на клиенте, то он с таким же успехом получит cookie и доступ через этот прокси. Он даже не заметит ничего.

    И, как я вижу, никакого решения против XSS нет - если уж кто-то внедрился в код фронта, то ничто уже не спасёт.


    1. anonymous
      00.00.0000 00:00

      НЛО прилетело и опубликовало эту надпись здесь


      1. savostin
        23.01.2023 22:49

        В статье рассматривается не защита от XSS, а защита от кражи сессии, или хотя бы минимизация ущерба, когда XSS уже произошел.


        1. anonymous
          00.00.0000 00:00

          НЛО прилетело и опубликовало эту надпись здесь


    1. funca
      23.01.2023 23:25

      Если злоумышленник может выполнить код на клиенте, то он с таким же успехом получит cookie

      HttpOnly кука не доступна для чтения из JavaScript на клиенте - злоумышленник не сможет увести её наружу через XSS, чтобы воспользоваться где-то в другом месте.


      1. savostin
        24.01.2023 00:11
        +4

        Я говорю о том, что backend proxy не вносит никакой разницы в эту схему. Не важно, что на клиенте хранится не токен доступа, а некий "токен доступа к токену доступа", если по нему безо всякого ограничения можно получить реальный токен доступа. С таким же успехом его, настоящий токен доступа, можно хранить на клиенте в httponly cookie.


        1. funca
          24.01.2023 00:37
          +5

          Покси нужен, чтобы положить токен (или его аналог) в куку - когда никто другой больше так не делает. Ведь бекенды, кидаясь друг в дружку запросами, обычно ожидают токен в заголовке Authorization: Bearer, - как настоятельно рекомендует делать OAuth2, - а не в куках.

          Когда начинающему фронтендеру прилетает задача достать данные из API голого бекенда, то первое что приходит на ум - надо где-то сохранять Authorization токен, чтобы не ходить за ним на каждый запрос. Ну и в добавок менеджер давит: а можешь сделать сам, без бекендера, а то он и так перегружен? В ответ предлагаются чудесные варианты решения на голом JS в браузере: с local/session storage, или самому установить себе куку. Работы на 5 митут. Собственно так и начинается содомия, от которой предостерегает автор в своей статье.


          1. savostin
            24.01.2023 00:54
            +6

            Логично. Сначала запретим API ставить cookie, а затем напишем middleware, который будет их ставить.

            Но смысл proxy теперь понятен...


            1. funca
              24.01.2023 01:12
              +1

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

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


    1. gmtd
      24.01.2023 19:22

      А чем сервис воркеры не угодили?

      Сильно улучшают UX при правильно использовании.


  1. Evengard
    24.01.2023 00:55
    +1

    Честно говоря, пришёл к гибридному решению. аccess_token сохраняется в локалсторедже, но действует он максимум час. Его увод сильно погоды не сделает. А вот refresh_token - в http-only куке на стороне api-сервера, который выступает как прокси к oauth-серверу и именно он обменивает auth_code (или рефреш токен) на свежие токены. На api сервере же заодно хранятся и client id/client secret для конкретного ресурса, и обращение к этому конкретному эндпоинту и выдаёт в ответ новый акцесс токен веб-приложению.


    1. Heggi
      24.01.2023 08:46

      У меня свой велосипед в сервисе аутентификации, но суть такая же, refresh-токен в httpOnly куке, с ограниченным path (только на продление токенов).
      Хотелось бы услышать минусы.


      1. anador Автор
        24.01.2023 08:56

        Использование refresh-токена - пример известного компромисса между безопасностью и удобством пользователей, поэтому и стоит вопрос подбора его времени жизни.

        Мне лично для их использования нравится совмещение ротации с защитой от переиспользования. Ротация refresh-токенов подразумевает, что при обращении с ним за получением access token мы получаем в ответе не только сам access token, а также и новый refresh token. При этом у нас получается некое "семейство" refresh-токенов, которые все идут от одного своего родителя.

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


        1. Heggi
          24.01.2023 14:34

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

          Сложно найти баланс между удобством, отказоустойчивостью и безопасностью.


  1. amakhrov
    24.01.2023 04:05
    +4

    Нет ни малейшего резона как-то прятать токен на стороне клиента, если при этом клиентский код (а значит, и XSS) все так же способен запросить этот токен заново.

    Если допустили XSS, то мы уже прилыли.

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


    1. anador Автор
      24.01.2023 09:00

      если при этом клиентский код (а значит, и XSS) все так же способен запросить этот токен заново

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

      можно привязать токен к айпи-адресу клиента или еще какому-то фингерпринту

      Подход с привязкой сессий к IP-адресу был распространен в 2010-х годах, однако сейчас кажется применимым в более ограниченной области использования. Наша мобильность повысилась, мы можем при работе с веб-приложением переключиться с мобильного интернета на Wi-Fi, например, включить или отключить VPN. В таком случае при привязке к IP мы ощутимо жертвуем удобством пользователей.


      1. amakhrov
        24.01.2023 09:15
        +1

        Что значит "вынесено за рамки клиента"? Мне казалось, вся статья (кроме пункта с прокси, который чит сам по себе :) ) про то, что клиент в явном виде получает и использует токен. Если не получает и не использует, то и проблемы нет никакой.


        1. anador Автор
          24.01.2023 09:20
          +1

          Да, как раз говорю про бэкенд прокси или сервис воркер. Ну и смотря, про что мы говорим, имея в виду "запросить токен заново", если про использование refresh-токена, то я выше в другой цепочке написал пример мер для повышения безопасности его использования.


          1. amakhrov
            24.01.2023 09:37

            Ага.

            Да, вариант с сервис воркером остроумный.

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


            1. amakhrov
              25.01.2023 20:20

              А не. Я еще нмного обдумал тему. Код в XSS мжет сделать то же самое, что делает сервис воркер при запросе refresh token. Получит новый рефреш токен, и по нему получит новый access token. Вывод: сервис воркер не нужен


      1. gmtd
        24.01.2023 19:15
        +2

        Можно привязать access token к IP, и при смене сети будет происходить просто обновление его через refresh token. Прозрачно для клиента.

        В то же время полностью защищаемся от кражи acess token-a (с другого IP он не будет работать сразу)


  1. ionicman
    24.01.2023 07:35
    +1

    Всегда было интересно, чем пара Access Token/Refresh Token безопаснее http-only куки, устанавливаемой на короткое время и продливаемой бэком? Точно также не даст CSRF, и точно также при угоне в течении ее жизни можно будет эксплойтить бэк. В чем принципиальная разница?


    1. vinnikov
      24.01.2023 08:53

      Для access и refresh проще отследить событие кражи токенов. Если Боб обманом узнаёт у Алисы ее refresh токен и идёт обменивать его на новый access токен, то сервер должен инвалидировать токены Алисы. Тогда при следующем запросе Алиса получит ответ, что ее access токен не валиден и также попытается обменять старый refresh токен на новую пару access и refresh. Так как и refresh токен Алисы уже был использован Бобом, то сервер поймёт, что произошла кража токена и может инвалидировать все access и refresh токены, которые были получены в результате запросов с изначальным refresh токеном Алисы, то есть вылогинить и Алису и Боба, после чего Алиса сможет повторно пройти аутентификацию и получить новую валидную пару токенов, а Боб нет.

      Я был уверен, что такое поведение является требованием стандарта, но как оказалось нет - OAuth2 даже не требует заменять refresh токен при перевыпуске access.


      1. anador Автор
        24.01.2023 09:06

        Я был уверен, что такое поведение является требованием стандарта, но как оказалось нет - OAuth2 даже не требует заменять refresh токен при перевыпуске access.

        Да, я согласен, что было бы здорово это добавить в стандарт, чтобы повысить безопасность ванильных имплементаций. На текущий момент это отражено в драфте OAuth 2.0 Security Best Current Practice, который пока еще не стал RFC.


      1. ionicman
        24.01.2023 09:51

        Есть клиент на нем access token, есть бэк, на нем refresh token, access token был уведен и в пределах его жизни Боб отлично работает с бэком, при его устаревании бэк продляет чей либо access token - Алисы или Боба, кто первый, после этого refresh token также перегенерируется и будет храниться на бэке - верно? Те узнать про компрометацию можно по невалидной паре at/rt и то, если она таки пройдет, а то если Алиса к бэку больше не обратится - то и детекта не будет.

        В случае с кукой - при ее генерации она хранится и на клиенте и на сервере, при продлении она пересохраняется и при продлении также сравнивается что также даёт информацию о компрметации.

        Или я что-то не так понял?


        1. amakhrov
          24.01.2023 20:51

          Не совсем. Угнанный access token дает Бобу возможность отправлять запросы на некий сторонний сервис (который выдал рефреш токен). Но по нему нельзя получить новый access token от нашего собственного сервера, который хранит refresh token. Для доступа к нашему бэку используем обычную сессионную куку.

          Другой вариант - refresh token в Http-Only cookie. В принципе, то же самое, только в профиль.


          1. ionicman
            25.01.2023 15:46

            Но по нему нельзя получить новый access token от нашего собственного сервера

            Почему? Алисе ведь он продляет этот at, в чем проблема у Боба сделать тоже самое имея этот-же at?


            1. amakhrov
              25.01.2023 20:22

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


              1. ionicman
                26.01.2023 01:17

                Это если мы говорим про обращение к thrd-party сервисам, и при этом at хранится на клиенте в js.

                А я говорил про авторизацию на своём собственном сервере с использованием ac/at. Получается что механизм этот ничем не лучше обычной сессии с кукой в httponly.

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


                1. amakhrov
                  26.01.2023 01:25

                  Ну дык. На своем (same site) сервере, конечно не нужны токены. Авторизуемся по HttpOnly куке. Необязательно короткоживущей (даже и вредно, пожалуй).

                  А токены используем для работы со сторонними серверами.


    1. amakhrov
      24.01.2023 09:12
      +1

      Токены и куки используются в разных сценариях. Токен для запросов к 3rd party доменам. Куки же first party, same site.


  1. igrishaev
    24.01.2023 09:50

    Что-то вы усложняете. Для авторизации на сайте используется кука с флагами httpOnly, secure, path и другими. В ее подписи участвуют IP и User Agent. Ну и базовая защита от CSRF.


    1. gmtd
      24.01.2023 19:49

      На клиенте нужно как-то знать, залогинен пользователь или нет


      1. igrishaev
        24.01.2023 22:01

        Не совсем понял вопрос. Сделайте апишку `GET /api/me`, которая вернет словарик с полями текущего пользователя по куке.


        1. gmtd
          24.01.2023 22:55

          И постоянно дёргать этот эндпойнт?


          1. TaksShine
            25.01.2023 12:48
            +1

            Когда нужно узнать, залогинен или нет, тогда и дергать.