Русский
Русский
English
Статистика
Реклама

Keycloak

Интеграция Netsparker с AD через Keycloak

16.12.2020 14:07:22 | Автор: admin

Привет, Хабр!

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

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

Netsparker это коммерческий динамический сканер веб-приложений, который позволяет обнаруживать большое количество уязвимостей в реальном времени и при этом располагает широким набором средств для интеграции с различными инструментами разработки и сборки. Это позволяет осуществлять поиск уязвимостей еще до того, как приложение выходит в производственную среду.
Подробнее: https://www.netsparker.com/

Keycloak решение с открытым кодом, которое способно выступать промежуточным звеном между приложением и сторонним аутентификационным сервисом. Это часто необходимо в случаях, если требования к аутентификации превосходят возможности приложения или имеющаяся инфраструктура позволяет сделать бесшовную аутентификацию между всеми используемыми сервисами (SSO).
Keycloak позволяет интегрировать ваши приложения с внешними системами аутентификации пользователей, может выступать в качестве посредника для использования social login сервисов и многое другое.
Подробнее: https://www.keycloak.org/

Проблема

По умолчанию в Netsparker есть несколько вариантов внедрения Single Sign-On: с Google Cloud, PingIdentity, Okta, Azure AD, ADFS, а также базовая интеграция при помощи SAML2.0.

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

Проблема: Netsparker не работает напрямую с ADПроблема: Netsparker не работает напрямую с AD

Дальше я расскажу, как мы подключали чистый AD к Netsparker при помощи промежуточного инструмента Keycloak. Ниже будет представлен самый базовый вариант, так как в каждой организации процесс будет отличаться.

Общая схема выглядит так:

Схема интеграции Netsparker с AD через KeycloakСхема интеграции Netsparker с AD через Keycloak

Существует как минимум два режима работы. В первом режиме база пользователей AD синхронизируется с базой пользователей Keycloak, и система аутентификации Keycloak используется для разрешения доступа пользователям в Netsparker. Во втором режиме Keycloak берет на себя непосредственно аутентификацию пользователя в AD без сохранения данных пользователей в своей базе. При этом, если необходимо, можно вести базу пользователей (например которых нет в AD) дополнительно и в Keycloak.

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

Приступим.

Экспорт конфигурации Netsparker SSO

Первый шаг. Чтобы включить Single Sign-On в Netsparker необходимо сделать следующее:

  1. Войти в Netsparker под административным пользователем и открыть Settings Single Sign-On.

  2. Поставить флажок Enable для включения поддержки SSO.
    (!) Опция Enforce to authenticate ... нужна для того, чтобы запретить пользователям без прав администратора вход напрямую в Netsparker с использованием пароля.
    Остальные поля мы заполним чуть позже данными из Keycloak.

    Экран настройки Netsparker SSOЭкран настройки Netsparker SSO


    После включения функции SSO на странице входа в Netsparker появится дополнительная ссылка для доступа через внешний сервис.
    Вот так это будет выглядеть для неаутентифицированного пользователя:

    Экран входа в Netsparker для неаутентифицированного пользователяЭкран входа в Netsparker для неаутентифицированного пользователя
  3. Справа на закладке SAML скачиваем базовый XML с метаданными Netsparker.
    Этот XML содержит настройки SAML-клиента (в нашем случае им является Netsparker), которые понадобятся для конфигурирования Keycloak, например Client Identifier, SAML Service URL.

    Сохранение SAML мета-данных NetsparkerСохранение SAML мета-данных Netsparker

На данном этапе с Netsparker'ом всё. Переходим в Keycloak.

Конфигурация Keycloak

  1. Заходим в панель администратора Keycloak и создаем новый Realm с названием, например, Netsparker.
    Realm это раздел, в котором будет храниться вся конфигурация, касающаяся конкретно нашего приложения (Netsparker), включая настройки синхронизации с AD, собственная база пользователей и т. п.

    Экран добавления Realm'аЭкран добавления Realm'а
  2. Теперь необходимо в этом Realm прописать нашего клиента (Netsparker), который будет пользоваться услугами Keycloak.
    Переходим на закладку Clients, нажимаем Create, импортируем XML, который сохранили из Netsparker.

    Экран добавления клиента KeycloakЭкран добавления клиента Keycloak

    Данные формы подгрузятся из файла, поэтому нажимаем Save.

  3. В открывшемся экране настроек клиента сразу сохраняем себе в блокнот данные сертификата (закладка SAML Keys).

    Данные сертификата клиента KeycloakДанные сертификата клиента Keycloak
  4. Возвращаемся на закладку Settings и вносим следующие изменения:

    Client Signature Required ставим в положение OFF, т. к. Netsparker пока не передает данные о своем алгоритме подписи, и поэтому SAML-запрос получается некорректным.
    Valid Redirect URIs модифицируем URL и заменяем часть строки адреса ?spid на *. Это поле задает шаблон для адресов, на которые Keycloak разрешено осуществлять перенаправление браузера. Это сделано для того, чтобы злоумышленник не мог подставить адрес подконтрольного ему сервиса.

    Настройки клиента KeycloakНастройки клиента Keycloak


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

  5. Нажимаем Save и возвращаемся в Netsparker.

Конфигурация Netsparker

Теперь на стороне сканера добавляем данные о провайдере аутентификационных данных (сокращенно IdP), в нашем случае это Keycloak.

  1. Донастроим то, что мы начали настраивать на первых шагах.
    Снова открываем страницу Netsparker Settings Single Sign-On и прописываем идентификатор IdP и URL для SAML-запросов.

    У Keycloak эти значения стандартные:
    IdP Identifier:
    https://<домен-где-установлен-Keycloak>/auth/realm/<наш-Realm>
    SAML Endpoint:
    https://<домен-где-установлен-Keycloak>>/auth/realms/<наш-Realm>/protocol/saml
    X.509 Certificate:
    Сертификат в base64, который мы сохранили из Keycloak.

    Настройка SSO в NetsparkerНастройка SSO в Netsparker
  2. Сохраняем конфигурацию, и на данный момент у нас уже есть базовая возможность пользоваться аутентификацией Keycloak.

    При нажатии на Sign in via your Identity Provider открывается промежуточная форма ввода email пользователя из Netsparker, из которой уже осуществляется переход на форму входа Keycloak. Пользовательские данные, вводимые на этой форме либо проверяются в базе пользователей Keycloak, либо прокидываются дальше на AD (мы посмотрим оба варианта).

    После успешного входа в Keycloak нас должно перенаправить обратно в Netsparker уже в аутентифицированном состоянии.

    Процесс входа в NetsparkerПроцесс входа в Netsparker


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

Создание пользователей вручную

Netsparker

Для начала заведем тестового пользователя в Netsparker для проверки интеграции.

  1. В Netsparker открываем Team New Team Member, заполняем поля Name, Email и проставляем какие-нибудь разрешения (те, что на снимке, выбраны просто для примера):

    Экран создания пользователей в NetsparkerЭкран создания пользователей в Netsparker
  2. Активируем пользователя, кликнув на код подтверждения на странице Invitations.

    Список "приглашенных" пользователейСписок "приглашенных" пользователей
  3. На открывшейся странице вводим пароль для созданного пользователя и нажимаем Create an account.
    Этот пароль будет использоваться, только если пользователь решит войти в Netsparker не через SSO, а через обычную форму входа:

    Экран регистрации нового пользователя NetsparkerЭкран регистрации нового пользователя Netsparker

Keycloak

На данный момент войти в Netsparker через SSO не получится, т. к. в базе Keycloak в нашем Realm еще нет ни одного пользователя и Keycloak не сможет его аутентифицировать.

Исправим это:

  1. Заходим в Keycloak Manage Users, нажимаем Add user, вводим данные пользователя и сохраняем:

    Экран добавления пользователя в KeycloakЭкран добавления пользователя в Keycloak

    Здесь важно указать Username в виде email-адреса и поставить Email Verified в положение On, чтобы можно было бесшовно перейти от Keycloak к Netsparker.

  2. После сохранения появляется страница деталей пользователя. На ней в закладке Credentials необходимо установить пароль этому пользователю. Он при необходимости может отличаться от пароля в Netsparker:

    Создание пароля для пользователя KeycloakСоздание пароля для пользователя Keycloak

    Ставим Temporary в положение Off и устанавливаем пароль.

Промежуточная проверка интеграции

Теперь можем убедиться, что наша интеграция Netsparker+Keycloak работает:

  1. Открываем страницу входа в Netsparker и выбираем Sign in via your Identity Provider.

  2. Вводим почтовый адрес нашего тестового пользователя и попадаем на страницу входа в Keycloak.

  3. Указываем аутентификационные данные созданного пользователя.

    Экран входа в KeycloakЭкран входа в Keycloak
  4. Если всё прошло успешно, Keycloak перенаправит нас на главную страницу Netsparker под нашим тестовым пользователем.

    Перенаправление из Keycloak в NetsparkerПеренаправление из Keycloak в NetsparkerОсновной экран сканера NetsparkerОсновной экран сканера Netsparker

Промежуточный итог
Итак, на данный момент у нас есть возможность входить в Netsparker, вводя данные пользователя в Keycloak. Следующий шаг и реализация начальной цели связать аутентификацию Netsparker c аутентификацией на AD. При необходимости мы также можем синхронизировать базу данных пользователей Keycloak с базой Active Directory.

Следующий этап настройки: Keycloak + ADСледующий этап настройки: Keycloak + AD

Дорога в AD

Теперь сделаем следующий шаг и настроим аутентификацию пользователей Active Directory через Keycloak по LDAP. Для этого нам потребуется функция User Federation.

  1. Открываем Keycloak User Federation и в списке провайдеров выбираем ldap.

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

    В окне Add user federation provider заполняем поля:
    Vendor: Active Directory.
    Connection URL: путь к вашему серверу.
    Например ldap://172.16.2.23:389.
    Users DN: путь навигации к записям пользователя.
    Например CN=Users,DC=cx,DC=local.
    Bind DN: путь к записи администратора AD.
    Например cn=cxadmin,cn=users,dc=cx,dc=local.
    Bind Credential: пароль учетной записи администратора.
    Например ваш валидный пароль :)

    (!) Если мы хотим, чтобы данные из AD сперва синхронизировались в локальную базу Keycloak, ставим переключатель Import User в положение On.

    Опция импортирования пользователейОпция импортирования пользователей

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

  3. Дальше необходима небольшая манипуляция с данными из AD. Так как Netsparker требует в качестве имени пользователя его email-адрес (помните ремарку в начале?), нам надо убедиться, что во-первых из AD придут только пользователи с почтовым адресом и, во-вторых, что имя пользователя будет содержать этот адрес, а не иной параметр.

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

    Custom User LDAP Filter: (&(objectCategory=person)(mail=*)(objectClass=user)).
    Username LDAP attribute: mail.

    Сохраняем форму, и в итоге наш экран настройки должен выглядеть так:

    Сводная страница настроек LDAP провайдераСводная страница настроек LDAP провайдера
  4. Теперь сделаем так, чтобы при синхронизации пользователей AD они автоматически получали email в качестве имени пользователя. Для этого открываем закладку Mappers:

    Настройка mapper'овНастройка mapper'ов

    И в mapperе Username заменяем атрибут cn на mail.

    Настройка отображения email адреса на имя пользователяНастройка отображения email адреса на имя пользователя
  5. Сохраняем настройки.

(!) Если мы настраивали синхронизацию базы Keycloak с AD, мы можем проверить эту настройку, нажав Synchronize all users. Если всё прошло успешно, то на вкладке Users у нас появятся наши пользователи из AD'a:)

Проверяем, что всё работает

Можем убедиться, что наша настройка верна, войдя под учётной записью из AD в Netsparker.

  1. Проверяем, что в Netsparker есть пользователь с таким почтовым адресом (Manage Team). Какой у него пароль неважно, так как аутентификация будет происходить на стороне AD.

    Искомый пользователь NetsparkerИскомый пользователь Netsparker
  2. Открываем страницу Netsparker SSO и вводим email-адрес пользователя AD.

    Промежуточный экран входа в Netsparker SSOПромежуточный экран входа в Netsparker SSO
  3. На странице входа Keycloak вводим данные пользователя AD с паролем.

    Экран входа в KeycloakЭкран входа в Keycloak
  4. Нам должна открыться страница Netsparker для этого пользователя.

    Основной экран Netsparker для аутентифицированного AD-пользователя AlexОсновной экран Netsparker для аутентифицированного AD-пользователя Alex

Настройка автоматического создания пользователей

Вручную заводить новых пользователей зачастую неудобно, поэтому в Netsparker есть возможность упростить заведение новых учётных записей, которая называется auto provisioning. С ее помощью устраняется необходимость вручную добавлять нового пользователя: если его нет в базе данных Netsparker, то такой пользователь будет создан автоматически при попытке входа с верными данными своей учётной записи AD.

Дальше нам будет необходимо сделать небольшие изменения в конфигурациях Keycloak и Netsparker. Так как auto provisioning требует, чтобы сессия инициировалась Identity Provider'ом, давайте его и настроим:

Keycloak

  1. Открываем настройки нашего клиента Keycloak Clients наш клиент.

  2. Нам потребуется внести изменения в три поля:
    Valid Redirect URIs (опционально): можно оставить как было, а можно ограничить перенаправление конкретным клиентом (который задаётся параметром spid). Для этого копируем из настроек SSO Netsparke'а полный URL SAML 2.0 Service URL.
    IDP Initiated SSO URL: необходимо задать URL, который мы хотим использовать для нашего конкретного клиента (Netsparker), чтобы явно указать, что должно использоваться SSO, вызываемое со стороны IdP. Например netsparker. После этого пользователи смогут входить конкретно в Netsparker, используя URL из подсказки.
    Assertion Consumer Service POST Binding URL: URL, куда Keycloak будет отправлять SAML-данные для входа в Netsparker. Указываем здесь SAML 2.0 Service URL из настроек Netsparker'a.

    Дополнительная настройка клиента KeycloakДополнительная настройка клиента Keycloak
  3. Сохраняем настройки.

Настройка mappers

Чтобы Keycloak формировал SAML-запрос в виде, необходимом Netsparker'у, необходимо добавить в него несколько обязательных полей (FirstName и LastName). В этом нам поможет mapper, который автоматически назначит полям SAML соответствующие поля из пользовательской записи.

Для этого открываем Keycloak Clients Mappers и создаем mapper, нажав на Create.

Дополнительная настройка mapper'овДополнительная настройка mapper'ов

Содержимое полей mapper задаем как на картинке ниже:

Настройка SAML-mapper'а для имени пользователяНастройка SAML-mapper'а для имени пользователя

Аналогично создаём mapper для фамилии:

Настройка SAML-mapper'а для фамилии пользователяНастройка SAML-mapper'а для фамилии пользователя

Итоговый список должен выглядеть так:

Список mapper'овСписок mapper'ов

Netsparker

Последнее, что нам нужно сделать донастроить Netsparker, чтобы он создавал пользователей и принимал запросы на это действие от Keycloak.

  1. Открываем Netsparker Settings Single Sign-On.

  2. В поле SAML 2.0 Endpoint указываем URL из Keycloak из подсказки поля IDP Initiated SSO URL.

  3. Выбираем опцию Enable Auto Provisioning и сохраняем настройки.

    Настройка автоматического создания пользователей в NetsparkerНастройка автоматического создания пользователей в Netsparker

Финальная проверка

Теперь проверим как это всё работает.

  1. Прежде всего теперь на странице Netsparker Team можем удалить пользователя AD, которого мы создавали вручную.

    Удаление пользователяУдаление пользователя
  2. В Keycloak завершаем все сессии, чтобы они нам не мешали: Keycloak Manage Sessions Logout all.

  3. Открываем страницу Keycloak для SSO (напомню, Target IDP initiated SSO URL это ссылка вида:
    https://yourdomain.com/auth/realms/netsparker/protocol/saml/clients/netsparker).

  4. Вводим данные пользователя из AD.

  5. Если всё прошло успешно, в Netsparker будет создан соответствующий пользователь с минимальными правами и для него будет установлена новая сессия.

    Основной экран Netsparker для пользователя, автоматически созданного при входе через ADОсновной экран Netsparker для пользователя, автоматически созданного при входе через AD

Что дальше?

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

Если была выбрана опция auto provisioning, необходимо от имени административного пользователя выдать необходимые разрешения созданному пользователю, чтобы тот мог работать с Netsparker.

Для простоты лучше первый раз для создания пользователя входить через Target IDP initiated SSO URL и уже впоследствии аутентифицироваться либо по этому же URL, либо осуществлять вход через привычную страницу Netsparker Login.

Еще один нюанс в данный момент выход пользователя из Netsparker не приводит к выходу пользователя из Keycloak. Поэтому, когда в следующий раз мы решим войти при помощи SSO, нас сразу перебросит в Netsparker под последним аутентифицированным пользователем. Это можно решить, например, уменьшив длительность сессии пользователя в Keycloak. Тогда его SSO-сессия будет автоматически уничтожена после указанного промежутка времени.
Настройка осуществляется в Realms Netsparker Tokens:

Настройка длительности SSO сессииНастройка длительности SSO сессии

Также пользователь может вручную закончить свою сессию на собственной странице учётной записи Keycloak по ссылке вида: https://keycloak.yourcompany.com/auth/realms/netsparker/account
После этого для входа в Netsparker потребуется снова аутентифицироваться в Keycloak и можно будет осуществить вход под другим пользователем.

Заключение

Мы рассмотрели в деталях, как можно аутентифицировать пользователей Netsparker через Active Directory с использованием промежуточного решения Keycloak. В принципе, такой же процесс может быть применён и для других приложений, которые не работают с Active Directory напрямую, но поддерживают SAML для описания деталей аутентификации.
Keycloak позволяет достаточно гибко настраивать и дополнять систему аутентификации, комплексно закрывая этот аспект безопасности для приложений, используемых в организации.

Подробнее..

Keycloak интеграция со Spring Boot и Vue.js для самых маленьких

10.05.2021 18:20:09 | Автор: admin

Вы больше не можете создать сервер авторизации с помощью @EnableAuthorizationServer, потому что Spring Security OAuth задеприкейтили, а проект Spring Authorization Serverвсё ещё экспериментальный? Выход есть! Напишем авторизацию своими руками... Что?.. Нет?.. Не хочется? И вообще получаются какие-то костыли и велосипеды? Ну ладно, тогда давайте возьмём уже что-то готовое. Например, Keycloak.

Что, зачем и почему?

Как-то сидя на карантине захотелось мне написать pet-проект, да не простой, а с использованием микросервисной архитектуры (ну или около того). На начальном этапе одного сервиса для фронта и одного для бэка, в принципе, будет достаточно. Если вдруг в будущем понадобятся ещё сервисы, то будем добавлять их по мере необходимости. Для бэка будем использовать Spring Boot. Для фронта - Vue.js, а точнее Vuetify, чтобы не писать свои компоненты, а использовать уже готовые.

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

Для авторизации пусть будет отдельный сервис. И раз уж мы решили использовать Spring Boot, то сможет ли он нам чем-то помочь в создании этого сервиса? Например, каким-нибудь готовым решением, таким как Authorization Server? Правильно, не сможет. Проект Spring Security OAuth в котором находился Authorization Server задеприкейтили, а сам проект Authorization Server стал эксперементальным и на данный момент находится в активной разработке. Что делать? Как быть? Можно написать свой сервис авторизации. Если подсматривать в исходники задеприкейченого Authorization Server, то, вероятно, задача будет не такой уж и страшной. Правда, при этом возможны ситуации когда реализацию каких-то интересных фич будет негде подсмотреть и решать вопросы о том "быть или не быть", "правильно ли так делать или чё-то фигня какая-то" придётся исходя из собственного опыта, что может привести к получению на выходе большого количества неприглядных костылей.

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

Keycloak

Keycloak представляет из себя сервис, который предназначен для идентификации и контроля доступа. Что он умеет:

  • SSO (Single-Sign On) - это когда вы логинитесь в одном едином месте входа, получаете идентификатор (например, токен), с которым можете получить доступ к различным вашим сервисам

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

  • Темы - можно кастомизировать страницы для Login Flows

  • Social Login - можно логиниться через различные социальные сети

  • и много чего ещё

И всё это он умеет практически из коробки, достаточно просто настроить требуемое поведение из админки (Admin Console), которая у Keycloak тоже имеется. А если вам всего этого вдруг окажется мало, то Keycloak является open sourceпродуктом, который распространяется по лицензии Apache License 2.0. Так что можно взять исходники Keycloak и дописать требуемый функционал, если он вам, конечно, настолько сильно нужен.

А ещё у Keycloak имеются достаточно удобные интеграции со Spring Boot и Vue.js, что значительно упрощает разработку связанную с взаимодействием с ним.

Getting Started with Keycloak

Запускать локально сторонние сервисы, требуемые для разработки своих собственных, лично я предпочитаю с помощью Docker Compose, т.к. наглядно и достаточно удобно в yml-файле описывать как и с какими параметрами требуется осуществлять запуск. А посему, Keycloak локально будем запускать с помощью Docker Compose.

В качестве докер-образа возьмём jboss/keycloak. Чтобы иметь возможность обращаться к Keycloak прокинем из контейнера порт 8080. Так же, чтобы иметь возможность заходить в админку Keycloak, требуется установить логин и пароль от админской учётки. Сделать это можно установив переменные окружения KEYCLOAK_USER для логина и KEYCLOAK_PASSWORD для пароля. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin    ports:      - 8080:8080

Создание своих realm и client

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

По умолчанию уже создан один realm и называется он master. В нём будет находится админская учётка логин и пароль от которой мы задали при запуске Keycloak с помощью Docker Compose. Данный realm предназначен для администрирования Keycloak и он не должен использоваться для ваших собственных приложений. Для своих приложений нужно создать свой realm.

Для начала нам нужно залогиниться в админке Keycloak, запустить который можно с помощью файла Docker Compose, описанного ранее. Для этого можно перейти по адресу http://localhost:8080/auth/ и выбрать Administration Console.

После этого мы попадаем на страницу авторизации админки Keycloak. Здесь можно ввести логин и пароль от админской учётки для входа в Keycloak.

После входа откроется страница настроек realm master.

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

На странице создания realm достаточно заполнить только поле Name.

После нажатия на кнопку Createмы попадём на страницу редактирования этого realm. Но пока дополнительно в нашем realm ничего менять не будем.

Теперь перейдём в раздел Clients. Как можно заметить, по умолчанию уже создано несколько технических клиентов, предназначенных для возможности администрирования через Keycloak, например, для того чтобы пользователи могли менять свои данные или чтобы можно было настраивать realm'ы с помощью REST API и много для чего ещё. Подробнее про этих клиентов можно почитать тут.

Давайте создадим своего клиента. Для этого в разделе Clientsнеобходимо нажать на кнопку Create.

На странице создания клиента необходимо заполнить поля:

  • Client ID - идентификатор клиента, будет использоваться в различных запросах к Keycloak для идентификации приложения.

  • Root URL - адрес клиентского приложения.

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

Интеграция со Spring Boot

В первую очередь давайте создадим проект на Spring Boot. Сделать это можно, например, с помощью Spring Initializr. В качестве системы автоматической сборки проекта будем использовать Gradle. В качестве языка пусть будет Java 15. Никаких дополнительных зависимостей в соответствующем блоке Dependencies добавлять не требуется.

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

Для того чтобы добавить Spring Boot Adapter, необходимо в проект подключить зависимость org.keycloak:keycloak-spring-boot-starter и сам adapter org.keycloak.bom:keycloak-adapter-bom. Сделать это можно изменив файл build.gradle следующим образом:

...dependencyManagement {imports {mavenBom 'org.keycloak.bom:keycloak-adapter-bom:12.0.3'}}dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'implementation 'org.keycloak:keycloak-spring-boot-starter'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...
Проблемы в Java 14+

Если запустить Spring Boot приложение на Java 14 или выше, то при обращении к вашим методам API, закрытым ролями кейклока, будут возникать ошибки видаjava.lang.NoClassDefFoundError: java/security/acl/Group. Связано это с тем, что в Java 9 этот, а так же другие классы из этого пакета были задеприкейчины и удалены в Java 14. Исправить данную проблему, вроде как, собираются в 13-й версии Keycloak. Чтобы решить её сейчас, можно использовать Java 13 или ниже, либо, вместо сервера приложений Tomcat, который используется в Spring Boot по умолчанию, использовать, например, Undertow. Для того чтобы подключить в Spring Boot приложение Undertow, нужно добавить в build.gradle зависимость org.springframework.boot:spring-boot-starter-undertow и исключить зависимоситьspring-boot-starter-tomcat.

...dependencies {implementation('org.springframework.boot:spring-boot-starter-web') {exclude module: 'spring-boot-starter-tomcat'}implementation ('org.keycloak:keycloak-spring-boot-starter') {exclude module: 'spring-boot-starter-tomcat'}implementation 'org.springframework.boot:spring-boot-starter-undertow'testImplementation 'org.springframework.boot:spring-boot-starter-test'}...

Теперь перейдём к конфигурации приложения. Вместо properties файла конфигурации давайте будем использовать более удобный (на мой взгляд, конечно же) yml. А так же, чтобы подчеркнуть, что данный конфиг предназначен для разработки, профиль dev. Т.е. полное название файла конфигурации будет application-dev.yml.

server:  port: 8082keycloak:  auth-server-url: http://localhost:8080/auth  realm: "list-keep"  resource: "list-keep"  bearer-only: true  security-constraints:    - authRoles:        - uma_authorization      securityCollections:        - patterns:            - /api/*

Давайте подробнее разберём данный конфиг:

  • server

    • port - порт на котором будет запущенно приложение

  • keycloak

    • auth-server-url - адрес на котором запущен Keycloak

    • realm - название нашего realm в Keycloak

    • resource - Client ID нашего клиента

    • bearer-only - если выставлено true, то приложение может только проверять токены, и в приложении нельзя будет залогиниться, например, с помощью логина и пароля из браузера

    • security-constraints - для описания ролевой политики

      • authRoles - список ролей Keycloak

      • securityCollections

        • patterns - URL-паттерны для методов REST API, которые требуется закрыть соответствующими ролями

      В данном конкретном случае мы закрываем ролью uma_authorization все методы, в начале которых присутствует путь /api/. Звёздочка в конце паттерна означает любое количество любых символов. Роль uma_authorization добавляется автоматически ко всем созданным пользователям, т.е. по сути данная ролевая политика означает что все методы /api/* доступны только авторизованным пользователям.

В общем-то, это все настройки которые нужно выполнить в Spring Boot приложении для интеграции с Keycloak. Давайте теперь добавим какой-нибудь тестовый контроллер.

@RestController@RequestMapping("/api/user")public class UserController {    @GetMapping("/current")    public User getCurrentUser(            KeycloakPrincipal<KeycloakSecurityContext> principal    ) {        return new User(principal.getKeycloakSecurityContext()                .getToken().getPreferredUsername()        );    }}
User.java
public class User {    private String name;    public User(String name) {        this.name = name;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

В данном контроллере есть лишь один метод /api/user/current, который возвращает информацию по текущему юзеру, а именно Preferred Username из токена. По умолчанию в Preferred Username находится username пользователя Keycloak.

Исходники проекта можно посмотреть тут.

Интеграция с Vue.js

Начнём с создания проекта. Создать проект можно, например, с помощью Vue CLI.

vue create list-keep-front

После ввода данной команды необходимо выбрать версию Vue. Т.к. в проекте будет использоваться библиотека Vuetify, которая на данный момент не поддерживает Vue 3, нужно выбрать Vue 2.

После этого нужно перейти в проект и добавить Vuetify.

vue add vuetify

После добавления Vuetify вместе с самой библиотекой в проект будут добавлены каталоги components и assets. В components будет компонент HelloWorld, с примером страницы на Vuetify, а в assets ресурсы, использующиеся в компоненте HelloWorld. Эти каталоги нам не пригодятся, поэтому можно их удалить.

Для удобства разработки сконфигурируем devServer следующим образом: запускать приложение будем на порту 8081, все запросы, которые начинаются с /api/ будем проксировать на адрес, на котором запущенно приложение на Spring Boot.

module.exports = {  devServer: {    port: 8081,    proxy: {      '^/api/': {        target: 'http://localhost:8082'      }    }  }}

Перейдём к добавлению в проект поддержки Keycloak. Для начала обратимся к официальной документации. Там нам рассказывают о том, что в проект нужно добавить Keycloak JS Adapter. Сделать это можно с помощью библиотеки keycloak-js. Добавим её в проект.

yarn add keycloak-js

Далее нам предлагают добавить в src/main.js код, который добавит в наш проект поддержку Keycloak.

// Параметры для подключения к Keycloaklet initOptions = {  url: 'http://127.0.0.1:8080/auth', // Адрес Keycloak  realm: 'keycloak-demo', // Имя нашего realm в Keycloak  clientId: 'app-vue', // Идентификатор клиента в Keycloak    // Перенаправлять неавторизованных пользователей на страницу входа  onLoad: 'login-required'}// Создать Keycloak JS Adapterlet keycloak = Keycloak(initOptions);// Инициализировать Keycloak JS Adapterkeycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {  if (!auth) {    // Если пользователь не авторизован - перезагрузить страницу    window.location.reload();  } else {    Vue.$log.info("Authenticated");        // Если авторизован - инициализировать приложение Vue    new Vue({      el: '#app',      render: h => h(App, { props: { keycloak: keycloak } })    })  }  // Пытаемся обновить токен каждые 6 секунд  setInterval(() => {    // Обновляем токен, если срок его действия истекает в течении 70 секунд    keycloak.updateToken(70).then((refreshed) => {      if (refreshed) {        Vue.$log.info('Token refreshed' + refreshed);      } else {        Vue.$log.warn('Token not refreshed, valid for '          + Math.round(keycloak.tokenParsed.exp          + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');      }    }).catch(() => {      Vue.$log.error('Failed to refresh token');    });  }, 6000)}).catch(() => {  Vue.$log.error("Authenticated Failed");});

С инициализацией Keycloak JS Adapter, вроде бы, всё понятно. А вот использование setInterval для обновления токенов мне показалось не очень практичным и красивым решением. Как минимум, кажется, что при бездействии пользователя на странице токены всё равно продолжат обновляться, хоть это и не требуется. На мой взгляд, обновление токенов лучше сделать так, как предлагает, например, автор данной статьи. Т.е. обновлять токены когда пользователь выполняет какое-либо действие в приложении. Автор указанной статьи выделяет три таких действия:

  • Взаимодействие с API (бэкендом)

  • Навигация (переход по страницам)

  • Переход на вкладку с нашим приложением, например, из другой вкладки

Приступим к реализации. Для того чтобы можно было обновлять токен из различных частей приложения, нам понадобится глобальный экземпляр Keycloak JS Adapter. Для этого во Vue.js существует функционал плагинов. Создадим свой плагин для Keycloak JS Adapter в файле /plugins/keycloak.js.

import Vue from 'vue'import Keycloak from 'keycloak-js'const initOptions = {    url: process.env.VUE_APP_KEYCLOAK_URL,    realm: 'list-keep',    clientId: 'list-keep'}const keycloak = Keycloak(initOptions)const KeycloakPlugin = {    install: Vue => {        Vue.$keycloak = keycloak    }}Vue.use(KeycloakPlugin)export default KeycloakPlugin

Значение адреса Keycloak, указанное в initOptions.url, может отличаться в зависимости от того где запущенно приложение (локально, на тесте, на проде), поэтому, чтобы иметь возможность указывать значения в зависимости от среды, будем использовать переменные окружения. Для локального запуска можно создать файл .env.local в корне проекта со следующим содержимым.

VUE_APP_KEYCLOAK_URL = http://localhost:8080/auth

Теперь нам достаточно импортировать файл с созданным нами плагином в main.js, и мы сможем из любого места приложения обратиться к нашему Keycloak JS Adapter с помощью Vue.$keycloak. Давайте это и сделаем, а так же создадим экземпляр Vue нашего приложения. Для этого изменим файл main.js следующим образом.

import Vue from 'vue'import App from './App.vue'import vuetify from './plugins/vuetify'import router from '@/router'import i18n from '@/plugins/i18n'import '@/plugins/keycloak'import { updateToken } from '@/plugins/keycloak-util'Vue.config.productionTip = falseVue.$keycloak.init({ onLoad: 'login-required' }).then((auth) => {  if (!auth) {    window.location.reload();  } else {    new Vue({      vuetify,      router,      i18n,      render: h => h(App)    }).$mount('#app')    window.onfocus = () => {      updateToken()    }  }})

Помимо инициализации Keycloak JS Adapter, здесь добавлен вызов функции updateToken() на событие window.onfocus, которое будет возникать при переходе пользователя на вкладку с нашим приложением. Наша функция updateToken() вызывает функцию updateToken() из Keycloak JS Adapter и, соответственно, обновляет токен, если срок жизни токена в секундах на данный момент меньше, чем значение TOKEN_MIN_VALIDITY_SECONDS, после чего возвращает актуальный токен.

import Vue from 'vue'const TOKEN_MIN_VALIDITY_SECONDS = 70export async function updateToken () {    await Vue.$keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS)    return Vue.$keycloak.token}

Теперь добавим обновление токена на оставшиеся действия пользователя, а именно на взаимодействие с API и на навигацию. С API мы будем взаимодействовать с помощью axios. Помимо обновления токена нам в каждом запросе необходимо добавлять http-хидер Authorization: Bearer с нашим токеном для авторизации в нашем Spring Boot сервисе. Так же давайте будем перенаправлять на какую-нибудь страницу с ошибками, например, /error, если API будет возвращать нам ошибки. Для того чтобы выполнять какие-либо действие на любые запросы/ответы в axios существуют интерцепторы, добавить которые можно в App.vue.

<template>  <v-app>    <v-main>      <router-view></router-view>    </v-main>  </v-app></template><script>import Vue from 'vue'import axios from 'axios'import { updateToken } from '@/plugins/keycloak-util'const AUTHORIZATION_HEADER = 'Authorization'export default Vue.extend({  name: 'App',  created: function () {    axios.interceptors.request.use(async config => {      // Обновляем токен      const token = await updateToken()      // Добавляем токен в каждый запрос      config.headers.common[AUTHORIZATION_HEADER] = `Bearer ${token}`      return config    })        axios.interceptors.response.use( (response) => {      return response    }, error => {      return new Promise((resolve, reject) => {        // Если от API получена ошибка - отправляем на страницу /error        this.$router.push('/error')        reject(error)      })    })  },  // Обновляем токен при навигации  watch: {    $route() {      updateToken()    }  }})</script>

Помимо интерцепторов мы здесь добавили наблюдателя (watch), который будет отслеживать переход пользователя по страницам приложения и обновлять при этом токен.

Интеграция с Keycloak закончена. Давайте теперь добавим тестовую страницу /pages/Home.vue, на которой будем вызывать с помощью axios тестовый метод /api/user/current, который мы ранее добавили в Spring Boot приложение, и выводить имя полученного пользователя.

<template>  <div>    <p>{{ user.name }}</p>  </div></template><script>import axios from 'axios'export default {  name: 'Home',  data() {    return {      user: {}    }  },  mounted() {    axios.get('/api/user/current')        .then(response => {          this.user = response.data        })  }}</script>

Для того чтобы можно было попасть на данную страницу в нашем приложении необходимо добавить её в router.js. Данная страница будет доступна по пути /.

import Vue from 'vue'import VueRouter from 'vue-router'import Home from '@/pages/Home'import Error from '@/pages/Error'import NotFound from '@/pages/NotFound'Vue.use(VueRouter)let router = new VueRouter({    mode: 'history',    routes: [        {            path: '/',            component: Home        },        {            path: '/error',            component: Error        },        {            path: '*',            component: NotFound        }    ]})export default router

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

И ещё немного о страницах

Помимо страницы /pages/Home.vue в роутере присутствуют страницы /pages/Error.vue и /pages/NotFound.vue. НаError , как уже упоминалось ранее, происходит переход из интерцептора при получении ошибок от API. На NotFound - если будет переход на неизвестную страницу.

Для примера давайте рассмотрим содержимое страницы Error.vue. Содержимое NotFound.vue практически ничем не отличается.

<template>  <v-container      class="text-center"      fill-height      style="height: calc(100vh - 58px);"  >    <v-row align="center">      <v-col>        <h1 class="display-2 primary--text">          {{ $t('oops.error.has.occurred') }}        </h1>        <p>{{ $t('please.try.again.later') }}</p>        <v-btn            href="http://personeltest.ru/aways/habr.com/"            color="primary"            outlined        >          {{ $t('go.to.main.page') }}        </v-btn>      </v-col>    </v-row>  </v-container></template><script>export default {  name: 'Error'}</script>

В шаблоне данной страницы используется локализация. Работает она с помощью плагина vue-i18n. Для того чтобы прикрутить локализацию своих текстовок нужно добавить переводы в виде json файлов в проект. Например, для русской локализации можно создать файл ru.json и положить его в каталог locales. Теперь эти текстовки необходимо загрузить в VueI18n. Сделать это можно, например, следующим образом. Давайте код по загрузке текстовок вынесем в/plugins/i18n.js.

import Vue from 'vue'import VueI18n from 'vue-i18n'Vue.use(VueI18n)function loadLocaleMessages () {    const locales = require.context('@/locales', true,                                    /[A-Za-z0-9-_,\s]+\.json$/i)    const messages = {}    locales.keys().forEach(key => {        const matched = key.match(/([A-Za-z0-9-_]+)\./i)        if (matched && matched.length > 1) {            const locale = matched[1]            messages[locale] = locales(key)        }    })    return messages}export default new VueI18n({    locale: 'ru',    fallbackLocale: 'ru',    messages: loadLocaleMessages()})

После этого к этим текстовкам можно будет обращаться из шаблона страницы с помощью $t.

Так же привожу содержимое /plugins/vuetify.js. В нём добавлена возможность использовать иконки Font Awesome на страницах нашего приложения.

import Vue from 'vue'import Vuetify from 'vuetify/lib/framework'import 'vuetify/dist/vuetify.min.css'import '@fortawesome/fontawesome-free/css/all.css'Vue.use(Vuetify);const opts = {    icons: {        iconfont: 'fa'    }}export default new Vuetify(opts)
Немного мыслей об обработке ошибок

Функции Keycloak JS Adapter init() и updateToken() возвращают объект KeycloakPromise, у которого есть возможность вызывать catch() и в нём обрабатывать ошибки. Но лично я не понял что именно в данном случае будет считаться ошибками и когда мы попадём в этот блок, т.к., например, если Keycloak не доступен, то в этот блок мы не попадаем. Поэтому в приведённом здесь приложении, я возможные ошибки от этих двух функций не обрабатываю. Возможно, если Keycloak не работает, то в продакшене стоит делать так, чтоб и наше приложение тоже становилось недоступным и не пытаться это как-то обработать. Ну или если всё-таки нужно такие ошибки понимать именно в Vue.js приложении, то, возможно, нужно как-то доработать keycloak-js.

Исходники проекта можно посмотреть тут.

Login Flows

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

  • Авторизация и регистрация пользователей

  • Локализация страниц

  • Подтверждение email

  • Вход через социальные сети

Локализация страниц в Keycloak

Запустим наши Spring Boot и Vue.js приложения. При переходе в клиентское Vue.js приложение нас перенаправит на страницу логина Keycloak.

В первую очередь давайте добавим поддержку русского языка. Для этого в админке Keycloak, на вкладке Theams, в настройки realm включаем флаг Internationalization Enabled . В Supported Locales убираем все локали кроме ru, пусть наше приложение на Vue.js поддерживает только один язык. В Default Locale выставляем ru.

Нажимаем Save и возвращаемся в наше клиентское приложение.

Как видим, русский язык у нас появился, правда, не все текстовки были локализованы. Это можно исправить, добавив собственные варианты перевода. Сделать это можно на вкладке Localization, в настройках realm.

Здесь имеется возможность добавить текстовки вручную по одной, либо загрузить их из json файла. Давайте сделаем это вручную. Для начала требуется добавить локаль. Вводим ru и нажимаем Create. После чего попадаем на страницу Add localization text. На этой странице нам необходимо заполнить поля Key и Value. Если с value всё ясно, это будет просто значение нашей текстовки, то вот где взять Key не совсем понятно. В документации допустимые ключи нигде не описаны (либо я просто плохо искал), поэтому остаётся лишь найти их в исходниках Keycloak. Находим в ресурсах нужную нам базовую тему base и страницу login, а затем файл с текстовками в локали en - messages_en.properties. В этом файле по значению определяем нужный нам ключ текстовки, добавляем его в Key на странице Add localization text, а так же добавляем нужное нам Value и нажимаем Save.

После этого на вкладке Localization в настройках realm, при выборе локали ru, появляется таблица, в которой можно посмотреть, отредактировать или удалить нашу добавленную текстовку.

Вернёмся в наше клиентское приложение. Теперь все текстовки на странице логина локализованы.

Регистрация пользователей

Поддержку регистрации пользователей можно добавить, включив флаг User registration на вкладке Login в настройках realm.

После этого на странице логина появится кнопка Регистрация.

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

Давайте немного подкрутим эту страницу. Для начала добавим отсутствующий перевод текстовки, аналогично тому, как мы делали это ранее для страницы логина. Так же давайте уберём поле Имя пользователя. На самом деле совсем его убрать нельзя, т.к. это поля обязательно для заполнения у пользователя Keycloak, но можно сделать так, чтобы в качестве имени пользователя использовался email, при этом поле Имя пользователя исчезнет с формы регистрации. Сделать это можно, включив флаг Email as username на вкладке Login в настройках realm. После этого возвращаемся на страницу регистрации и видим что поле исчезло.

Кроме этого на странице логина поле, которое ранее называлось Имя пользователя или E-mail, теперь называется просто E-mail. Правда, пользователи, которые, например, были созданы до выставления этого флага, и у которых email отличается от имени пользователя, могут продолжать в качестве логина использовать имя пользователя и всё будет корректно работать.

Подтверждение email

Давайте включим подтверждение email у пользователей, чтобы после регистрации они не могли зайти в наше приложение, пока не подтвердят свой email. Сделать это можно, включив флаг Verify email на вкладке Login в настройках realm. И нет, после этого волшебным образом всё не заработает, нужно ещё где-то добавить конфигурацию SMTP-сервера, с которого мы будем осуществлять рассылку. Сделать это можно на вкладке Email, в настройках realm. Ниже приведён пример настроек SMTP-сервера Gmail.

Нажимаем Test connection и получаем ошибку.

Ошибка возникает из-за того, что при нажатии на Test connection должно отправиться письмо на адрес пользователя, под которым мы сейчас залогинены в Keycloak, но этот адрес не задан. Соответственно, если вы заранее задали этот email, ошибки не будет.

Давайте зададим email нашему пользователю Keycloak. Для этого перейдём в realm master на страницу Users и нажмём View all users, чтобы отобразить всех пользователей.

Перейдём на страницу редактирования нашего пользователя и зададим ему email.

Возвращаемся на страницу конфигурации SMTP-сервера, снова пробуем Test connection и видим что всё рабо... Нет, мы снова видим ошибку. Правда, уже другую.

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

Снова жмём Test connection и, наконец-то, получаем Success.

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

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

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

На почту нам придёт письмо с ссылкой, по которой можно подтвердить email.

После перехода по ссылке мы попадём на нашу тестовую страницу /pages/Home.vue, на которой просто выводится имя пользователя. Т.к. в настройках нашего realm мы указали Email as username, то на данной странице мы увидим email нашего пользователя.

Social Login

Теперь добавим вход через социальные сети. В качестве примера давайте рассмотрим вход с помощью Google. Для того чтобы добавить данный функционал нужно в нашем realm создать соответствующий Identity Provider. Для этого нужно перейти на страницу Identity Providers и в списке Add provider... выбрать Google.

После этого мы попадём на страницу создания Identity Provider.

Здесь нам требуется задать два обязательных параметра - Client ID и Client Secret. Взять их можно из Google Cloud Platform.

Сказ о получении ключей из Google Cloud Platform

В первую очередь нам нужно создать в Google Cloud Platform проект.

Жмём CREATE PROJECT и попадаем на страницу создания проекта.

Задаём имя, жмём CREATE, ждём некоторое время, пока не будет создан наш проект, и после этого попадаем на DASHBOARD проекта.

Выбираем в меню APIs & Services -> Credentials. И попадаем на страницу на которой мы можем создавать различные ключи для нашего приложения.

Жмём Create credentials -> OAuth client ID и попадаем на очередную страницу.

Видим, что нам так просто не хотят давать возможность создавать ключи, а сначала просят создать некий OAuth consent screen. Что ж, хорошо, жмём CONFIGURE CONSENT SCREEN и снова новая страница.

Здесь давайте выберем External. Ну как выберем, выбора, на самом деле, у нас нет, т.к. Internal доступно только пользователямGoogle Workspace и эта штука платная и нужна, в общем-то, только организациям. Нажимаем Create и попадаем на страницу OAuth consent screen. Здесь заполняем название приложения и почты и жмём SAVE AND CONTINUE.

На следующей странице можно задать так называемые области действия OAuth 2.0 для API Google. Ничего задавать не будем, жмём SAVE AND CONTINUE.

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

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

Жмём BACK TO DASHBOARD, чтобы всё это уже закончить, и попадаем на страницу, на которой мы можем редактировать все те данные, которые мы вводили на предыдущих страницах.

Жмём Credentials, затем снова Create credentials -> OAuth client ID и попадаем на страницу создания OAuth client ID. И снова нужно что-то вводить. Google, ну сколько можно?! Ниже приведены поля, которые необходимо заполнить на этой странице.

  • Application type - выбираем Web application

  • Name - пишем имя нашего приложения

  • Authorized redirect URIs - сюда пишем значение из поля Redirect URI со страницы создания Identity Provider, чтобы Google редиректил пользователей на корректный адрес Keycloak после авторизации

Жмём CREATE и, наконец-то, получаем требуемые нам Client ID и Client Secret, которые нам нужно указать на странице создания Identity Provider в Keycloak.

Заполняем поля Client ID и Client Secret и жмём Save, чтобы создать Identity Provider. Теперь вернёмся на страницу логина нашего клиентского приложения. На этой странице появится нелокализованная текстовка, добавить её можно аналогично тому, как это было сделано ранее. Ниже на скрине ниже эта проблема уже устранена.

Итак, это всё что требовалось сделать, теперь мы можем входить в наше приложение с помощью Google.

Импорт и экспорт в Keycloak

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

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

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

Импортировать данные можно на странице Import. Либо в yml-файле Docker Compose, если вы его используете. Для этого нужно указать в переменной окружения KEYCLOAK_IMPORT путь до ранее экспортированного файла и примонтировать этот файл в контейнер с помощью volumes. Итоговый файл приведен ниже.

# For developmentversion: "3.8"services:  keycloak:    image: jboss/keycloak:12.0.2    environment:      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin      KEYCLOAK_IMPORT: "/tmp/realm-export.json"    volumes:      - "./keycloak/realm-export.json:/tmp/realm-export.json"    ports:      - 8080:8080
Импорт файлов локализации

Как уже упоминалось ранее, файлы локализации можно импортировать через админку. Помимо этого у Keycloak есть Admin REST API, а именно метод POST /{realm}/localization/{locale}, с помощью которого можно это сделать. В теории это можно использовать в Docker Compose, чтобы при запуске сразу загружать все текстовки в автоматическом режиме. На практике для этого можно написать bash-скрипт и вызвать его после того как в контейнере запустится Keycloak. Пример такого скрипта приведен ниже.

#!/bin/bashDIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli");export DIRECT_GRANT_RESPONSEACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');export ACCESS_TOKENcurl -i --request POST http://localhost:8080/auth/admin/realms/list-keep/localization/ru -F "file=@ru.json" --header "Content-Type: multipart/form-data" --header "Authorization: Bearer $ACCESS_TOKEN";

И в докер образе jboss/keycloak даже есть возможность запускать скрипты при старте (см. раздел Running custom scripts on startup на странице докер образа). Но запускаются они до фактического старта Keycloak. Поэтому пока я оставил данный вопрос не решенным. Если у кого-то есть идеи как это можно красиво сделать - оставляйте их в комментариях.

Заключение

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

Подробнее..

Учим Tekton Pipelines и смотрим глазами NASA на космос, пока Ansible сам разбирается с нашими container images

24.07.2020 20:08:25 | Автор: admin


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

Начни новое:



Строй:



Пообщаться:



По-русски:


Большая серия про Ansible в записи
https://www.redhat.com/en/global/russia-cis/webinars#ansible

Подробнее..

Перевод Запускаем Keycloak в HA режиме на Kubernetes

20.07.2020 14:14:41 | Автор: admin


TL;DR: будет описание Keycloak, системы контроля доступа с открытым исходным кодом, разбор внутреннего устройства, детали настройки.


Введение и основные идеи


В этой статье мы увидим основные идеи, которые следует помнить при разворачивании кластера Keycloak поверх Kubernetes.


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


Keycloak это комплексная система, написанная на Java и построенная поверх сервера приложений Wildfly. Если кратко, это framework для авторизации, дающий пользователям приложений федеративность и возможность SSO (single sign-on).


Приглашаем почитать официальный сайт или Википедию для подробного понимания.


Запуск Keycloak


Для Keycloak необходимо два постоянно хранимых источника данных для запуска:


  • База данных, применяемая для хранения устоявшихся данных, например информации о пользователях
  • Datagrid cache, который применяется для кэширования данных из базы, а также для хранения некоторых короткоживущих и часто изменяемых метаданных, например пользовательских сессий. Релизуется Infinispan, который обычно значительно быстрее базы данных. Но в любом случае сохраняемые в Infinispan данные эфемерны и их не надо куда-либо сохранять при перезапуске кластера.

Keycloak работает в четырех различных режимах:


  • Обычный один и только один процесс, настраивается через файл standalone.xml
  • Обычный кластер (высокодоступный вариант) все процессы должны использовать одну и ту же конфигурацию, которую надо синхронизировать вручную. Настройки хранятся в файле standalone-ha.xml, дополнительно надо сделать общий доступ к базе данных и балансировщик нагрузки.
  • Доменный кластер запуск кластера в обычном режиме быстро становится рутинным и скучным занятием при росте кластера, поскольку каждый раз при изменении конфигурации надо все изменения внести на каждом узле кластера. Доменный режим работы решает этот вопрос путем настройки некоторого общего места хранения и публикации конфигурации. Эти настройки хранятся в файле domain.xml
  • Репликация между датацентрами в случае, если хотите запустить Keycloak в кластере из нескольких датацентров, чаще всего в разных местах географически. В этом варианте работы каждый датацентр будет иметь собственный кластер Keycloak серверов.

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


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


Обычный кластер Keycloak


Для запуска Keycloak в этом режиме нужно:


  • настроить внешнюю общую базу данных
  • установить балансировщик нагрузки
  • иметь внутреннюю сеть с поддержкой ip multicast

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


Для лучшего понимания того, как Keycloak работает в отказоустойчивом (HA) кластере, важно знать, как сильно это все зависит от способностей Wildfly к кластеризации.


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


  • mod_cluster: работает совместно с Apache в качестве балансировщика HTTP, зависит от TCP multicast для поиска узлов по умолчанию. Может быть заменен внешним балансировщиком.


  • infinispan: распределенный кэш, использующий каналы JGroups в качестве транспортного уровня. Дополнительно может применять протокол HotRod для свящи с внешним кластером Infinispan для синхронизации содержимого кэша.


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



Балансировщик нагрузки


При установке балансировщика в качестве ingress контроллера в кластере Kubernetes важно иметь ввиду следующие вещи:


Работа Keycloak подразумевает, что удаленный адрес клиента, подключаемого по HTTP к серверу аутентификации, является реальным ip-адресом клиентского компьютера. Настройки балансировщика и ingress должны корректно устанавливать заголовки HTTP X-Forwarded-For и X-Forwarded-Proto, а также сохранять изначальный заголовок HOST. Последняя версия ingress-nginx (> 0.22.0) отключает это по умолчанию


Активация флага proxy-address-forwarding путем установки переменной окружения PROXY_ADDRESS_FORWARDING в true дает Keycloak понимание, что он работает за proxy.


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


Конкретно у нас вопреки документации не сработало прикрепление сессии с именем cookie AUTH_SESSION_ID. Keycloak зациклил перенаправление, поэтому мы рекомендуем выбрать другое имя cookie для sticky session.

Также Keycloak прикрепляет имя узла, ответившего первым, к AUTH_SESSION_ID, а поскольку каждый узел в высокодоступном варианте использует одну и ту же базу данных, каждый из них должен иметь отдельный и уникальный идентификатор узла для управления транзакциями. Рекомендуется ставить в JAVA_OPTS параметры jboss.node.name и jboss.tx.node.id уникальными для каждого узла можно к примеру ставить имя пода. Если будете ставить имя пода не забывайте про ограничение в 23 символа для переменных jboss, так что лучше использовать StatefulSet, а не Deployment.


Еще одни грабли если под удаляется или перезапускается, его кэш теряется. С учетом этого стоит установить число владельцев кэша для всех кэшей не менее чем в два, так будет оставаться копия кэша. Решение запустить скрипт для Wildfly при запуске пода, подложив его в каталог /opt/jboss/startup-scripts в контейнере:


Содержимое скрипта
embed-server --server-config=standalone-ha.xml --std-out=echobatchecho * Setting CACHE_OWNERS to "${env.CACHE_OWNERS}" in all cache-containers/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})run-batchstop-embedded-server

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


Приватная сеть с поддержкой ip multicast


Если применяете Weavenet в качестве CNI, multicast будет работать сразу же и ваши узлы Keycloak будут видеть друг друга, как только будут запущены.


Если у вас нет поддержки ip multicast в кластере Kubernetes, можно настроить JGroups на работу с другими протоколами для поиска узлов.


Первый вариант испольльзование KUBE_DNS, который использует headless service для поиска узлов Keycloak, вы просто передаете JGroups имя сервиса, которое будет использовано для поиска узлов.


Еще один вариант применение метода KUBE_PING, который работает с API для поиска узлов (надо настроить serviceAccount с правами list и get, после чего настроить поды для работы с этой serviceAccount).


Способ поиска узлов для JGroups настраивается путем выставления переменных окружения JGROUPS_DISCOVERY_PROTOCOL и JGROUPS_DISCOVERY_PROPERTIES. Для KUBE_PING надо выбрать поды задавая namespace и labels.


Если используете multicast и запускаете два и больше кластеров Keycloak в одном кластере Kubernetes (допустим один в namespace production, второй staging) узлы одного кластера Keycloak могут присоединиться к другому кластеру. Обязательно используйте уникальный multicast адрес для каждого кластера путем установки переменныхjboss.default.multicast.address и jboss.modcluster.multicast.address в JAVA_OPTS.

Репликация между датацентрами



Связь


Keycloak использует множественные отдельные кластера кэшей Infinispan для каждого датацентра, где расположены кластера Keycloack, составленные из узлов Keycloak. Но при этом нет разницы между узлами Keycloak в разных датацентрах.


Узлы Keycloak используют внешнюю Java Data Grid (сервера Infinispan) для связи между датацентрами. Связь работает по протоколу Infinispan HotRod.


Кэши Infinispan должны быть настроены с атрибутом remoteStore, для того, чтобы данные могли сохраняться в удаленных (в другом датацентре, прим. переводчика) кэшах. Есть отдельные кластера infinispan среди JDG серверов, так что данные, сохраняемые на JDG1 на площадке site1 будут реплицированы на JDG2 на площадке site2.


Ну и наконец, принимающий сервер JDG оповещает сервера Keycloak своего кластера через клиентские соединения, что является особенностью протокола HotRod. Узлы Keycloak на site2 обновляют свои кэши Infinispan, и конкретная пользовательская сессия становится также доступной на узлах Keycloak на site2.


Для некоторых кэшей также возможно не делать резервные копии и полностью отказаться от записи данных через сервер Infinispan. Для этого надо убрать настройку remote-store конкретному кэшу Infinispan (в файле standalone-ha.xml), после чего некоторый конкретный replicated-cache также перестанет быть нужным на стороне Infinispan сервера.


Настройка кэшей


Есть два типа кэшей в Keycloak:


  • Локальный. Он расположен рядом с базой, служит для уменьшения нагрузки на базу данных, а также для снижения задержки ответа. В этом типе кэша хранится realm, клиенты, роли и пользовательские метаданные. Этот тип кэша не реплицируется, даже если этот кэш часть кластера Keycloak. Если меняется некоторая запись в кэше остальным серверам в кластере отправляется сообщение об изменении, после чего запись исключается из кэша. См. описание work далее, для более детального описания процедуры.


  • Реплицируемый. Обрабатывает пользовательские сессии, offline токены, а также следит за ошибками входа для определения попыток фишинга паролей и других атак. Хранимые данные в этим кэшах временные, хранятся только в оперативной памяти, но могут быть реплицированы по кластеру.



Кэши Infinispan


Сессии концепция в Keycloak, отдельные кэши, которые называются authenticationSessions, применяются для хранения данных конкретных пользователей. Запросы с этих кэшей обычно нужны браузеру и серверам Keycloak, не приложениям. Здесь и проявляется зависимость от sticky sessions, а сами такие кэши не нужно реплицировать, даже и в случае Active-Active режима.


Токены действия. Очередная концепция, обычно применяется для различных сценариев, когда, к примеру, пользователь должен сделать что-то асинхронно по почте. Например, во время процедуры forget password кэш actionTokens применяется для отслеживания метаданных связанных токенов к примеру токен уже использован, и не может быть активирован повторно. Этот тип кэша обычно должен реплицироваться между датацентрами.


Кэширование и устаревание хранимых данных работает для того, чтобы снять нагрузку с базы данных. Подобное кэширование улучшает производительность, но добавляет очевидную проблему. Если один сервер Keycloak обновляет данные, остальные сервера должны быть оповещены об этом, чтобы они могли провести актуализацию данных в своих кэшах. Keycloak использует локальные кэши realms, users и authorization для кэширования данных из базы.


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


Пользовательские сессии. Кэши с именами sessions, clientSessions, offlineSessions и offlineClientSessions, обычно реплицируются между датацентрами и служат для хранения данных об пользовательских сессиях, которые активны во время активности пользователя в браузере. Эти кэши работают с приложением, обрабатывающим запросы HTTP от конечных пользователей, так что они связаны с sticky sessions и должны реплицироваться между датацентрами.


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


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


<replicated-cache-configuration name="keycloak-sessions" mode="ASYNC" start="EAGER" batching="false"></replicated-cache-configuration><replicated-cache name="work" configuration="keycloak-sessions" /><replicated-cache name="sessions" configuration="keycloak-sessions" /><replicated-cache name="offlineSessions" configuration="keycloak-sessions" /><replicated-cache name="actionTokens" configuration="keycloak-sessions" /><replicated-cache name="loginFailures" configuration="keycloak-sessions" /><replicated-cache name="clientSessions" configuration="keycloak-sessions" /><replicated-cache name="offlineClientSessions" configuration="keycloak-sessions" />

Необходимо настроить и запустить кластер Infinispan перед запуском кластера Keycloak

Затем надо настроить remoteStore для Keycloak кэшей. Для этого достаточно скрипта, который делается аналогично предыдущему, который использоваться для настройки переменной CACHE_OWNERS, надо сохранить его в файл и положить в каталог /opt/jboss/startup-scripts:


Содержимое скрипта
embed-server --server-config=standalone-ha.xml --std-out=echobatchecho *** Update infinispan subsystem ***/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)echo ** Add remote socket binding to infinispan server **/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})echo ** Update replicated-cache work element **/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=work, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache sessions element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=sessions, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache offlineSessions element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=offlineSessions, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache clientSessions element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=clientSessions, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache offlineClientSessions element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=offlineClientSessions, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache loginFailures element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    remote-servers=["remote-cache"], \    cache=loginFailures, \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache actionTokens element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( \    passivation=false, \    fetch-state=false, \    purge=false, \    preload=false, \    shared=true, \    cache=actionTokens, \    remote-servers=["remote-cache"], \    properties={ \        rawValues=true, \        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} \    } \)/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true)echo ** Update distributed-cache authenticationSessions element **/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)echo *** Update undertow subsystem ***/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)run-batchstop-embedded-server

Не забывайте установить JAVA_OPTS для узлов Keycloak для работы HotRod: remote.cache.host, remote.cache.port и имя сервиса jboss.site.name.


Ссылки и дополнительная документация



Статья переведена и подготовлена для Хабра сотрудниками обучающего центра Слёрм интенсивы, видеокурсы и корпоративное обучение от практикующих специалистов (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)

Подробнее..

OpenShift 4.5, лучшие практики edge-разработки и горы полезных книг и ссылок

10.08.2020 10:05:17 | Автор: admin


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

Начни новое:



Качай:


  • Скачай и установи OpenShift Container Platform 4

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

Почитать на досуге:



Пообщаться:


Подробнее..

Red Hat Flatpak, DevNation Day, шпаргалка по программированию на Cи и пять вебинаров на русском

27.08.2020 16:05:58 | Автор: admin


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

Начни новое:



Качай:


  • Шпаргалка по языку программирования C
    C это классика компилируемых языков программирования, концептуальный предок Lua, C++, Java, Go и многих других современных языков, ну и просто отличный выбор, чтобы начать учиться программированию. Эта шпаргалка содержит полезную выжимку по синтаксису C.

Строй:



Событие сентября присоединяйтесь!




15 сентября состоится DevNation Day абсолютно бесплатная виртуальная конференция по новым компьютерным технологиям и защите компьютерным программ (тм) ну то есть вопросам разработки и технологий. В это году в центре внимания 4 темы: Kubernetes/OpenShift, JavaScript, Python и Java.

Помимо экспертов Red Hat выступят представители Google, MongoDB, Redis, Snyk, Tail, Auth0, Ionic и многих других ведущих компаний. Никуда ехать не надо сидите (или лежите) там, где вам удобно, смотрите-слушайте и общайтесь с докладчиками через онлайновые опросы и чаты.

Пообщаться:



По-русски:


Мы начинаем серию пятничных вебинаров про нативный опыт использования Red Hat OpenShift Container Platform и Kubernetes. Регистрируйтесь и приходите:

Подробнее..

Подключение Keycloak к Spring Boot приложению

14.04.2021 12:16:12 | Автор: admin

Привет Хабр!

Как известно, spring OAuth2.0.x переведен в режим поддержки уже почти как 2 года назад , а большая часть его функциональности теперь доступна в spring-security (матрица сопоставления). В spring-security отказались переносить Authorization service (roadmap) и предлагают использовать вместо него свободные или платные аналоги, в частности keycloak. В этом посте мы хотели бы поделится различными вариантами подключения keycloak к приложениям spring-boot.

Содержание

Немного о Keycloak

Это реализация SSO (Single sign on) с открытым исходным кодом для управления идентификацией и доступом пользователей.

Основной функционал, поддерживаемый в Keycloak:

  • Single-Sign On and Single-Sign Out.

  • OpenID/OAuth 2.0/SAML.

  • Identity Brokering аутентификация с помощью внешних OpenID Connect или SAML.

  • Social Login поддержка Google, GitHub, Facebook, Twitter.

  • User Federation синхронизация пользователей из LDAP и Active Directory серверов.

  • Kerberos bridge использование Kerberos сервера для автоматической аутентификации пользователей.

  • Гибкое управление политиками через realm.

  • Адаптеры для JavaScript, WildFly, JBoss EAP, Fuse, Tomcat, Jetty, Spring.

  • Возможность расширения с использованием плагинов.

  • И многое-многое другое...

Запускаем и настраиваем keycloak

Для запуска keycloak на машине разработчика удобно использовать docker-compose. В этом случае мы можем в разное время для разных приложений запускать свой сервис авторизации, тем самым избавляя себя от кучи проблем, связанных с конфигурацией под различные приложения. Ниже приведен один из вариантов конфигурации docker-compose для запуска standalone сервера с базой данных postgres:

docker-compose.yml
version: "3.8"services:  postgres:    container_name: postgres    image: library/postgres    environment:      POSTGRES_USER: ${POSTGRES_USER:-postgres}      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}      POSTGRES_DB: keycloak_db    ports:      - "5432:5432"    restart: unless-stopped  keycloak:    image: jboss/keycloak    container_name: keycloak    environment:      DB_VENDOR: POSTGRES      DB_ADDR: postgres      DB_DATABASE: keycloak_db      DB_USER: ${POSTGRES_USER:-postgres}      DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin_password    ports:      - "8484:8080"    depends_on:      - postgres    links:      - "postgres:postgres"

После успешного запуска необходимо произвести настройки realm, клиентов, ролей и пользователей.

Произведем некоторые первоначальные настройки. Создадим realm "my_realm":

После этого создадим клиент "my_client", через который будем производить авторизацию пользователей (оставим все настройки по-умолчанию):

Не забываем указывать redirect_url. В нашем случае он будет равен: http://localhost:8080/*

Создадим роли для пользователей нашей системы - "ADMIN", "USER":

Добавляем пользователей "admin" с ролью "ADMIN":

И пользователя "user" с ролью "USER". Не забываем устанавливать пароли на вкладке "Credentials":

Основная настройка закончена, теперь можно приступить к подключению spring boot приложений.

Подключаем Keycloak при помощи адаптера

В официальной документации к keycloak для использования в приложениях рекомендуют использовать готовые библиотеки - адаптеры, которые дают возможность избавиться от boilerplate кода и излишнего конфигурирования. Есть реализация для большинства популярных языков и фреймворков (supported-platforms). Мы будем использовать Spring Boot Adapter.

Создадим небольшое демонстрационное, приложение на spring-boot (исходники можно найти здесь) и подключим к нему Keycloak Spring Boot адаптер. Конфигурационный файл maven будет выглядеть так:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>" xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><groupId>org.akazakov.keycloak</groupId><artifactId>demo-keycloak-adapter</artifactId><version>0.0.1-SNAPSHOT</version><name>Demo Keycloak Adapter</name><description>Demo project for Spring Boot and Keycloak</description><properties><java.version>11</java.version></properties><dependencyManagement><dependencies><dependency><groupId>org.keycloak.bom</groupId><artifactId>keycloak-adapter-bom</artifactId><version>12.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.keycloak</groupId><artifactId>keycloak-spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies></project>

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

@RestController@RequestMapping("/api")public class SampleController {    @GetMapping("/anonymous")    public String getAnonymousInfo() {        return "Anonymous";    }    @GetMapping("/user")    @PreAuthorize("hasRole('USER')")    public String getUserInfo() {        return "user info";    }    @GetMapping("/admin")    @PreAuthorize("hasRole('ADMIN')")    public String getAdminInfo() {        return "admin info";    }    @GetMapping("/service")    @PreAuthorize("hasRole('SERVICE')")    public String getServiceInfo() {        return "service info";    }    @GetMapping("/me")    public Object getMe() {        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();        return authentication.getName();    }}

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

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak}keycloak:  auth-server-url: http://localhost:8484/auth  realm: my_realm  resource: my_client  public-client: true

После этого добавим конфигурацию spring-security, переопределим KeycloakWebSecurityConfigurerAdapter, поставляемый вместе с адаптером:

@KeycloakConfiguration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {    @Override    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {        return new NullAuthenticatedSessionStrategy();    }    @Autowired    public void configureGlobal(AuthenticationManagerBuilder authManagerBuilder) {        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());        authManagerBuilder.authenticationProvider(keycloakAuthenticationProvider);    }    @Bean    public KeycloakConfigResolver keycloakConfigResolver() {        return new KeycloakSpringBootConfigResolver();    }    @Override    protected void configure(HttpSecurity http) throws Exception {        super.configure(http);        http                .authorizeRequests()                .antMatchers("/api/anonymous/**").permitAll()                .anyRequest().fullyAuthenticated();    }}

Теперь проверим работу нашего приложения. Запустим приложение и попробуем зайти пользователем на соответствующий url. Например: http://localhost:8080/api/admin. В результате, браузер перенаправит нас на окно логина пользователя:

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

Если перейдем по адресу получения информации о текущем пользователе (http://localhost:8080/api/me), то получим в результате uuid пользователя в keycloak:

Если нам нужно, чтобы сервис только проверял токен доступа и не инициализировал процедуру аутентификации пользователя, достаточно включить bearer-only: true в конфигурацию приложения:

keycloak:  auth-server-url: http://localhost:8484/auth  realm: my_realm  resource: my_client  public-client: true  bearer-only: true

Используем OAuth2 Client из spring-security

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

Одной из ключевых особенностей spring security 5 является поддержка протоколов OAuth2 и OIDC. Мы можем использовать OAuth2 клиент из пакета spring-security для интеграции с сервером keycloak.

Итак, для использования клиента подключим соответствующую библиотеку в зависимости от проекта (исходный код примера). Полный текст pom.xml:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"         xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.3.9.RELEASE</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>    <groupId>org.akazakov.keycloak</groupId>    <artifactId>demo-keycloak-oauth</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>demo-keycloak-oauth</name>    <description>Demo project for Spring Boot OAuth and Keycloak</description>    <properties>        <java.version>11</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-oauth2-client</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.springframework.security</groupId>            <artifactId>spring-security-test</artifactId>            <scope>test</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>

Далее в application.yaml необходимо указать параметры подключения к сервису авторизации:

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak-oauth}  security:    oauth2:      client:        provider:          keycloak:            issuer-uri: http://localhost:8484/auth/realms/my_realm        registration:          keycloak:            client-id: my_client

По умолчанию роли пользователей будут вычисляться на основе значения "scope" в access token, и к ним прибавляется "ROLE_USER" для всех авторизованных пользователей системы. Можно оставить как есть и перейти на модель scope. Но в нашем примере мы будем использовать роли пользователей в рамках realm'а. Все, что нам нужно, это переопределить oidcUserService и задать свой маппинг ролей для пользователя. Нужные роли приходят в разделе "groups" токена доступа, его мы и будем использовать для определения ролей пользователя. В результате, наша конфигурация для spring security с переопределенным oidcUserService будет выглядеть так:

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests(authorizeRequests -> authorizeRequests                        .antMatchers("/api/anonymous/**").permitAll()                        .anyRequest().authenticated())                .oauth2Login(oauth2Login -> oauth2Login                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint                                .oidcUserService(this.oidcUserService())                        )                );    }    @Bean    public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {        final OidcUserService delegate = new OidcUserService();        return (userRequest) -> {            OidcUser oidcUser = delegate.loadUser(userRequest);            final Map<String, Object> claims = oidcUser.getClaims();            final JSONArray groups = (JSONArray) claims.get("groups");            final Set<GrantedAuthority> mappedAuthorities = groups.stream()                    .map(role -> new SimpleGrantedAuthority(("ROLE_" + role)))                    .collect(Collectors.toSet());            return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());        };    }}

В данном случае работа приложения будет практически аналогична работе с использованием keycloak адаптера.

Подключаем приложение как ResourceService

Довольно часто не нужно, чтобы наше приложение инициировало аутентификацию пользователя. Достаточно лишь проверки авторизации пользователя по предоставляемому токену доступа. Вариантом подключения авторизации с keycloak без использования адаптера является настройка приложения как resource server. В этом случае приложение не может инициировать аутентификацию пользователя, а только авторизует пользователя и проверяет подпись токена доступа. Подключим соответствующие библиотеки: spring-security-oauth2-resource-server и spring-security-oauth2-jose (исходный код). Полный файл pom.xml будет выглядеть так:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.akazakov.keycloak</groupId><artifactId>demo-keycloak-resource</artifactId><version>0.0.1-SNAPSHOT</version><name>demo-keycloak-resource</name><description>Demo project for Spring Boot and Spring security and Keycloak</description><properties><java.version>11</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

Далее нам необходимо указать путь к JWK (JSON Web Key) набору ключей, с помощью которых наше приложение будет проверять токены доступа. В keycloak они доступны по адресу: http://${host}/auth/realms/${realm)/protocol/openid-connect/certs. В итоге application.yml будет выгдядеть следующим образом:

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak-resource}  security:    oauth2:      resourceserver:        jwt:          jwk-set-uri: ${KEYCLOAK_REALM_CERT_URL:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/certs}

Как и в случае с OAuth2 Client нам также необходимо переопределить конвертер ролей пользователя. В данном случае мы можем переопределить jwtAuthenticationConverter.

Полный текст WebSecurityConfiguration:

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests(authorizeRequests -> authorizeRequests                        .antMatchers("/api/anonymous/**").permitAll()                        .anyRequest().authenticated())                .oauth2ResourceServer(resourceServerConfigurer -> resourceServerConfigurer                        .jwt(jwtConfigurer -> jwtConfigurer                                .jwtAuthenticationConverter(jwtAuthenticationConverter()))                );    }    @Bean    public Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());        return jwtAuthenticationConverter;    }    @Bean    public Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {        JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();        return new Converter<>() {            @Override            public Collection<GrantedAuthority> convert(Jwt jwt) {                Collection<GrantedAuthority> grantedAuthorities = delegate.convert(jwt);                if (jwt.getClaim("realm_access") == null) {                    return grantedAuthorities;                }                JSONObject realmAccess = jwt.getClaim("realm_access");                if (realmAccess.get("roles") == null) {                    return grantedAuthorities;                }                JSONArray roles = (JSONArray) realmAccess.get("roles");                final List<SimpleGrantedAuthority> keycloakAuthorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());                grantedAuthorities.addAll(keycloakAuthorities);                return grantedAuthorities;            }        };    }}

Здесь мы создаем конвертер (jwtGrantedAuthoritiesConverter), который принимает токен и извлекает из секции "realm_access" роли пользователя. Далее мы можем либо сразу вернуть их, либо, как в данном случае, расширить список, который извлекается конвертером по умолчанию.

Проверим работу. Воспользуемся встроенным в Intellij idea http клиентом, либо плагином к VSCode - Rest Client. В начале получим токен пользователя, произведем запрос к keycloak, используя логин и пароль зарегистрированного пользователя:

###POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>Content-Type: application/x-www-form-urlencodedclient_id=my_client&grant_type=password&scope=openid&username=admin&password=admin> {% client.global.set("auth_token", response.body.access_token); %}

Ответ будет примерно следующего содержания:

Ответ
POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>HTTP/1.1 200 OK...Content-Type: application/json{  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMGQwMjg2YWUtYTlmYy00MzcxLWFmM2ItZjJlNTM5N2I4NzViIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjkzMGIxMTNmLWI0NzUtNDhkMC05NTQxLWMyYzI2MWZlYmRmZCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiQURNSU4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.dvGvYhhhfH8r6EP8k_spFwBS35ulYMTWNL4lcz9PR2e-p4FU-ehre1EQA8xpbkYzYEWRB_elzTya5IhbYR8KArrujplIDNAOlqJ9W6a4Tx-r44QCteM0DW4BNzbZAH2L0Bg7aSstRKUuULceRNYQcdCvSFjEU5DsHk26a6TM5KCrkv0ryGo11pam-pnbs2Z2jOSfSHvOAfMNL9OVJYRBjlTmsEzzgH9dHSa_pT2Q-SvgvfCcwfY0XkgUZkMPUtz85-lqchROb4XpHOiy3Cfn8MgrGNwhf-MsmN5wiAGe0DI_LW2Jxr3boZMLS4AuuNQ7agr65g-JuO9-LhlgndxN8g",  "expires_in": 300,  "refresh_expires_in": 1800,  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmNGEwNWQxNy0yNWU4LTRjMjEtOTMyMC0zMzcwODlhNTg5MjQifQ.eyJleHAiOjE2MTY2NTU4NjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMjNmNDBiZWUtNmQ3Ny00ZTIxLTg0NTItNDg1NDc2OTk1ZDUyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwic3ViIjoiOTMwYjExM2YtYjQ3NS00OGQwLTk1NDEtYzJjMjYxZmViZGZkIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIn0.r4BrjwfavKFF8dst3AyRi0LTfymbSVfDKDT9KyMpmzk",  "token_type": "bearer",  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwiYXV0aF90aW1lIjowLCJqdGkiOiJiN2UwNDhmZS01ZTRjLTQxMWYtYTBjMC0xNGExYzhlOGJhYWEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg0ODQvYXV0aC9yZWFsbXMvbXlfcmVhbG0iLCJhdWQiOiJteV9jbGllbnQiLCJzdWIiOiI5MzBiMTEzZi1iNDc1LTQ4ZDAtOTU0MS1jMmMyNjFmZWJkZmQiLCJ0eXAiOiJJRCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhdF9oYXNoIjoiRlh2VzB2Z3pwd3R6N1FabEZtTFhJdyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.ZDeZg4Z-PPmn2fVm7opGLRutzDh6l8uRYqZzbqIX7wk0GhgtMHV1CW8RvDd51AuYw81WyoMyRAD_-T6ne58Rt9f5XNZZfS8xoXzTFV1xH6XigOVQH2jIHN-2VIM1IgJnteo7nuTz9zo4OXIFvEjaFHq4AXDkiq6jhThv0qPS3WrAA-MutyW8G37GM0fsCgANvlGKoWm1_1wKyeTZ0Gfug32Vf6gUikfxA9bmaS4oGYGc6lqFE6EHgtjIn0q9gNUfpEXaqpiL3mCBu9V6sJG5Rp_MOqp-aXrM9NbLTz2JTXevtClHI6qVUIoh8OXXXT98QmKrVr9Cyr9BRUrQyt0Zzg",  "not-before-policy": 0,  "session_state": "5d29d46e-b926-4d59-89f8-2436edcae4f0",  "scope": "openid profile email"}Response code: 200 (OK); Time: 114ms; Content length: 2987 bytes

Теперь проверим, что методы доступны пользователю с соответствующими правами:

GET <http://localhost:8080/api/admin>Authorization: Bearer {{auth_token}}Content-Type: application/json

В ответ получим:

GET <http://localhost:8080/api/admin>HTTP/1.1 200 ...admin infoResponse code: 200; Time: 34ms; Content length: 10 bytes

Авторизация вызовов сервисов с использованием keycloak

При работе с микросервисной архитектурой иногда возникают требования авторизованных вызовов между сервисами. В случаях, когда инициатором взаимодействия является какой-то внутренний процесс или служба, нам где-то нужно брать токен доступа. В качестве решения данного вопроса мы можем использовать Client Credentials Flow, чтобы получить токен из keycloak (исходный код примера доступен по ссылке).

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

Для возможности авторизации сервиса нам нужно изменить тип доступа ("Access Type") на "confidential" и включить флаг "Service accounts Enabled". В остальном конфигурация не отличается от конфигурации по умолчанию:

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

Далее эту роль необходимо добавить клиенту. На вкладке "Service Account Roles" выбираем необходимую роль - в нашем случае роль "SERVICE":

Сохраняем client_id и client_secret для дальнейшего использования в сервисах для авторизации:

Для демонстрации создадим небольшое приложение, которое будет получать информацию доступную по адресу http://localhost:8080/api/service из предыдущих примеров.

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

@Componentpublic class KeycloakAuthClient {    private static final Logger log = LoggerFactory            .getLogger(KeycloakAuthClient.class);    private static final String TOKEN_PATH = "/token";    private static final String GRANT_TYPE = "grant_type";    private static final String CLIENT_ID = "client_id";    private static final String CLIENT_SECRET = "client_secret";    public static final String CLIENT_CREDENTIALS = "client_credentials";    @Value("${app.keycloak.auth-url:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect}")    private String authUrl;    @Value("${app.keycloak.client-id:service_client}")    private String clientId;    @Value("${app.keycloak.client-secret:acb719cf-4afd-42d3-91f2-93a60b3f2023}")    private String clientSecret;    private final RestTemplate restTemplate;    public KeycloakAuthClient(RestTemplate restTemplate) {        this.restTemplate = restTemplate;    }    public KeycloakAuthResponse authenticate() {        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();        paramMap.add(CLIENT_ID, clientId);        paramMap.add(CLIENT_SECRET, clientSecret);        paramMap.add(GRANT_TYPE, CLIENT_CREDENTIALS);        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);        String url = authUrl + TOKEN_PATH;        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(paramMap, headers);        log.info("Try to authenticate");        ResponseEntity<KeycloakAuthResponse> response =                restTemplate.exchange(url,                        HttpMethod.POST,                        entity,                        KeycloakAuthResponse.class);        if (!response.getStatusCode().is2xxSuccessful()) {            log.error("Failed to authenticate");            throw new RuntimeException("Failed to authenticate");        }        log.info("Authentication success");        return response.getBody();    }}

Метод authenticate производит вызов к keycloak и в случае успешного ответа возвращает объект KeycloakAuthResponse:

public class KeycloakAuthResponse {    @JsonProperty("access_token")    private String accessToken;    @JsonProperty("expires_in")    private Integer expiresIn;    @JsonProperty("refresh_expires_in")    private Integer refreshExpiresIn;    @JsonProperty("refresh_token")    private String refreshToken;    @JsonProperty("token_type")    private String tokenType;    @JsonProperty("id_token")    private String idToken;    @JsonProperty("session_state")    private String sessionState;    @JsonProperty("scope")    private String scope;    // Getters and setters or lombok ...}

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

@SpringBootApplicationpublic class DemoServiceAuthApplication implements CommandLineRunner {    private static final String BEARER = "Bearer ";    private static final String SERVICE_INFO_URL = "http://localhost:8080/api/service";    private final KeycloakAuthClient keycloakAuthClient;    private final RestTemplate restTemplate;    private static final Logger log = LoggerFactory            .getLogger(DemoServiceAuthApplication.class);    public DemoServiceAuthApplication(KeycloakAuthClient keycloakAuthClient, RestTemplate restTemplate) {        this.keycloakAuthClient = keycloakAuthClient;        this.restTemplate = restTemplate;    }    public static void main(String[] args) {        SpringApplication.run(DemoServiceAuthApplication.class, args);    }    @Override    public void run(String... args) {        final KeycloakAuthResponse authenticate = keycloakAuthClient.authenticate();        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_JSON);        headers.setBearerAuth(authenticate.getAccessToken());        log.info("Make request to resource server");        final ResponseEntity<String> responseEntity = restTemplate.exchange(SERVICE_INFO_URL, HttpMethod.GET, new HttpEntity(headers), String.class);        if (!responseEntity.getStatusCode().is2xxSuccessful()) {            log.error("Failed to request");            throw new RuntimeException("Failed to request");        }        log.info("Response data: {}", responseEntity.getBody());    }}

Сначала мы авторизуем наш сервис через keycloak, потом производим запрос к защищенному ресурсу, добавив в HTTP Headers параметр Authorization: Bearer ...

В результате выполнения программы мы получим содержимое защищенного метода:

.   ____          _            __ _ _ /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )  '  |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot ::                (v2.4.4)2021-04-13 16:04:36.672  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Starting DemoServiceAuthApplication using Java 14.0.1 on MacBook-Pro.local with PID 19240 (/Users/akazakov/Projects/spring-boot-keycloak/demo-service-auth/target/classes started by akazakov in /Users/akazakov/Projects/spring-boot-keycloak)2021-04-13 16:04:36.674  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : No active profile set, falling back to default profiles: default2021-04-13 16:04:37.199  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Started DemoServiceAuthApplication in 0.814 seconds (JVM running for 6.425)2021-04-13 16:04:37.203  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Try to authenticate2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Authentication success2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Make request to resource server2021-04-13 16:04:54.088  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Response data: service infoDisconnected from the target VM, address: '127.0.0.1:57479', transport: 'socket'Process finished with exit code 0

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

Выводы

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

Спасибо за внимание!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru