REST DataStore with External Authentication

This guide shows how to use the Jmix REST DataStore together with external authentication, using Keycloak as an OpenID Connect (OIDC) identity provider.

In distributed systems, it’s common for applications to authenticate users centrally but share data and logic between services. With Jmix, you can build secure applications where user data and access control are managed externally, while services communicate through REST APIs and forward user tokens for authentication.

You’ll learn how to:

  • Set up a Keycloak server for managing users, roles, and tokens.

  • Configure Jmix applications to authenticate users via Keycloak.

  • Implement user synchronization between Keycloak and Jmix.

  • Use the REST DataStore to access data from another application on behalf of the logged-in user.

The guide covers two architectural patterns:

  • Integrated Applications: Both the Client and Service applications have UIs and run independently, communicating through REST with token-based authentication.

  • Separate Tiers: The UI-only Frontend application retrieves data from a Backend service via REST, with all authentication handled through Keycloak.

By the end of this guide, you’ll have a working example of both scenarios and understand the key concepts behind integrating REST DataStore with external authentication in a secure and maintainable way.

Prerequisites

Get Sample Project

  1. Setup the development environment.

  2. Clone the sample project and switch to release_2_5 branch:

    git clone https://github.com/jmix-framework/jmix-restds-oidc-sample
    cd jmix-restds-oidc-sample
    git checkout release_2_5

Set Up Keycloak

  1. Run Keycloak in a Docker container:

    docker run -p 8180:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin --name=keycloak quay.io/keycloak/keycloak:26.1 start-dev
  2. Log in to the Keycloak admin console and create a new realm called jmix-restds-oidc-sample.

  3. Create the following clients in this realm:

    • integrated-apps-client

      • Capability config tab:

        • Client authentication: On

      • Login settings tab:

        • Valid redirect URIs: http://localhost:8080/*, http://localhost:8081/*

        • Web origins: +

    • integrated-apps-service

      • Capability config tab:

        • Client authentication: On

      • Login settings tab:

        • Valid redirect URIs: http://localhost:8080/*, http://localhost:8081/*

        • Web origins: +

    • separate-tiers-frontend

      • Capability config tab:

        • Client authentication: On

      • Login settings tab:

        • Valid redirect URIs: http://localhost:8080/*

        • Web origins: +

  4. For each client, switch to the Client scopes tab and open the [client-name]-dedicated scope. Add predefined mapper for realm roles. Open the created realm roles mapper and set the following values:

    • Token Claim Name: roles

    • Add to userinfo: On

  5. Create two realm roles: system-full-access and employee.

  6. Create two users: alice and bob. Fill in the Email, First name and Last name attributes. Set user passwords on the Credentials tab.

  7. On the Role mapping tab, assign system-full-access role to alice and employee role to bob. Use Filter by realm roles to find the roles.

Integrated Applications Example

In this example, the distributed system consists of two applications: Client and Service.

Both applications have their own databases and UI, and use the Jmix OpenID Connect add-on to enable user authentication through Keycloak. The Client application accesses customer data through the REST DataStore.

integrated apps 1
Figure 1. Integrated Applications Overview

The Service application includes the Customer JPA entity and customer management views. The Client application has a corresponding Customer DTO entity and related views.

integrated apps 3
Figure 2. Data Models of the Applications

When the Client’s REST DataStore makes a request to the Service’s REST API, it includes the authorization token of the current user obtained from Keycloak during login. Thus, when executing REST requests, the Service code acts on behalf of the user who is logged into the Client.

integrated apps 2
Figure 3. Client/Service Integration Flow

Each application has two roles: system-full-access and employee.

The employee role in Service application provides access only to the Customer entity and related views. In the Client application, this role also enables managing the Order entity.

According to the Keycloak setup, alice will be granted the system-full-access role and bob will be granted the employee role.

Example in Action

  1. Open the root project in IntelliJ IDEA with the Jmix Studio plugin installed.

    Launch the integrated-apps-client and integrated-apps-service applications using their run/debug configurations.

    The applications are configured to run on different ports: Client on 8080, Service on 8081.

  2. Open the Client application by navigating to http://localhost:8080 in your web browser. You will be redirected to the Keycloak login page.

    Log in as bob with the password that you have set in Keycloak. You will be able to manage customers in Customer (DTO) views.

    Log in as alice. Open the list of users and make sure it contains both bob and alice users with data replicated from Keycloak.

  3. Open the Service application by navigating to http://localhost:8081 in your web browser. You will be redirected to the Keycloak login page.

    Log in as alice. You will be able to manage customers and see the list of users with data replicated from Keycloak.

Service Application Details

  1. The Service application includes the REST API and OpenID Connect add-ons:

    integrated-apps/service-app/build.gradle
    implementation 'io.jmix.rest:jmix-rest-starter'
    implementation 'io.jmix.oidc:jmix-oidc-starter'
  2. To run on a specific port and prevent cookie conflicts, the Service application requires the following properties:

    integrated-apps/service-app/src/main/resources/application.properties
    server.port=8081
    server.servlet.session.cookie.name=SERVICEAPP_JSESSIONID
  3. Spring Security OAuth2 properties provide integration with Keycloak for authenticating users in the UI and validating tokens sent by the Client in REST API requests:

    integrated-apps/service-app/src/main/resources/application.properties
    spring.security.oauth2.client.registration.keycloak.client-id=integrated-apps-service
    spring.security.oauth2.client.registration.keycloak.client-secret=uZXD0xfHn594ncTKQR5SHhTBFTKGmR8T
    spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,offline_access
    spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
    
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
  4. The REST API endpoints are secured by specifying the following property:

    integrated-apps/service-app/src/main/resources/application.properties
    jmix.resource-server.authenticated-url-patterns=/rest/**
  5. In order to bypass the Spring Boot’s default login page and go directly to Keycloak on login, the application includes the following property and customized security configuration:

    integrated-apps/service-app/src/main/resources/application.properties
    jmix.oidc.use-default-ui-configuration=false
    integrated-apps/service-app/src/main/java/com/company/serviceapp/security/AppSecurityConfiguration.java
    package com.company.serviceapp.security;
    
    import io.jmix.oidc.OidcVaadinWebSecurity;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    
    @Configuration
    public class AppSecurityConfiguration extends OidcVaadinWebSecurity {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
    
            http.oauth2Login(oauth2Login -> oauth2Login
                    .loginPage("/oauth2/authorization/keycloak")
                    .defaultSuccessUrl("/", true));
        }
    }
  6. For compatibility with Jmix OIDC add-on, the User entity extends the JmixOidcUserEntity class:

    integrated-apps/service-app/src/main/java/com/company/serviceapp/entity/User.java
    @JmixEntity
    @Entity
    @Table(name = "USER_", indexes = {
            @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
    })
    public class User extends JmixOidcUserEntity implements HasTimeZone {
  7. AppUserMapper bean performs synchronization of User entity instances with the user information from Keycloak:

    integrated-apps/service-app/src/main/java/com/company/serviceapp/security/AppUserMapper.java
    package com.company.serviceapp.security;
    
    import com.company.serviceapp.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.security.oauth2.core.oidc.user.OidcUser;
    import org.springframework.stereotype.Component;
    
    @Component
    public class AppUserMapper extends SynchronizingOidcUserMapper<User> {
    
        public AppUserMapper(UnconstrainedDataManager dataManager, UserRepository userRepository, ClaimsRolesMapper claimsRolesMapper, RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
            super(dataManager, userRepository, claimsRolesMapper, roleGrantedAuthorityUtils);
        }
    
        @Override
        protected Class<User> getApplicationUserClass() {
            return User.class;
        }
    
        @Override
        protected void populateUserAttributes(OidcUser oidcUser, User jmixUser) {
            jmixUser.setUsername(getOidcUserUsername(oidcUser));
            jmixUser.setFirstName(oidcUser.getGivenName());
            jmixUser.setLastName(oidcUser.getFamilyName());
            jmixUser.setEmail(oidcUser.getEmail());
        }
    
        @Override
        protected String getOidcUserUsername(OidcUser oidcUser) {
            return oidcUser.getPreferredUsername();
        }
    }

Client Application Details

  1. The Client application includes the REST DataStore and OpenID Connect add-ons:

    integrated-apps/client-app/build.gradle
    implementation 'io.jmix.restds:jmix-restds-starter'
    implementation 'io.jmix.oidc:jmix-oidc-starter'
  2. Spring Security OAuth2 properties provide integration with Keycloak for authenticating users in the UI:

    integrated-apps/client-app/src/main/resources/application.properties
    spring.security.oauth2.client.registration.keycloak.client-id=integrated-apps-client
    spring.security.oauth2.client.registration.keycloak.client-secret=DmkKz9syYW5OAptnYCdXKnGTD5kDoMRm
    spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,offline_access
    spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
    
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
  3. The Client application includes the same configuration as that of the Service application described in items 5, 6, and 7 of the previous section:

    • jmix.oidc.use-default-ui-configuration=false property and AppSecurityConfiguration class that configures OAuth2 login page.

    • User entity extends JmixOidcUserEntity for compatibility with OIDC add-on.

    • AppUserMapper class that performs synchronization of User entity instances with the user information from Keycloak.

  4. The serviceapp REST DataStore is configured as follows:

    integrated-apps/client-app/src/main/resources/application.properties
    jmix.core.additional-stores=serviceapp
    jmix.core.store-descriptor-serviceapp=restds_RestDataStoreDescriptor
    
    serviceapp.baseUrl=http://localhost:8081
  5. To use the OAuth2 token of the current user when making requests to the Service’s REST API, the Client application defines the following bean that implements the RestAuthenticator interface:

    integrated-apps/client-app/src/main/java/com/company/clientapp/security/RestOidcAuthenticator.java
    package com.company.clientapp.security;
    
    import io.jmix.restds.impl.RestAuthenticator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.context.annotation.Scope;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
    import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
    import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    @Component
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public class RestOidcAuthenticator implements RestAuthenticator {
    
        @Autowired
        private OAuth2AuthorizedClientManager authorizedClientManager;
    
        @Override
        public void setDataStoreName(String name) {
        }
    
        @Override
        public ClientHttpRequestInterceptor getAuthenticationInterceptor() {
            return new AuthenticatingClientHttpRequestInterceptor();
        }
    
        private String getAccessToken() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null) {
                throw new IllegalStateException("Cannot get access token: Authentication object is null");
            }
    
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("keycloak")
                    .principal(authentication)
                    .build();
    
            OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
            if (authorizedClient == null) {
                throw new IllegalStateException("Cannot authorize " + authorizeRequest);
            }
            return authorizedClient.getAccessToken().getTokenValue();
        }
    
        private class AuthenticatingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
                request.getHeaders().setBearerAuth(getAccessToken());
                return execution.execute(request, body);
            }
        }
    }

    This bean’s name is specified in the [restds-name].authenticator property:

    integrated-apps/client-app/src/main/resources/application.properties
    serviceapp.authenticator=restOidcAuthenticator

Separate Tiers Example

In this example, the distributed system comprises the Backend and Frontend applications.

The Backend application connects to the database and provides the REST API. The Frontend application features a user interface and retrieves data from the Backend via the REST DataStore.

Both applications use the Jmix OpenID Connect add-on for integration with Keycloak: the Frontend authenticates users in UI, the Backend validates tokens sent by the Client with REST requests.

separate tiers 1
Figure 4. Separate Tiers Overview

The data models of the applications have the same structure; however, the Backend includes JPA entities, while the Frontend includes their corresponding DTO entities.

separate tiers 2
Figure 5. Data Models of the Applications

When the Frontend REST DataStore makes a request to the Backend REST API, it includes the authorization token of the current user obtained from Keycloak during login. Thus, when executing REST requests, the Backend code acts on behalf of the user who is logged into the Frontend.

separate tiers 3
Figure 6. Frontend/Backend Integration Flow

Each application has two roles: system-full-access and employee. The employee role provides access only to the Customer entity and, in the Frontend application, to the corresponding views.

According to the Keycloak setup, alice will be granted the system-full-access role and bob will be granted the employee role.

Example in Action

  1. Open the root project in IntelliJ IDEA with the Jmix Studio plugin installed.

    Launch the separate-tiers-backend and separate-tiers-frontend applications using their run/debug configurations.

    The applications are configured to run on different ports: Frontend on 8080, Backend on 8081.

  2. Open the Frontend application by navigating to http://localhost:8080 in your web browser. You will be redirected to the Keycloak login page.

    Log in as bob with the password that you have set in Keycloak. You will be able to manage customers in Customer views.

    Log in as alice. Open the list of users and make sure it contains both bob and alice users with data replicated from Keycloak.

Backend Application Details

  1. The Backend application includes the REST API and OpenID Connect add-ons:

    separate-tiers/backend-app/build.gradle
    implementation 'io.jmix.rest:jmix-rest-starter'
    implementation 'io.jmix.oidc:jmix-oidc-starter'
    
    implementation 'io.jmix.flowui:jmix-flowui-data-starter'

    While the Backend application doesn’t have a UI itself, it still needs the jmix-flowui-data-starter dependency to provide the database persistence for UI filter configurations and user settings.

  2. The Backend application runs on a non-standard port:

    separate-tiers/backend-app/src/main/resources/application.properties
    server.port=8081
  3. The following Spring Security OAuth2 property provides validation of tokens sent by the Client in REST API requests:

    separate-tiers/backend-app/src/main/resources/application.properties
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
  4. The REST API endpoints are secured by specifying the following property:

    separate-tiers/backend-app/src/main/resources/application.properties
    jmix.resource-server.authenticated-url-patterns=/rest/**
  5. For compatibility with Jmix OIDC add-on, the User entity extends the JmixOidcUserEntity class:

    separate-tiers/backend-app/src/main/java/com/company/backendapp/entity/User.java
    @JmixEntity
    @Entity
    @Table(name = "USER_", indexes = {
            @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
    })
    public class User extends JmixOidcUserEntity implements HasTimeZone {
  6. BackendUserMapper bean performs synchronization of User entity instances with the user information from Keycloak:

    separate-tiers/backend-app/src/main/java/com/company/backendapp/security/AppUserMapper.java
    package com.company.backendapp.security;
    
    import com.company.backendapp.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.security.oauth2.core.oidc.user.OidcUser;
    import org.springframework.stereotype.Component;
    
    @Component
    public class BackendUserMapper extends SynchronizingOidcUserMapper<User> {
    
        public BackendUserMapper(UnconstrainedDataManager dataManager, UserRepository userRepository, ClaimsRolesMapper claimsRolesMapper, RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
            super(dataManager, userRepository, claimsRolesMapper, roleGrantedAuthorityUtils);
        }
    
        @Override
        protected Class<User> getApplicationUserClass() {
            return User.class;
        }
    
        @Override
        protected void populateUserAttributes(OidcUser oidcUser, User jmixUser) {
            jmixUser.setUsername(getOidcUserUsername(oidcUser));
            jmixUser.setFirstName(oidcUser.getGivenName());
            jmixUser.setLastName(oidcUser.getFamilyName());
            jmixUser.setEmail(oidcUser.getEmail());
        }
    
        @Override
        protected String getOidcUserUsername(OidcUser oidcUser) {
            return oidcUser.getPreferredUsername();
        }
    }

Frontend Application Details

  1. The Client application includes the REST DataStore and OpenID Connect add-ons:

    separate-tiers/frontend-app/build.gradle
    implementation 'io.jmix.restds:jmix-restds-starter'
    implementation 'io.jmix.oidc:jmix-oidc-starter'
    
    implementation 'io.jmix.flowui:jmix-flowui-restds-starter'

    The jmix-flowui-restds-starter dependency provides implementation of UI filter configuration and user settings persistence based on REST DataStore.

  2. Spring Security OAuth2 properties provide integration with Keycloak for authenticating users in the UI:

    separate-tiers/frontend-app/src/main/resources/application.properties
    spring.security.oauth2.client.registration.keycloak.client-id=separate-tiers-frontend
    spring.security.oauth2.client.registration.keycloak.client-secret=vVDzrbhOzbbzlgx3TDhZvNcuauMemhVR
    spring.security.oauth2.client.registration.keycloak.scope=openid,profile,address,email,offline_access
    spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
    
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/jmix-restds-oidc-sample
  3. In order to bypass the Spring Boot’s default login page and go directly to Keycloak on login, the application includes the following property and customized security configuration:

    separate-tiers/frontend-app/src/main/resources/application.properties
    jmix.oidc.use-default-ui-configuration=false
    separate-tiers/frontend-app/src/main/java/com/company/frontendapp/security/AppSecurityConfiguration.java
    package com.company.frontendapp.security;
    
    import io.jmix.oidc.OidcVaadinWebSecurity;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    
    @Configuration
    public class AppSecurityConfiguration extends OidcVaadinWebSecurity {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
    
            http.oauth2Login(oauth2Login -> oauth2Login
                    .loginPage("/oauth2/authorization/keycloak")
                    .defaultSuccessUrl("/", true));
        }
    }
  4. For compatibility with Jmix OIDC add-on, the User DTO entity extends the DefaultJmixOidcUser class:

    separate-tiers/frontend-app/src/main/java/com/company/frontendapp/entity/User.java
    @Store(name = "backend")
    @JmixEntity(annotatedPropertiesOnly = true)
    public class User extends DefaultJmixOidcUser implements HasTimeZone {
  5. FrontendUserMapper bean maps the user information from Keycloak to the User DTO entity:

    integrated-apps/service-app/src/main/java/com/company/serviceapp/security/AppUserMapper.java
    package com.company.frontendapp.security;
    
    import com.company.frontendapp.entity.User;
    import io.jmix.core.Metadata;
    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 FrontendUserMapper extends BaseOidcUserMapper<User> {
    
        private final Metadata metadata;
        private final ClaimsRolesMapper claimsRolesMapper;
    
        public FrontendUserMapper(Metadata metadata, ClaimsRolesMapper claimsRolesMapper) {
            this.metadata = metadata;
            this.claimsRolesMapper = claimsRolesMapper;
        }
    
        @Override
        protected String getOidcUserUsername(OidcUser oidcUser) {
            return oidcUser.getPreferredUsername();
        }
    
        @Override
        protected User initJmixUser(OidcUser oidcUser) {
            return metadata.create(User.class);
        }
    
        @Override
        protected void populateUserAttributes(OidcUser oidcUser, User jmixUser) {
            jmixUser.setUsername(getOidcUserUsername(oidcUser));
            jmixUser.setFirstName(oidcUser.getGivenName());
            jmixUser.setLastName(oidcUser.getFamilyName());
            jmixUser.setEmail(oidcUser.getEmail());
        }
    
        @Override
        protected void populateUserAuthorities(OidcUser oidcUser, User jmixUser) {
            Collection<? extends GrantedAuthority> authorities = claimsRolesMapper.toGrantedAuthorities(oidcUser.getClaims());
            jmixUser.setAuthorities(authorities);
        }
    }

    Notice that this mapper extends BaseOidcUserMapper and does not persist the user data to any storage. Instead, the Frontend relies on synchronization of users by the BackendUserMapper of the Backend application.

  6. The backend REST DataStore is configured as follows:

    separate-tiers/frontend-app/src/main/resources/application.properties
    jmix.core.additional-stores=backend
    jmix.core.store-descriptor-backend=restds_RestDataStoreDescriptor
    
    backend.baseUrl=http://localhost:8081
    
    jmix.restds.ui-config-store=backend

    The jmix.restds.ui-config-store property defines the REST data store that will be used for UI filter configuration and user settings and persistence.

  7. To use the OAuth2 token of the current user when making requests to the Backend REST API, the Frontend application defines the following bean that implements the RestAuthenticator interface:

    separate-tiers/frontend-app/src/main/java/com/company/frontendapp/security/RestOidcAuthenticator.java
    package com.company.frontendapp.security;
    
    import io.jmix.restds.impl.RestAuthenticator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.BeanDefinition;
    import org.springframework.context.annotation.Scope;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
    import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
    import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    @Component
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public class RestOidcAuthenticator implements RestAuthenticator {
    
        @Autowired
        private OAuth2AuthorizedClientManager authorizedClientManager;
    
        @Override
        public void setDataStoreName(String name) {
        }
    
        @Override
        public ClientHttpRequestInterceptor getAuthenticationInterceptor() {
            return new AuthenticatingClientHttpRequestInterceptor();
        }
    
        private String getAccessToken() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null) {
                throw new IllegalStateException("Cannot get access token: Authentication object is null");
            }
    
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("keycloak")
                    .principal(authentication)
                    .build();
    
            OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
            if (authorizedClient == null) {
                throw new IllegalStateException("Cannot authorize " + authorizeRequest);
            }
            return authorizedClient.getAccessToken().getTokenValue();
        }
    
        private class AuthenticatingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
                request.getHeaders().setBearerAuth(getAccessToken());
                return execution.execute(request, body);
            }
        }
    }

    This bean’s name is specified in the [restds-name].authenticator property:

    separate-tiers/frontend-app/src/main/resources/application.properties
    backend.authenticator=restOidcAuthenticator

Summary

This guide demonstrates how to use REST DataStore with external authentication in Jmix applications, leveraging Keycloak as the OpenID Connect provider. Two architectural patterns are covered:

  1. Integrated Applications Example

    • Client and Service applications run independently, each with their own UI and database.

    • The Client accesses the Service’s REST API using REST DataStore, forwarding the user’s OAuth2 token for authentication.

    • Both applications use Jmix OIDC add-on for Keycloak integration, with role-based access control.

    • The Service validates incoming OAuth2 tokens and applies the authentication of the user logged into the Client when executing code.

  2. Separate Tiers Example

    • A Frontend application (UI-only) retrieves data from a Backend (REST API + database) via REST DataStore.

    • The Frontend authenticates users via Keycloak and includes their token in REST requests.

    • The Backend validates OAuth2 tokens and applies the authentication of the user logged into the Client when executing code.

Key implementation details:

  • The REST DataStore configuration includes a custom RestAuthenticator to forward OAuth2 tokens.

  • The security customization includes custom mappers based on classes from Jmix OIDC add-on for synchronizing user data from Keycloak.

By following this guide, developers can implement secure, distributed Jmix applications with external authentication and REST-based data access.