Authentication

Authentication is the process of verifying the identity of a user or process that interacts with the system. For example, the system can authenticate users by their username and password. For authenticated users, the system can perform authorization, which is a check of permissions to a particular resource.

Jmix directly uses Spring Security servlet authentication, so if you are familiar with this framework, you can easily extend or override the standard authentication mechanism provided by Jmix out-of-the-box.

Current User

To determine who is currently authenticated, use the CurrentAuthentication bean. It has the following methods:

  • getUser() returns the currently authenticated user as UserDetails. You can cast it to the class of users defined in your project.

  • getAuthentication() returns the Authentication object associated with the current execution thread. The Authentication object stores the names of the user’s roles.

    Jmix uses the Spring Security SimpleGrantedAuthority class to represent user roles. This class effectively stores a single string representing a role. The format of this string is:

    • For resource roles: ROLE_<role-code>, for example, ROLE_system-full-access.

    • For row-level roles: ROW_LEVEL_ROLE_<role-code>, for example, ROW_LEVEL_ROLE_my-role.

    Granted authorities of the appropriate Java class and content can be created from role codes using the RoleGrantedAuthorityUtils class.

    You can customize the prefix for resource role authorities using the standard Spring mechanism by configuring the org.springframework.security.config.core.GrantedAuthorityDefaults bean.

    Similarly, you can adjust the prefix for row-level role authorities using the jmix.security.default-row-level-role-prefix application property.

  • getLocale() and getTimeZone() return the locale and time zone of the current user.

  • isSet() returns true if the current execution thread is authenticated, that is contains information about the user. If it’s not, getUser(), getLocale() and getTimeZone() methods described above will throw the IllegalStateException.

Below is an example of getting the information about the current user:

@Autowired
private CurrentAuthentication currentAuthentication;

private void printAuthenticationInfo() {
    UserDetails user = currentAuthentication.getUser();
    Authentication authentication = currentAuthentication.getAuthentication();
    Locale locale = currentAuthentication.getLocale();
    TimeZone timeZone = currentAuthentication.getTimeZone();

    System.out.println(
            "User: " + user.getUsername() + "\n" +
                    "Authentication: " + authentication + "\n" +
                    "Roles: " + getRoleNames(authentication) + "\n" +
                    "Locale: " + locale.getDisplayName() + "\n" +
                    "TimeZone: " + timeZone.getDisplayName()
    );
}

private String getRoleNames(Authentication authentication) {
    return authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));
}

CurrentAuthentication is just a wrapper around SecurityContextHolder, so it is fully compatible with all Spring Security mechanisms.

For example, you can use DelegatingSecurityContextRunnable to propagate authentication to new threads as described in Spring Security documentation.

Client Authentication

The backend of a Jmix application can have different clients, for example, Jmix UI or REST API. Each client has its own standard authentication mechanism, such as the UI login view or REST access token.

Custom Password Validation

To implement a custom password validation in the application, create a bean (or multiple beans) implementing the PasswordValidator interface, for example:

package com.company.demo.security;

import com.company.demo.entity.User;
import io.jmix.securityflowui.password.PasswordValidationContext;
import io.jmix.securityflowui.password.PasswordValidationException;
import io.jmix.securityflowui.password.PasswordValidator;
import org.springframework.stereotype.Component;

@Component
public class MyPasswordValidator implements PasswordValidator<User> {

    @Override
    public void validate(PasswordValidationContext<User> context) throws PasswordValidationException {
        if (context.getPassword().length() < 3)
            throw new PasswordValidationException("Password is too short, must be >= 3 characters");
    }
}

All password validators will be automatically used in the ChangePassword action dialog.

To add the validation to the User detail view, use the PasswordValidation helper bean:

@Autowired
private PasswordValidation passwordValidation;

@Subscribe
public void onValidation(final ValidationEvent event) {
    // ...
    if (entityStates.isNew(getEditedEntity())) {
        List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
        if (!validationErrors.isEmpty()) {
            event.getErrors().add(String.join("\n", validationErrors));
        }
    }

BruteForce Protection

The framework has a mechanism for the protection against password brute force cracking.

The jmix.security.bruteforceprotection.enabled application property enables the protection. If the protection is enabled, the combination of user login and IP address is blocked for a time interval in case of multiple unsuccessful login attempts. A maximum number of login attempts for the combination of user login and IP address is defined by the jmix.security.bruteforceprotection.max-login-attempts-number application property. Blocking interval in seconds is determined by the jmix.security.bruteforceprotection.block-interval application property.

  • jmix.security.bruteforceprotection.enabled

    Enables a mechanism for the protection against password brute force cracking. The default value: false.

  • jmix.security.bruteforceprotection.block-interval

    Defines the blocking interval in seconds after exceeding a maximum number of failed login attempts if the jmix.security.bruteforceprotection.enabled property is on. The default value: 60 seconds.

  • jmix.security.bruteforceprotection.max-login-attempts-number

    Defines a maximum number of failed login attempts for the combination of user login and IP address if the jmix.security.bruteforceprotection.enabled property is on. The default value: 5.

Session Attributes

If you need to share some values across multiple requests from the same connected user, use the SessionData bean. It has methods for reading and writing named values stored in the current user session.

You can inject the SessionData bean into UI views directly:

public class CustomerListView extends StandardListView<Customer> {

    @Autowired
    private SessionData sessionData;

In a singleton bean, use SessionData through org.springframework.beans.factory.ObjectProvider:

@Component
public class CustomerService {

    @Autowired
    private ObjectProvider<SessionData> sessionDataProvider;

    public void saveSessionValue(String value) {
        sessionDataProvider.getObject().setAttribute("my-attribute", value);
    }
Session attributes can also be used in JPQL queries.

When handling UI requests, the shared values are stored in the HTTP session.

System Authentication

The execution thread can be not authenticated if it was started by an internal scheduler or handles a request from the JMX interface. At the same time, your business logic or data access code usually requires information on who is currently working with the system for logging or authorization.

To temporarily associate the current execution thread with a user, use the SystemAuthenticator bean. It has the following methods:

  • withSystem() - accepts a lambda and executes it as the system user.

  • withUser() - accepts a username of a regular application user and a lambda and executes the lambda as the given user with their permissions.

Below is an example of authenticating an MBean operation:

@Autowired
private SystemAuthenticator systemAuthenticator;
@Autowired
private CurrentAuthentication currentAuthentication;

@ManagedOperation
public String doSomething() {
    return systemAuthenticator.withSystem(() -> {
        UserDetails user = currentAuthentication.getUser();
        System.out.println("User: " + user.getUsername()); // system
        // ...
        return "Done";
    });
}

@ManagedOperation
public String doSomething2() {
    return systemAuthenticator.withUser("admin", () -> {
        UserDetails user = currentAuthentication.getUser();
        System.out.println("User: " + user.getUsername()); // admin
        // ...
        return "Done";
    });
}

You can also use the @Authenticated annotation to authenticate an entire bean method as executed by the system user. For example:

@Autowired
private CurrentAuthentication currentAuthentication;

@Authenticated // authenticates the entire method
@ManagedOperation
public String doSomething3() {
    UserDetails user = currentAuthentication.getUser();
    System.out.println("User: " + user.getUsername()); // system
    // ...
    return "Done";
}

Authentication Events

Spring framework sends specific application events related to authentication.

Studio can help you generate listeners to authentication events. Click New (+) → Event Listener in the Jmix tool window and select Authentication Event in the dialog.

Below is an example of handling authentication events.

@Component
public class AuthenticationEventListener {

    private static final Logger log =
            LoggerFactory.getLogger(AuthenticationEventListener.class);

    @EventListener
    public void onInteractiveAuthenticationSuccess(
            InteractiveAuthenticationSuccessEvent event) { (1)
        User user = (User) event.getAuthentication().getPrincipal(); (2)
        log.info("User logged in: " + user.getUsername());
    }

    @EventListener
    public void onAuthenticationSuccess(
            AuthenticationSuccessEvent event) { (3)
        User user = (User) event.getAuthentication().getPrincipal(); (4)
        log.info("User authenticated " + user.getUsername());
    }

    @EventListener
    public void onAuthenticationFailure(
            AbstractAuthenticationFailureEvent event) { (5)
        String username = (String) event.getAuthentication().getPrincipal(); (6)
        log.info("User login attempt failed: " + username);
    }

    @EventListener
    public void onLogoutSuccess(LogoutSuccessEvent event) { (7)
        User user = (User) event.getAuthentication().getPrincipal(); (8)
        log.info("User logged out: " + user.getUsername());
    }
}
1 InteractiveAuthenticationSuccessEvent is sent when a user logs in to the system through UI or REST API.
2 InteractiveAuthenticationSuccessEvent contains the user entity.
3 AuthenticationSuccessEvent is sent on any successful authentication including system.
4 AuthenticationSuccessEvent contains the user entity.
5 AbstractAuthenticationFailureEvent is sent when the authentication attempt has failed, for example because of invalid credentials.
6 AbstractAuthenticationFailureEvent contains only the user name provided for authentication.
7 LogoutSuccessEvent is sent when the user logs out.
8 LogoutSuccessEvent contains the user entity.