Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL) | by Дмитрий ПереводIT | NOP::Nuances of Programming | Medium

Знакомство: что такое jwt?

В целях аутентификации JWT выступает в роли токена, выдаваемого сервером. Этот токен содержит относящееся к конкретному пользователю информационное наполнение в формате JSON. Клиенты могут использовать токен при взаимодействии с API (отправляя его как HTML-заголовок), чтобы API могли идентифицировать пользователя и выполнить соответствующее ему действие.

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

Хороший вопрос! Именно поэтому JWT также содержит подпись, которая создаётся сервером, выдавшим токен (предположим, конечной точкой вашей авторизации в системе). Любой другой сервер, получающий этот токен, может независимо проверить подпись, чтобы убедиться в подлинности информационного наполнения JSON и в том, что это наполнение было создано уполномоченным источником.

Но что если у меня есть действительный и подписанный JWT, а кто-то украдёт его из клиента? Смогут ли они постоянно использовать мой JWT?

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

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

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

Есть ли у JWT какая-то конкретная структура?

В сериализованном виде JWT выглядит примерно так:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

Если вы раскодируете этот base64, то получите JSON в виде 3 важных частей: заголовка, информационного наполнения и подписи.

3 части JWT

Сериализованная форма будет иметь следующий формат:

[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [signature ]

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

Вот упрощённая схема, демонстрирующая, как JWT выдаётся (/login), а затем используется для совершения вызова API к другому сервису ( /api):

Рабочий процесс выдачи и использования JWT

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

Это уже наболевшая тема интернет-форумов. Мы коротко и категорично отвечаем, что бэкенд-разработчикам нравится использовать JWT по следующим причинам:

a) микросервисы;

b) отсутствие необходимости в централизованной базе данных токенов.

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

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

Java-токены

Смарт-карта, а точнее её чип, имплантированный в пластик или встроенный в корпус USB-ключа является полноценным компьютером в миниатюре: с жёстким диском (EEPROM), оперативной памятью (ROM), процессором и, конечно, операционной системой. Функционалом, операционной системой и «установленными» на неё приложениями и определяются возможности токена.

Предыдущие поколения токенов, как правило, использовали проприетарную лицензируемую операционную систему (один из монополистов этого рынка – компания Siemens с её CardOS). Закрытая архитектура делала крайне сложной разработку дополнительных приложений и компонентов самой операционной системы, например, реализацию поддержки национальных криптографических алгоритмов.

Современные токены строятся на базе Java-карты, являющейся стандартом на рынке (более 10 крупных производителей). Функциональность конкретного токена определятся набором загруженных апплетов, выполняющихся на виртуальной Java-машине. Открытая платформа и широкая популярность языка программирования Java позволяет разрабатывать и в короткие сроки внедрять новые возможности.

Среди перспективных разработок – реализация мобильного электронного кошелька пользователя, контроль целостности критических данных средствами апплета, выполняющего в заведомо доверенной среде смарт-карты и т.п.
Важной особенностью современных Java-токенов является поддержка USB CCID Class Driver.

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

USB CCID Class Driver встроен в операционную систему Windows Vista и автоматически скачивается с сайта Windows Update при обнаружении нового подключенного устройства в Windows XP, 2003.
Описанные технологии позволяют использовать современные токены, в отличие от устройств предыдущего поколения на более широком парке компьютерной техники и для решения более широкого спектра задач.

Централизованное управление средствами аутентификации

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

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

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

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

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

Ручной учёт токенов, персонализация и правильное назначение их пользователям в зависимости от должностных обязанностей, а так же аудит использования и контроль правильности применения политик в масштабах крупной компании просто немыслим.
Описанные выше и многие другие задачи из соображений безопасности и в том числе  для уменьшения влияния человеческого фактора как правило автоматизируют с использованием специализированных систем класса  Token Management System (TMS).

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

Похожее:  МОЭСК: личный кабинет физического и юридического лица на официальном сайте

К базовым функциям TMS относятся: ввод в эксплуатацию токена (смарт-карты, USB-ключа, комбинированного USB-ключа или генератора одноразовых паролей), персонализация токена сотрудником, добавление возможности доступа к новым приложениям, а так же его отзыв, замена или временная выдача новой карты, разблокирование PIN-кода, обслуживание вышедшей из строя смарт-карты и отзыв её.

Итоги

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

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

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

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

Врезка – Пример внедрения

В апреле этого года Компания BCC, бизнес-партнер Aladdin Software Security R.D., завершила проект по модернизации информационной инфраструктуры Юридического факультета Санкт-Петербургского государственного университета. В рамках проекта, выполненного с применением продуктов и технологий корпорации Microsoft и средств аутентификации от Aladdin, факультет получил одну из наиболее совершенных информационных систем, функционирующих в российских государственных высших учебных заведениях.

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

Авторизация

Теперь откроем страницу авторизации с использованием CCT.

Для работы с CCT и выполнения автоматических операций обмена кода на токен библиотека AppAuth предоставляет сущность AuthorizationService. Эта сущность создается при входе на экран. При выходе с экрана она должна очиститься. В примере это делается внутри ViewModel экрана авторизации.

Создаем в init:

private val authService: AuthorizationService = AuthorizationService(getApplication())

Очищаем в onCleared:

authService.dispose()

Для открытия страницы авторизации в CCT нужен интент. Для этого получаем AuthorizationRequest на основе заполненных раньше данных в AuthConfig:

private val serviceConfiguration = AuthorizationServiceConfiguration(
   Uri.parse(AuthConfig.AUTH_URI),
   Uri.parse(AuthConfig.TOKEN_URI),
   null, // registration endpoint
   Uri.parse(AuthConfig.END_SESSION_URI)
)

fun getAuthRequest(): AuthorizationRequest {
   val redirectUri = AuthConfig.CALLBACK_URL.toUri()
   return AuthorizationRequest.Builder(
       serviceConfiguration,
       AuthConfig.CLIENT_ID,
       AuthConfig.RESPONSE_TYPE,
       redirectUri
   )
       .setScope(AuthConfig.SCOPE)
       .build()
}

Создаем интент:

// тут можно настроить вид chromeCustomTabs
val customTabsIntent = CustomTabsIntent.Builder().build()

val openAuthPageIntent = authService.getAuthorizationRequestIntent(
   getAuthRequest(),
   customTabsIntent
)

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

Поэтому используем ActivityResultContracts. Также можно использовать startActivityForResult.

private val getAuthResponse = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
   val dataIntent = it.data ?: return
   handleAuthResponseIntent(dataIntent)
}

getAuthResponse.launch(openAuthPageIntent)

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

Схема взаимодействия активити
Схема взаимодействия активити

Внутри openAuthPageIntent будет зашита вся информация, которую мы раньше указывали в AuthConfig, а также сгенерированный code_challenge.

Аутентификация по одноразовым паролям

Аутентификация по одноразовым паролям обычно применяется дополнительно к аутентификации по паролям для реализации
two-factor authentication
(2FA). В этой концепции пользователю необходимо предоставить данные двух типов для входа в систему: что-то, что он знает (например, пароль), и что-то, чем он владеет (например, устройство для генерации одноразовых паролей). Наличие двух факторов позволяет в значительной степени увеличить уровень безопасности, что м. б. востребовано для определенных видов веб-приложений.

Другой популярный сценарий использования одноразовых паролей — дополнительная аутентификация пользователя во время выполнения важных действий: перевод денег, изменение настроек и т. п.

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

  1. Аппаратные или программные токены, которые могут генерировать одноразовые пароли на основании секретного ключа, введенного в них, и текущего времени. Секретные ключи пользователей, являющиеся фактором владения, также хранятся на сервере, что позволяет выполнить проверку введенных одноразовых паролей. Пример аппаратной реализаций токенов — RSA SecurID; программной — приложение Google Authenticator.
  2. Случайно генерируемые коды, передаваемые пользователю через SMS или другой канал связи. В этой ситуации фактор владения — телефон пользователя (точнее — SIM-карта, привязанная к определенному номеру).
  3. Распечатка или scratch card со списком заранее сформированных одноразовых паролей. Для каждого нового входа в систему требуется ввести новый одноразовый пароль с указанным номером.

Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL) | by Дмитрий ПереводIT | NOP::Nuances of Programming | Medium
Аппаратный токен RSA SecurID генерирует новый код каждые 30 секунд.

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

Аутентификация по сертификатам

Сертификат представляет собой набор атрибутов, идентифицирующих владельца, подписанный
certificate authority
(CA). CA выступает в роли посредника, который гарантирует подлинность сертификатов (по аналогии с ФМС, выпускающей паспорта). Также сертификат криптографически связан с закрытым ключом, который хранится у владельца сертификата и позволяет однозначно подтвердить факт владения сертификатом.

На стороне клиента сертификат вместе с закрытым ключом могут храниться в операционной системе, в браузере, в файле, на отдельном физическом устройстве (smart card, USB token). Обычно закрытый ключ дополнительно защищен паролем или PIN-кодом.

В веб-приложениях традиционно используют сертификаты стандарта X.509. Аутентификация с помощью X.509-сертификата происходит в момент соединения с сервером и является частью протокола SSL/TLS. Этот механизм также хорошо поддерживается браузерами, которые позволяют пользователю выбрать и применить сертификат, если веб-сайт допускает такой способ аутентификации.

Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL) | by Дмитрий ПереводIT | NOP::Nuances of Programming | Medium
Использование сертификата для аутентификации.

Во время аутентификации сервер выполняет проверку сертификата на основании следующих правил:

  1. Сертификат должен быть подписан доверенным certification authority (проверка цепочки сертификатов).
  2. Сертификат должен быть действительным на текущую дату (проверка срока действия).
  3. Сертификат не должен быть отозван соответствующим CA (проверка списков исключения).

Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL) | by Дмитрий ПереводIT | NOP::Nuances of Programming | Medium
Пример X.509 сертификата.

После успешной аутентификации веб-приложение может выполнить авторизацию запроса на основании таких данных сертификата, как subject (имя владельца), issuer (эмитент), serial number (серийный номер сертификата) или thumbprint (отпечаток открытого ключа сертификата).

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

Выход из системы

При использовании JWT “logout” просто стирает токен на стороне клиента, после чего он уже не может быть использован в последующих вызовах API.

Значит вызова API /logout не существует?

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

Похожее:  «Мосэнергосбыт» обновил личный кабинет клиента и выпустил новое мобильное приложение

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

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

Дополнительно вы можете добавить процесс обработки чёрных списков. В данном случае можно создать вызов API /logout, и ваш сервер аутентификации будет помещать токены в “недействительный список”. Как бы то ни было, все API сервисы, использующие JWT, теперь должны добавить дополнительный шаг к их верификации, чтобы проверять централизованный “чёрный список”. Это снова вводит центральное состояние и возвращает нас к тому, что мы имели до использования JWT.

Разве наличие чёрного списка не исключает преимущество JWT, гласящее о необязательности центрального хранилища?

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

Что произойдёт, если я авторизован на нескольких вкладках?

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

window.addEventListener('storage', this.syncLogout) 

//....

syncLogout (event) {
if (event.key === 'logout') {
console.log('logged out from storage!')
Router.push('/login')
}
}

Теперь при выходе нам нужно совершить два действия:

  1. обнулить токен;
  2. установить в локальном хранилище элемент logout.

Дизайн rest api: как сейчас принято передавать авторизационный токен?

Дисклеймер: я тупой фронтэнд-жаваскриптер и делать это большей частью не мне. Но лучше спросить чем не спросить.

Сейчас мои коллеги переписывают авторизацию в одном веб-приложении.
Там restful json api, и предусматривается отдача часть api на использование внешним потребителям (доверенному списку а не всем подряд), то есть кросдоменность – нужна.
Теоретическая поддержка клиентами: браузеры, самый старый из которых IE11. Такой клиент давно есть и работает.
Мобильные устройства последних версий. Там ничего нет, только в планах и того кто мог бы что-то по этому поводу сказать тоже нет.

И возник у нас небольшой спор, как собственно передавать авторизационные токены?
Варианта собственно три, в порядке убывания встречаемости.
1). В url запроса по типу. /?auth_token=.
2). HTTP-заголовок Authorisation
3). Кастомный HTTP заголовок.

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

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

Собственно вопросы:
1. Где считается хорошей практикой передавать авторизационный токен в текущем 20огосподиуже18 году?
2. Какие есть преимущества у 1го варианта которых я не увидел? Правильно ли я понимаю что его держит инерция и соображения поддержки легаси в основном?
3. Есть ли какие-то грабли которые будут щекотать лоб при реализации второго варианта?

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

Отображение серверной части (ssr)

В отображении серверной стороны при работе с JWT-токенами есть дополнительные трудности.

Вот что нам нужно:

  1. Браузер делает запрос к URL приложения.
  2. SSR-сервер отображает страницу на основе идентичности пользователя.
  3. Пользователь получает отображённую страницу, а затем продолжает использовать приложение в качестве SPA (одностраничного приложения).

Откуда SSR сервер узнаёт, авторизован ли пользователь?

Браузер должен отправить SSR-серверу информацию о текущей идентичности пользователя. Единственный способ сделать это — использовать куки.

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

Внимание! Для SSR на аутентифицированных страницах очень важно, чтобы домен API аутентификации (и, следовательно, домен куки refresh_token) совпадал с доменом SSR-сервера. В противном случае наши куки не смогут быть отправлены SSR-серверу.

Вот что делает SSR-сервер:

  1. При получении запроса на отображение конкретной страницы он захватывает куки refresh_token.
  2. Далее сервер использует эту куки, чтобы получить новый JWT для пользователя.
  3. Затем сервер использует этот новый токен и совершает все аутентифицированные запросы GraphQL для извлечения правильных данных.

Может ли пользователь продолжить выполнение аутентифицированных API-запросов, когда SSR-страница будет загружена?

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

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

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

Весь поток SSR от начала до конца:

Постоянные сессии

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

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

Приложения обычно спрашивают пользователей, хотят ли те “stay logged in” (оставаться авторизованными) в течение нескольких сессий или сохраняют их авторизованными по умолчанию. Именно это хотим реализовать и мы.

На данный момент мы не можем этого сделать, т.к. JWT находится в памяти и не сохраняется. Ранее мы уже рассматривали, почему не можем хранить JWT непосредственно в куки или локальном хранилище.

Как же можно безопасно сохранять сессии?

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

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

  1. Если мы видим, что в памяти у нас нет JWT, тогда мы запускаем процесс фонового обновления.
  2. Если токен обновления по-прежнему действителен (или не был пересоздан), тогда мы получаем новый JWT и можем продолжать.
Похожее:  Классификация механизмов аутентификации пользователей и их обзор / Хабр

Возможный случай ошибки:

В случае если наш срок действия токена обновления истечёт (предположим, пользователь вернётся в приложение через большой промежуток времени) или будет пересоздан (к примеру, из-за принудительного выхода), тогда клиент получит ошибку 401 для неавторизованного refresh_token.

Ещё в одном случае у нас может просто изначально не быть refresh_token, что также приведёт к ошибке, полученной от конечной точки /refresh_token, в результате которой пользователь будет перенаправлен на экран авторизации.

Вот пример кода, показывающий, как мы можем обработать эти ошибки при помощи logoutLink:

const logoutLink = onError(({ networkError }) => {
if (networkError.statusCode === 401) logout();
})

Стандарты oauth и openid connect

В отличие от SAML и WS-Federation, стандарт OAuth (Open Authorization) не описывает протокол аутентификации пользователя. Вместо этого он определяет механизм получения доступа одного приложения к другому от имени пользователя. Однако существуют схемы, позволяющие осуществить аутентификацию пользователя на базе этого стандарта (об этом — ниже).

Первая версия стандарта разрабатывалась в 2007 – 2022 гг., а текущая версия 2.0 опубликована в 2022 г. Версия 2.0 значительно расширяет и в то же время упрощает стандарт, но обратно несовместима с версией 1.0. Сейчас OAuth 2.0 очень популярен и используется повсеместно для предоставления делегированного доступа и третье-сторонней аутентификации пользователей.

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

> Попросить пользователя указать данные своей учетной записи? — плохой вариант.> Попросить пользователя создать ключ доступа? — возможно, но весьма сложно.

Как раз эту проблему и позволяет решить стандарт OAuth: он описывает, как приложение путешествий (client) может получить доступ к почте пользователя (resource server) с разрешения пользователя (resource owner). В общем виде весь процесс состоит из нескольких шагов:

  1. Пользователь (resource owner) дает разрешение приложению (client) на доступ к определенному ресурсу в виде гранта. Что такое грант, рассмотрим чуть ниже.
  2. Приложение обращается к серверу авторизации и получает токен доступа к ресурсу в обмен на свой грант. В нашем примере сервер авторизации — Google. При вызове приложение дополнительно аутентифицируется при помощи ключа доступа, выданным ему при предварительной регистрации.
  3. Приложение использует этот токен для получения требуемых данных от сервера ресурсов (в нашем случае — сервис Gmail).

Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL) | by Дмитрий ПереводIT | NOP::Nuances of Programming | Medium
Взаимодействие компонентов в стандарте OAuth.

Стандарт описывает четыре вида грантов, которые определяют возможные сценарии применения:

Фоновое обновление

Существует две главных проблемы, с которыми могут столкнуться пользователи нашего JWT-приложения:

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

Для решения этих проблем большинство JWT-провайдеров предоставляют токен обновления, который имеет два свойства:

  1. Он может использоваться для совершения вызова API (например, /refresh_token) с запросом нового JWT-токена до истечения срока действия текущего.
  2. Он может быть безопасно сохранён на клиенте в течение нескольких сессий.

Как работает обновляемый токен?

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

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

Как осуществляется безопасное хранение токена обновления на клиенте?

Явное аннулирование токена пользователем

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

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

В черном списке будет храниться сборник (в кодировке SHA-256 в HEX) токена с датой аннулирования, которая должна превышать срок действия выданного токена.

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

Пример реализации:

Хранилище черного списка:Для централизованного хранения черного списка будет использоваться база данных со следующей структурой:

create table if not exists revoked_token(jwt_token_digest varchar(255) primary key,
revokation_date timestamp default now());

Управление аннулированиями токенов:

// Контролирование отката токена (logout).
// Используйте БД, чтобы разрешить нескольким экземплярам проверять
// отозванный токен и разрешить очистку на уровне централизованной БД.
public class TokenRevoker {

 // Подключение к БД
 @Resource("jdbc/storeDS")
 private DataSource storeDS;

 // Проверка является ли токен отозванным
 public boolean isTokenRevoked(String jwtInHex) throws Exception {
     boolean tokenIsPresent = false;
     if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
         // Декодирование токена
         byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);

         // Вычисление SHA256 от токена
         MessageDigest digest = MessageDigest.getInstance("SHA-256");
         byte[] cipheredTokenDigest = digest.digest(cipheredToken);
         String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);

         // Поиск токена в БД
         try (Connection con = this.storeDS.getConnection()) {
             String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?";
             try (PreparedStatement pStatement = con.prepareStatement(query)) {
                 pStatement.setString(1, jwtTokenDigestInHex);
                 try (ResultSet rSet = pStatement.executeQuery()) {
                     tokenIsPresent = rSet.next();
                 }
             }
         }
     }

     return tokenIsPresent;
 }

// Добавление закодированного в HEX токена в таблица отозванных токенов
public void revokeToken(String jwtInHex) throws Exception {
     if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
         // Декодирование токена
         byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);

         // Вычисление SHA256 от токена
         MessageDigest digest = MessageDigest.getInstance("SHA-256");
         byte[] cipheredTokenDigest = digest.digest(cipheredToken);
         String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);

         // Проверка на наличие токена уже в БД и занесение в БД в
	   // обратном случае
         if (!this.isTokenRevoked(jwtInHex)) {
             try (Connection con = this.storeDS.getConnection()) {
                 String query = "insert into revoked_token(jwt_token_digest) values(?)";
                 int insertedRecordCount;
                 try (PreparedStatement pStatement = con.prepareStatement(query)) {
                     pStatement.setString(1, jwtTokenDigestInHex);
                     insertedRecordCount = pStatement.executeUpdate();
                 }
                 if (insertedRecordCount != 1) {
                     throw new IllegalStateException("Number of inserted record is invalid,"   " 1 expected but is "   insertedRecordCount);
                 }
             }
         }

     }
 }

Заключение первой части

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

Stay tuned.

Спойлер второй части

Минимальная реализация интеграция Identity Server в ваше приложение выглядит так:

public void Configuration(IAppBuilder app)
{
    var factory = new IdentityServerServiceFactory();
    factory.UseInMemoryClients(Clients.Get())
           .UseInMemoryScopes(Scopes.Get())
           .UseInMemoryUsers(Users.Get());

    var options = new IdentityServerOptions
    {
        SiteName = Constants.IdentityServerName,
        SigningCertificate = Certificate.Get(),
        Factory = factory,
    };

    app.UseIdentityServer(options);
}

Минимальная реализация интеграции веб-клиента с Identity Server:

public void Configuration(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = "Cookies"
    });

    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        ClientId = Constants.ClientName,
        Authority = Constants.IdentityServerAddress,
        RedirectUri = Constants.ClientReturnUrl,
        ResponseType = "id_token",
        Scope = "openid email",
        SignInAsAuthenticationType = "Cookies",
    });
}

Минимальная реализация интеграции веб-API с Identity Server:

public void Configuration(IAppBuilder app)
{
    app.UseIdentityServerBearerTokenAuthentication(
        new IdentityServerBearerTokenAuthenticationOptions
        {
            Authority = Constants.IdentityServerAddress,
            RequiredScopes = new[] { "write" },
            ValidationMode = ValidationMode.Local,

            // credentials for the introspection endpoint
            ClientId = "write",
            ClientSecret = "secret"
        });

    app.UseWebApi(WebApiConfig.Register());
}

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (Пока оценок нет)
Загрузка...

Добавить комментарий

Ваш адрес email не будет опубликован.

Adblock
detector