Разработка backend#

Разработка/доработка KeyCloak.SE помимо основных действий (например: Добавление расширение Admin REST API) требует ряда обязательных действий:

  • Подключение к разработанной/доработанной функциональности событий (events);

  • Написание документации - JavaDoc;

  • Проведение тестирования разработчиками на этапе кодирования приложения - UnitTesting;

  • Проведение разработки с использованием SonarQube.

JS Аутентификаторы#

JS Scripts в KeyCloak.SE позволяются осуществлять доработку продукта с целью создания аутентификаторов без пересборки исходного дистрибутива.

Включение проверки СНИЛС из клиентского сертификата при аутентификации через ЕСИА#

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

Для этого необходимо перейти по \keycloak\standalone\configuration\profile.properties и в этом файле добавить следующий блок кода (если файл отсутствует - его необходимо создать):

profile.properties

# Enable JavaScript
feature.scripts=enabled
# Enable editing JavaScript based Components via Admin-Console
feature.upload_scripts=enabled

Далее необходимо разработать необходимый JS скрипт аутентификатора. В рамках указанной задачи прикладываем готовый скрипт:

Проверка СНИЛС из клиентского сертификата при аутентификации через ЕСИА

AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
 
HEADER_PARAM = "X-Test";              // параметр HTTP хедера, где передаются данные о кл. сертификате. Заменить на свое значение!!
EMAIL_KEY_PARAM = "S.E=";
SNILS_KEY_PARAM = "S.SNILS=";
DELIMETER = "; ";                               // параметр разделителя данных кл. сертификата. Заменить на свое значение!!
KEY_VALUE_DELIMETER = '=';
 
function authenticate(context) {
    var clientCertData = httpRequest.getHttpHeaders().getHeaderString(HEADER_PARAM) || "";
 
    LOG.debug(script.name + " trace auth: http header value = " + clientCertData);
     
    var certParams = clientCertData.split(DELIMETER);
     
    if (certParams) {
        var foundUser = getUserBySnils(certParams);      // аутентифкация по СНИЛС из клиентского сертификата.
        //var foundUser = getUserByEmail(certParams);  // аутентифкация по email из клиентского сертификата.
         
        if (foundUser) {
            LOG.debug(script.name + " trace auth: found user id = " + foundUser.id);
             
            var username = foundUser ? foundUser.username : "anonymous";
         
            LOG.debug(script.name + " trace auth: username of found user: " + username);
 
            context.success();
             
            LOG.info(script.name + ": SNILS authentication from client certificate is PASSED");
            return;
        }
    }
     
    context.failure(AuthenticationFlowError.INVALID_USER);
}
 
function getUserBySnils(clientCertData) {
     
    var snils = clientCertData.filter(function(x) {
            return x.startsWith(SNILS_KEY_PARAM);
        }).toString().split(KEY_VALUE_DELIMETER)[1];
    LOG.debug(script.name + " trace auth: extracted snils from client certificate = " + snils);
         
    var foundedUsers = session.users().searchForUserByUserAttribute("snils", snils, realm);
    var count = foundedUsers ? foundedUsers.length : 0;
     
    if (count > 0) {
        return foundedUsers[0];  // возвращаем первого пользователя из списка найденных пользователей с указанным СНИЛС.
    }
     
    return undefined;
}
 
function getUserByEmail(clientCertData) {
    var email = clientCertData.filter(function(x) {
            return x.startsWith(EMAIL_KEY_PARAM);
        }).toString().split(KEY_VALUE_DELIMETER)[1];
    LOG.debug(script.name + " trace auth: extracted email from client certificate = " + email);
     
    return session.users().getUserByEmail(email, realm);
}

Далее рассмотрим процесс настройки указанной функциональности. Для этого необходимо открыть Admin Console и отредактировать flow постлогина через ЕСИА, после чего добавить JS аутентификатор (нажать "добавить исполнение", выбрать поставщик "Script")

Создание JS аутентификатора

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

Заполнение данных JS аутентификатора

Выставить первый порядок у JS аутентификатора, требование в REQUIRED.

Корректировка flow

После этой настройки при каждой аутентификации через ЕСИА будет дополнительно сверяться СНИЛС из клиентского сертификата, переданного в HTTP заголовке с данными из УЗ KeyCloak.SE.

Добавление/расширение Admin Rest API#

Написание нового модуля#

Для доработки или добавления функциональности в KeyCloak.SE существуют SPI (Service Provider Interfaces). Он состоит из интерфейсов ProviderFactory и Provider, а так же конфигурационного файла.

При реализации функциональности следует использовать существующие SPI (например, RealmResourceSPI) или реализовывать SPI самостоятельно. Чтобы создать собственный SPI, необходимо наследоваться от Spi класса.

RealmResourceSPI.java

public class RealmResourceSPI implements Spi {
 
    @Override
    public boolean isInternal() {
        return true;
    }
 
    @Override
    public String getName() {
        return "realm-restapi-extension";
    }
 
    @Override
    public Class<? extends Provider> getProviderClass() {
        return RealmResourceProvider.class;
    }
 
    @Override
    public Class<? extends ProviderFactory> getProviderFactoryClass() {
        return RealmResourceProviderFactory.class;
    }
}

Затем его следует прописать в конфигурационном файле с полным наименованием родительского класса (полный путь с пакетом), а в файл org.keycloak.provider. Spi записать полный путь с пакетом самого класса org.keycloak.services.resource. RealmResourceSPI. Также следует прописать ProviderFactory.

resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory

org.keycloak.examples.rest.HelloResourceProviderFactory

Provider будет инициализироваться из фабрики.

HelloResourceProviderFactory.java

public class HelloResourceProviderFactory implements RealmResourceProviderFactory {
 
    public static final String ID = "hello";
 
    @Override
    public String getId() {
        return ID;
    }
 
    @Override
    public RealmResourceProvider create(KeycloakSession session) {
        return new HelloResourceProvider(session);
    }
 
    @Override
    public void init(Scope config) {
    }
 
    @Override
    public void postInit(KeycloakSessionFactory factory) {
    }
 
    @Override
    public void close() {
    }
 
}

В Provider реализуется необходимая функциональность REST API.

HelloResourceProvider.java

public class HelloResourceProvider implements RealmResourceProvider {
 
    private KeycloakSession session;
 
    public HelloResourceProvider(KeycloakSession session) {
        this.session = session;
    }
 
    @Override
    public Object getResource() {
        return this;
    }
 
    @GET
    @Produces("text/plain; charset=utf-8")
    public String get() {
        String name = session.getContext().getRealm().getDisplayName();
        if (name == null) {
            name = session.getContext().getRealm().getName();
        }
        return "Hello " + name;
    }
 
    @Override
    public void close() {
    }
 
}

Подключение модуля#

После написания и компиляции модуля его необходимо подключить к KeyCloak.SE:

  1. Скопировать через Modules

COPY --chown=1000:jboss dependencies/modules/ /opt/keycloak/providers

  1. Установить переменные окружения

  2. Запустить kc.sh

События (events)#

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

  1. Создать кастомный CustomEventListenerProvider, который будет имплементировать org.keycloak.events.EventListenerProvider EventListenerProvider.java

public class CustomEventListenerProvider implements EventListenerProvider {
 
    @Override
    public void onEvent(Event event) {
      log.info("Example caught event", EventUtils.toString(event));
    }
 
    @Override
    public void onEvent(AdminEvent adminEvent, boolean b) {
        log.info("Example caught admin event {}", EventUtils.toString(adminEvent));
        }
 
    @Override
    public void close() {
 
    }
}
  1. У данного интерфейса необходимо определить три метода:

    1. onEvent метод, перехватывающий обычные события в системе, такие как событие неправильного ввода пароля;

    2. onAdminEvent перехватывает события администратора, например: событие сброса пароля пользователя через консоль администратора Keycloak;

    3. close своего рода деструктор, вызывается при удалении текущего провайдера.

  2. Создать собственную фабрику CustomEventListenerProviderFactory, которая имплементирует org.keycloak.events.EventListenerProviderFactory

CustomEventListenerProviderFactory.java

public class CustomEventListenerProviderFactory implements EventListenerProviderFactory {
 
    private static final String LISTENER_ID = "event-listener-extension";
 
    @Override
    public EventListenerProvider create(KeycloakSession session) {
        return new CustomEventListenerProvider();
    }
 
    @Override
    public void init(Config.Scope scope) {
 
    }
 
    @Override
    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
 
    }
 
    @Override
    public void close() {
 
    }
 
    @Override
    public String getId() {
        return LISTENER_ID;
    }
 
}

Здесь необходимо переопределить пять методов:

  • create будет возвращать наш кастомный провайдер CustomEventListenerProvider. Вызывается при каждом новом событии в системе.

  • init срабатывает только один раз при первом создании фабрики.

  • postInit вызывается один раз после инициализации всех фабрик провайдеров в системе.

  • close выполняется при завершении работы Keycloak.

  • getId устанавливает название нашего расширения при создании фабрики.

Здесь описано только минимальное расширение для отлавливания событий в Keycloak, вы же можете делать с ними все, что вам необходимо.

JavaDoc#

Javadoc — это инструмент, который поставляется с JDK и используется для создания документации кода Java в формате HTML из исходного кода Java, для чего требуется документация в заранее определенном формате.

Javadoc генерируется с помощью так называемого «доклета». Различные доклеты могут по-разному анализировать аннотации Java и создавать разные выходные данные. Но по большому счету почти каждая документация по Java использует стандартный доклет.

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

Unit testing#

Unit Testing – это тип тестирования программного обеспечения, при котором тестируются отдельные модули или компоненты программного обеспечения. Его цель заключается в том, чтобы проверить, что каждая единица программного кода работает должным образом. Данный вид тестирование выполняется разработчиками на этапе кодирования приложения. Модульные тесты изолируют часть кода и проверяют его работоспособность. Единицей для измерения может служить отдельная функция, метод, процедура, модуль или объект.

Документация по одному из самых популярных фреймворком JUnit: https://junit.org/junit5/docs/current/user-guide/

Sonarqube#

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

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

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

Разработка frontend (FTL)#

Построение тем keycloak основано на шаблонизаторе FreeMarker + AngularJS.

Шаблоны FreeMarker имеют расширение .ftl. Синтасис разметки фактически является синтаксисом html расширенным новыми тегами и конструкциями. Freemarker интегрируется с java и позволяет использовать интерполяцию java переменных.

Темы#

Создание темы#

За кастомные темы Keycloak отвечает модуль kcse-platform-v-theme.

Принцип создания новой темы - переопределение файлов базовой темы. Переопределенные компоненты содержатся в директории theme/mytheme по аналогии с темой platform-v. Кроме того кастомную тему необходимо объявить в файле META-INF/keycloak-theme.json в массиве themes

{
    "themes": [{
        "name": "platform-v",
        "types": [
            "admin",
            "account",
            "login",
            "email",
            "welcome"
        ]
    }]
}

Добавление органов управления на страницу#

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

Так, например, для добавления новых блоков на страницу Роли > Роли по умолчанию (Roles > Default roles) следует скопировать из базовой темы страницу realm-default-roles.html и положить в папку кастомной темы по аналогичному оригиналу пути theme/admin/resources/partials. Теперь страницу можно отредактировать. Затем пересобрать модуль. Для просмотра изменений необходимо помнить о кэшировании страниц в браузере.

Частые ошибки

Если изменения не отображаются

  • Отключите или сбросьте кэш браузера

  • Для standalone-версии приложения убедитесь, что тема развернулась. Рядом с собранным .jar в папке /standalone/deployments должен появиться файл с расширением .deployed

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

Локализация#

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

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

messages_en.properties

# custom messages
readFromTM_domain_label=Domain for read TM

В KeyCloak.SE реализована данная возможность при помощи соответствующих файлов:

directory