OpenID Connect

Дополнение Jmix OpenID Connect предоставляет предопределенные конфигурации Spring Security и набор сервисов, которые позволяют легко реализовать в приложении следующие возможности:

  • Аутентификация пользователя с использованием внешнего поставщика OpenID (например, Keycloak).

  • Маппинг атрибутов и ролей пользователя из поставщика OpenID на пользователя Jmix.

  • Сохранение сущности пользователя и назначений ролей после успешной аутентификации пользователя поставщиком OpenID.

Дополнение использует поддержку Spring Security для OAuth2 и OpenID Connect 1.0. Вы можете прочитать об этом в документации Spring Security.

Дополнение применяет конфигурацию OidcAutoConfiguration, если ее явно не отключить, установив свойство приложения jmix.oidc.use-default-configuration=false. Данная конфигурация включает аутентификацию OpenID Connect для URL пользовательского интерфейса и REST API.

См. также руководство REST DataStore with External Authentication для ознакомления с практическими примерами применения дополнения.

Установка

Для автоматической установки через Jmix Marketplace следуйте инструкциям в разделе Add-ons.

Для ручной установки добавьте следующую зависимость в свой build.gradle:

implementation 'io.jmix.oidc:jmix-oidc-starter'

Конфигурация клиента

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

Для настройки клиента используется стандартный подход Spring Security. Например, это можно сделать, добавив следующие свойства в файл application.properties:

spring.security.oauth2.client.registration.keycloak.client-id=<client-id>
spring.security.oauth2.client.registration.keycloak.client-secret=<client-secret>
spring.security.oauth2.client.registration.keycloak.scope=openid, profile, email
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/<realm>
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/<realm>

keycloak в ключе свойства - это идентификатор провайдера. Он может иметь любое значение, например, okta, тогда свойство будет spring.security.oauth2.client.registration.okta.client-id.

Значения идентификатора клиента (Client ID) и секрета (Client Secret) должны быть взяты из поставщика OpenID.

Свойство issuer-uri содержит путь к поставщику OpenID Configuration Endpoint.

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

spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

Использование стандартной конфигурации дополнения

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

  • Неаутентифицированные пользователи будут перенаправлены на страницу входа поставщика OpenID.

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

  • Коллекция кодов ролей пользователя будет взята из клэйма "roles" токена и, затем, для каждой роли из значения клэйма, ресурсная роль и роль на уровне строк будут установлены в объекте аутентификации пользователя.

Класс DefaultJmixOidcUser реализует интерфейс JmixOidcUser. Класс пользователя должен всегда реализовывать этот интерфейс, потому что Jmix-приложения требуют интерфейс UserDetails, а Spring Security работает с интерфейсом OidcUser. JmixOidcUser просто расширяет оба эти интерфейса.

Сопоставление атрибутов и ролей пользователя

Если вам нужно работать с пользователем в памяти, но вы хотите заполнить некоторые атрибуты пользователя, создайте класс, который расширяет DefaultJmixOidcUser. В приведенном ниже примере у него есть атрибут position:

import io.jmix.oidc.user.DefaultJmixOidcUser;

public class MyUser extends DefaultJmixOidcUser {

    private String position;

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }
}

Затем вам нужно зарегистрировать экземпляр OidcUserMapper как бин Spring. Вы можете расширить BaseOidcUserMapper и переопределить его методы:

import examples.oidcex1.entity.MyUser;
import io.jmix.oidc.claimsmapper.ClaimsRolesMapper;
import io.jmix.oidc.usermapper.BaseOidcUserMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class MyOidcUserMapper extends BaseOidcUserMapper<MyUser> {

    private ClaimsRolesMapper claimsRolesMapper;

    public MyOidcUserMapper(ClaimsRolesMapper claimsRolesMapper) {
        this.claimsRolesMapper = claimsRolesMapper;
    }

    @Override
    protected String getOidcUserUsername(OidcUser oidcUser) {
        return oidcUser.getName();
    }

    @Override
    protected MyUser initJmixUser(OidcUser oidcUser) {
        return new MyUser();
    }

    @Override
    protected void populateUserAttributes(OidcUser oidcUser, MyUser jmixUser) {
        jmixUser.setPosition((String) oidcUser.getClaims().get("position"));
        jmixUser.setDelegate(oidcUser);
    }

    @Override
    protected void populateUserAuthorities(OidcUser oidcUser, MyUser jmixUser) {
        Collection<? extends GrantedAuthority> authorities = claimsRolesMapper.toGrantedAuthorities(oidcUser.getClaims());
        jmixUser.setAuthorities(authorities);
    }
}
При реализации пользовательского маппера OIDC убедитесь, что пользователь Jmix сохраняет исходные данные пользователя OIDC, включая ID токен. ID токен необходим для обработки выхода из системы. Если ваш класс пользователя расширяет DefaultJmixOidcUser или JmixOidcUserEntity, вызовите setDelegate(oidcUser), как показано в примере выше.

Обратите внимание, что в приведенном выше примере сопоставление клэймов от пользователя OIDC к полномочиям (granted authorities) Jmix делегируется интерфейсу ClaimsRolesMapper. Реализацией ClaimsRolesMapper по умолчанию является DefaultClaimsRolesMapper, который получает клэйм с именем "roles" из ID токена. Этот клэйм должен содержать коллекцию имен ролей. Затем для каждой роли из значения клэйма выполняется поиск ресурсной роли и роли уровня строк в Jmix. Если они найдены, соответствующие полномочия будут добавлены пользователю. Имя клэйма для ролей можно настроить с помощью следующего свойства приложения:

jmix.oidc.default-claims-roles-mapper.roles-claim-name=myRoles

При необходимости вы можете создать свой маппер клэймов в роли. Самый простой способ сделать это - расширить BaseClaimsRolesMapper и переопределить его методы getResourceRolesCodes() и/или getRowLevelRolesCodes(). Приведенный ниже пример демонстрирует, как назначать роли на основе клэйма "position":

import io.jmix.oidc.claimsmapper.BaseClaimsRolesMapper;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

@Component
public class MyClaimsRoleMapper extends BaseClaimsRolesMapper {

    public MyClaimsRoleMapper(ResourceRoleRepository resourceRoleRepository,
                              RowLevelRoleRepository rowLevelRoleRepository,
                              RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
        super(resourceRoleRepository, rowLevelRoleRepository, roleGrantedAuthorityUtils);
    }

    @Override
    protected Collection<String> getResourceRolesCodes(Map<String, Object> claims) {
        Collection<String> jmixRoleCodes = new HashSet<>();
        String position = (String) claims.get("position");
        if ("Manager".equals(position)) {
            jmixRoleCodes.add("edit-contracts");
            jmixRoleCodes.add("view-archive");
        } else {
            jmixRoleCodes.add("view-contracts");
        }
        return jmixRoleCodes;

    }
}

Работа с JPA-сущностью пользователя

Для работы с дополнением Jmix OIDC, JPA-сущность User должна реализовывать интерфейс io.jmix.oidc.user.JmixOidcUser, который, в свою очередь, реализует org.springframework.security.oauth2.core.oidc.user.OidcUser, требуемый Spring Security.

Простейший способ сделать сущность User совместимой с дополнением OIDC - сделать этот класс подклассом абстрактного класса io.jmix.oidc.user.JmixOidcUserEntity:

@JmixEntity
@Entity
@Table(name = "USER_", indexes = {
        @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
})
public class User extends JmixOidcUserEntity implements HasTimeZone {

    //...
}

В случае проекта на Kotlin необходимо сделать дополнительные изменения в сущности User:

  • Добавить аннотацию @get:JvmName("getEmail_") на свойство email.

  • Добавить метод override fun getEmail(): String? = email

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

import examples.oidcex1.entity.User;
import io.jmix.core.UnconstrainedDataManager;
import io.jmix.core.security.UserRepository;
import io.jmix.oidc.claimsmapper.ClaimsRolesMapper;
import io.jmix.oidc.usermapper.SynchronizingOidcUserMapper;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;

@Component
public class MySynchronizingOidcUserMapper extends SynchronizingOidcUserMapper<User> {

    public MySynchronizingOidcUserMapper(UnconstrainedDataManager dataManager,
                                         UserRepository userRepository,
                                         ClaimsRolesMapper claimsRolesMapper,
                                         RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
        super(dataManager, userRepository, claimsRolesMapper, roleGrantedAuthorityUtils);

        //store role assignments in the database (false by default)
        setSynchronizeRoleAssignments(true);
    }

    @Override
    protected Class<User> getApplicationUserClass() {
        return User.class;
    }

    @Override
    protected void populateUserAttributes(OidcUser oidcUser, User jmixUser) {
        jmixUser.setUsername(oidcUser.getName());
        jmixUser.setFirstName(oidcUser.getGivenName());
        jmixUser.setLastName(oidcUser.getFamilyName());
        jmixUser.setEmail(oidcUser.getEmail());
        jmixUser.setDelegate(oidcUser);
    }
}

Защита API

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

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/<realm>

Значение свойства - это URL, содержащийся в клэйме iss для JWT-токенов, которые авторизационный сервер будет выдавать. См. документацию Spring Security для получения дополнительной информации.

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

jmix.oidc.jwt-authentication-converter.username-claim=preferred_username

В большинстве случаев значение свойства должно согласовываться со значением свойства spring.security.oauth2.client.provider.keycloak.user-name-attribute.

Токены доступа, полученные от поставщика OpenID, могут использоваться для доступа к защищенным конечным точкам, предоставляемым дополнением REST API.

Для локального экземпляра Keycloak токены доступа можно получить следующим образом:

curl -X POST http://localhost:8180/realms/sample1/protocol/openid-connect/token \
--user <client-id>:<client-secret> \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&scope=openid&username=<username>&password=<password>"

Например:

curl -X POST http://localhost:8180/realms/sample1/protocol/openid-connect/token \
--user jmix-app:UONXQZf6unxVuWsxXvhMAPv5IxFz5P7D \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&&scope=openid&username=johndoe&password=mypass"

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    @GetMapping("/authenticated/hello")
    public String authenticatedHello() {
        return "authenticated-hello";
    }

    @GetMapping("/anonymous/hi")
    public String anonymousHello() {
        return "anonymous-hi";
    }
}

Представьте, что вам нужно защитить все URL, начинающиеся с /authenticated/, и сделать все URL, начинающиеся с /anonymous/, доступными для анонимного доступа. Это можно сделать несколькими способами. Простейший подход - использовать свойства приложения:

# All endpoints that match the given pattern will require a bearer token
jmix.resource-server.authenticated-url-patterns = /authenticated/**

# However, endpoints that match the following pattern will be accessible without a token
jmix.resource-server.anonymous-url-patterns = /anonymous/**

Свойства OIDC

jmix.oidc.filter-chain.force-api-scope-enabled

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

Значение по умолчанию: true

jmix.oidc.filter-chain.force-ui-scope-enabled

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

Значение по умолчанию: true

jmix.oidc.use-default-configuration

Определяет, применять ли конфигурацию по умолчанию. По умолчанию установлено значение true. Установите это свойство в false, если вы хотите иметь доступ к бинам и интерфейсам дополнения, но не хотите использовать предварительно определенную конфигурацию Spring Security для защиты конечных точек. В этом случае вам придется написать свою собственную конфигурацию безопасности.

jmix.oidc.use-default-configuration = false

jmix.oidc.default-claims-roles-mapper.roles-claim-name

Определяет имя клэйма в токене ID, которое содержит коллекцию названий ролей. Это свойство используется классом DefaultClaimsRolesMapper. Значение по умолчанию - roles.

jmix.oidc.default-claims-roles-mapper.roles-claim-name = myRoles

Настройка локального экземпляра Keycloak

Один из самых популярных поставщиков OpenID - Keycloak. Для ознакомления с дополнением Jmix OIDC вы можете запустить Keycloak локально с помощью Docker.

Запуск Keycloak с использованием Docker

Используйте следующую команду для запуска экземпляра Keycloak с помощью Docker на порту 8180:

docker run -p 8180:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin --name keycloak quay.io/keycloak/keycloak:22.0 start-dev

См. документацию Keycloak для получения дополнительной информации.

URL Keycloak: http://localhost:8180

Административные учетные данные:

Имя пользователя: admin
Пароль: admin

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

Создание Realm

Войдите в консоль администратора Keycloak.

Откройте выпадающее окно вверху левой панели.

Нажмите Create Realm.

create realm 1

Дайте имя новому Realm, например, "sample".

create realm 2

Создание клиента

Чтобы подключить приложение Jmix к Keycloak, вам нужно создать нового клиента jmix-app с типом клиента OpenID Connect.

create client 1

Включите Client authentication.

create client 2

Введите Valid redirect URIs:

http://localhost:8080/*

и Web origins:

http://localhost:8080
create client 3

Откройте только что созданный клиент и перейдите на вкладку Credentials. Там отображается Client secret, который вам понадобится в проекте Jmix для настройки подключения.

client credentials

Параметры клиента должны использоваться в файле application.properties. См. раздел Настройка клиента.

Создание роли

Затем вам следует создать новую роль в Realm. По умолчанию имя роли должно совпадать с кодом роли Jmix. Создайте роль system-full-access:

create role

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

Создайте пользователя с именем пользователя johndoe:

create user

После сохранения пользователя появится вкладка Credentials. Там вы можете установить пароль пользователя.

create user credentials

На вкладке Role mappings назначьте роль system-full-access:

assign role

Если вы хотите заполнить атрибуты пользователя (например, "должность"), то можете сделать это на вкладке Attributes в редакторе пользователя.

Создание маппера ролей

Чтобы вернуть информацию о ролях Realm в токене ID, вам нужно определить маппер для клиента jmix-app. Откройте редактор клиента и перейдите на вкладку Client scopes:

create mapper 1

Откройте редактор области jmix-app-dedicated. Добавьте предопределенный маппер для "realm roles":

create mapper 2

Откройте только что созданный маппер "realm roles" Измените значение атрибута Token Claim Name на roles и выберите флажок Add to userinfo. Таким образом вы указываете, что клэйм, содержащий список ролей пользователя, будет называться roles.

create mapper 3