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 set in the current execution thread. You can use it to get the collection of current user authorities. In the standard Jmix security implementation, the collection contains authority objects for each resource and row-level role assigned to the user.

  • 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, GraphQL, or REST API. Each client has its own standard authentication mechanism, for example:

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 security.ex1.security;

import io.jmix.securityui.password.PasswordValidationContext;
import io.jmix.securityui.password.PasswordValidationException;
import io.jmix.securityui.password.PasswordValidator;
import org.springframework.stereotype.Component;
import security.ex1.entity.User;

@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 edit screen or detail view, use the PasswordValidation helper bean:

@Autowired
private PasswordValidation passwordValidation;

@Subscribe
protected void onBeforeCommit(BeforeCommitChangesEvent event) {
    if (entityStates.isNew(getEditedEntity())) {
        // ...
        List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
        if (!validationErrors.isEmpty()) {
            notifications.create(Notifications.NotificationType.WARNING)
                    .withCaption(String.join("\n", validationErrors))
                    .show();
            event.preventCommit();
        }
        getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue()));
    }
}

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 screens directly:

public class CustomerBrowse extends StandardLookup<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.

If you want to share session attributes between REST API requests authenticated with the same token, add the following dependency to your build.gradle:

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

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.