Custom Endpoints

Requests to the Jmix application are protected by the Spring Security framework. This section explains how you can configure access to custom API endpoints.

General Information

To gain a deep understanding of how endpoint security works, read the relevant sections of the Spring Security documentation:

Spring Security uses special SecurityFilterChain beans to determine which URLs should be protected. Each SecurityFilterChain bean is configured by the HttpSecurity builder. An application can have multiple SecurityFilterChain beans declared. In this case, it is extremely important to indicate their correct order. See the Multiple HttpSecurity Instances section of the Spring Security documentation for how to configure multiple HttpSecurity objects.

Each Jmix application by default contains a security configuration that extends the VaadinWebSecurity class. This configuration sets up access to internal Vaadin endpoints and delegates all requests authorization to Jmix and Vaadin mechanisms (grants access to views using view controller annotation or by analyzing user’s resource roles). SecurityFilterChain created by this configuration always has the lowest priority and is always invoked last. The Custom Endpoints Security section below explains how to define you own SecurityFilterChain to protect custom endpoints.

Add-ons like OpenID Connect or Authorization Server bring their own SecurityFilterChain beans that protect authorization or resource server endpoints. These beans are always invoked before the one from the UI module. See the Token Based Authentication section below for how to protect API endpoints when using these add-ons.

Custom Endpoints Security

To define custom security rules for endpoints, declare a new SecurityFilterChain bean. It is important that the order of this bean must be less than the order of SecurityFilterChain beans provided by Jmix framework.

You can find order value constants used by Jmix in the JmixSecurityFilterChainOrder interface. The rule of thumb is to use the JmixSecurityFilterChainOrder.CUSTOM, JmixSecurityFilterChainOrder.CUSTOM - 10 and similar values for your security filter chains.

A simple SecurityFilterChain bean definition may look as follows:

@Bean
@Order(JmixSecurityFilterChainOrder.CUSTOM)
SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/public/**")
            .authorizeHttpRequests(authorize ->
                    authorize.anyRequest().permitAll()
            );
    return http.build();
}

The configuration above grants access to all requests that match the /public/** pattern.

Public Endpoints

Let’s assume that you have a controller with two methods, and you want these methods to be available for any user without authentication.

@RestController
public class GreetingController {

    @PostMapping("/greeting/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/greeting/public/hi")
    public String hi() {
        return "Hi!";
    }
}

Public access can be configured using the following configuration:

@Configuration
public class AnonymousControllerSecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.CUSTOM) (1)
    SecurityFilterChain greetingFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/greeting/**") (2)
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().permitAll() (3)
                )
                .csrf(csrf -> csrf.disable()); (4)
        JmixHttpSecurityUtils.configureAnonymous(http); (5)
        return http.build();
    }
}
1 JmixSecurityFilterChainOrder.CUSTOM order is less than any other Jmix security filter chain order, so your filter chain will be used before any filter chain from Jmix.
2 securityMatcher() is used to determine whether a given HttpSecurity should be applied to the request. The request matcher from the example will match requests with URLs that follow the pattern /greeting/**. Requests for other URLs will be processed by a default security filter chain from Jmix UI module.
3 permitAll() instruction grants access to endpoints.
4 Disables CSRF for POST requests.
5 The invocation of JmixHttpSecurityUtils.configureAnonymous(http) configures anonymous authentication by setting the anonymous user returned by the Jmix UserRepository to the security context.

HTTP Basic Authentication

The example demonstrates how to protect controller endpoints using HTTP Basic authentication.

The controller class:

@RestController
@RequestMapping("/api")
public class BasicGreetingController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/public/hi")
    public String publicHi() {
        return "Hi!";
    }
}

Requests with URLs matching the /api/** pattern should be protected with HTTP Basic authentication, while requests to /api/public/** should be available to all users. This can be achieved by using the configuration below:

@Configuration
public class BasicControllerSecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.CUSTOM) (1)
    SecurityFilterChain basicControllerFilterChain(
            HttpSecurity http,
            AuthenticationManager authenticationManager) throws Exception {
        http.securityMatcher("/api/**") (2)
                .authorizeHttpRequests(requests ->
                        requests
                                .requestMatchers("/api/public/**").permitAll() (3)
                                .anyRequest().authenticated() (4)
                )
                .httpBasic(Customizer.withDefaults()) (5)
                .authenticationManager(authenticationManager); (6)
        return http.build();
    }
}
1 JmixSecurityFilterChainOrder.CUSTOM order is less than any other Jmix security filter chain order, so your filter chain will be used before any filter chain from Jmix.
2 Security matcher indicates that the HttpSecurity will only be applied for /api/** requests.
3 If the request is selected by security matcher then we can provide further rules. All requests for /api/public/** must be permitted without authentication.
4 All requests that don’t match the /api/public/** pattern defined above must be authenticated.
5 Enable basic authentication.
6 Use the AuthenticationManager configured by Jmix for basic authentication.

Requests to /api/** endpoints must contain a header in the form of Authorization: Basic <credentials>, where <credentials> is the Base64 encoding of username and password joined by a single colon. For example:

GET /api/hello HTTP/1.1
Host: server.example.com
Authorization: Basic YWRtaW46YWRtaW4=
In this example, public access to /api/public/** could be alternatively configured using another SecurityFilterChain bean that has securityMatcher("/api/public/**") and an order less than the current one, e.g. JmixSecurityFilterChainOrder.CUSTOM - 10.

Token Based Authentication

You can protect custom endpoints using bearer tokens issued by the Authorization Server or an external identity provider, such as Keycloak, when using the OpenID Connect add-on. The security configurations of the Authorization Server and the OpenID Connect add-ons provide extension points for this purpose. In the context of the OAuth 2.1 specification, your application that hosts the protected endpoints acts as a resource server.

Suppose you have a REST controller:

@RestController
public class GreetingController {

    @PostMapping("/greeting/hello")
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/greeting/public/hi")
    public String hi() {
        return "Hi!";
    }
}

There are several ways to determine which endpoints should be protected with token-based authentication and which can be accessed anonymously.

Application Properties

The simplest way to determine which endpoints of a resource server should be protected and which should have public access, is by using the following application properties:

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

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

URL Patterns Providers

Another way to define which endpoints need to be protected with token-based authentication is to create a bean implementing the AuthenticatedUrlPatternsProvider interface and return a list of URL patterns from its getAuthenticatedUrlPatterns() method. This approach is more flexible and allows you to define more complex rules for endpoint protection.

@Component
public class GreetingAuthenticatedUrlPatternsProvider implements AuthenticatedUrlPatternsProvider {

    @Override
    public List<String> getAuthenticatedUrlPatterns() {
        return List.of("/greeting/**");
    }

}

Resource server endpoints that must be accessed anonymously can be defined in the same way using the AnonymousUrlPatternsProvider interface.

@Component
public class GreetingAnonymousUrlPatternsProvider implements AnonymousUrlPatternsProvider {

    @Override
    public List<String> getAnonymousUrlPatterns() {
        return List.of("/greeting/public/**");
    }

}

RequestMatcher Provider

Application properties and URL patterns providers allow you to define endpoints protection rules that operate on a collection of strings with URL patterns. If you need more complex rules than just the URL pattern, such as the HTTP method as well, you can implement the AuthenticatedRequestMatcherProvider interface and return a RequestMatcher object from its getAuthenticatedRequestMatcher() method.

@Component
public class GreetingAuthenticatedRequestMatcherProvider implements AuthenticatedRequestMatcherProvider {

    @Override
    public RequestMatcher getAuthenticatedRequestMatcher() {
        return new AntPathRequestMatcher("/greeting/**", HttpMethod.POST.name());
    }
}

RequestMatcher for public endpoints of the resource server can be defined in the same way using the AnonymousRequestMatcherProvider interface.

@Component
public class GreetingAnonymousRequestMatcherProvider implements AnonymousRequestMatcherProvider {

    @Override
    public RequestMatcher getAnonymousRequestMatcher() {
        return new AntPathRequestMatcher("/greeting/public/**", HttpMethod.GET.name());
    }
}

Accessing Resource Server Endpoints

After any of the above configurations are defined, all requests to protected /greeting/** endpoints of the resource server will require a bearer access token in the Authorization header. For example:

GET /greeting/hello HTTP/1.1
Host: server.example.com
Authorization: Bearer <ACCESS_TOKEN>

Troubleshooting

If you come across a 401 Unauthorized or 403 Forbidden HTTP error or have any other issues related to endpoint security, then it’s very likely that Spring Security logging will help you to understand what is going on.

To enable the logging add the following application property with DEBUG or TRACE value to the application.properties file:

logging.level.org.springframework.security = TRACE