Can't log in: multitenent application

I have created a new application in JMIX with multitenancy add-on. I can log in using the admin user but when I try to log in using the user created for a tenant, the log-in fails.

I have followed all the steps recommended as follows:

  1. User Entity

    @JmixEntity
    @Entity(name = “st_User”)
    @Table(name = “ST_USER”, indexes = {
    @Index(name = “IDX_ST_USER_ON_USERNAME”, columnList = “USERNAME”, unique = true)
    })
    public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant {
    @TenantId
    @Column(name = “TENANT”)
    private String tenant;

     @Id
     @Column(name = "ID", nullable = false)
     @JmixGeneratedValue
     private UUID id;
     ..............
       ..........  
     @Override
     public String getTenantId() {
             return tenant;
         }
    
     public String getTenant() {
         return tenant;
     }
    
     public void setTenant(String tenant) {
         this.tenant = tenant;
     }
    
     public UUID getId() {
         return id;
     }
    
  2. Added tenant field in both user browser and editor screen

  3. User Edit controller

    @UiController(“st_User.edit”)
    @UiDescriptor(“user-edit.xml”)
    @EditedEntityContainer(“userDc”)
    @Route(value = “users/edit”, parentPrefix = “users”)
    public class UserEdit extends StandardEditor {

     @Autowired
     private EntityStates entityStates;
    
     @Autowired
     private PasswordEncoder passwordEncoder;
    
     @Autowired
     private PasswordField passwordField;
    
     @Autowired
     private TextField<String> usernameField;
    
     @Autowired
     private PasswordField confirmPasswordField;
    
     @Autowired
     private Notifications notifications;
    
     @Autowired
     private MessageBundle messageBundle;
    
     @Autowired
     private ComboBox<String> timeZoneField;
     @Autowired
     private ComboBox<String> tenantField;
    
     @Autowired
     private TenantProvider tenantProvider;
    
     @Autowired
     private MultitenancyUiSupport multitenancyUiSupport;
    
     @Subscribe
     public void onBeforeShow(BeforeShowEvent event) {
         String currentTenantId = tenantProvider.getCurrentUserTenantId();
         if (!currentTenantId.equals(TenantProvider.NO_TENANT)
                 && Strings.isNullOrEmpty(tenantField.getValue())) {
             tenantField.setEditable(false);
             tenantField.setValue(currentTenantId);
         }
     }
    
     @Subscribe("tenantField")
     public void onTenantFieldValueChange(HasValue.ValueChangeEvent<String> event) {
         usernameField.setValue(
                 multitenancyUiSupport.getUsernameByTenant(
                         usernameField.getValue(), event.getValue()));
     }
    
    
     @Subscribe
     public void onInitEntity(InitEntityEvent<User> event) {
         usernameField.setEditable(true);
         passwordField.setVisible(true);
         confirmPasswordField.setVisible(true);
         tenantField.setEditable(true);
     }
    
     @Subscribe
     public void onAfterShow(AfterShowEvent event) {
         if (entityStates.isNew(getEditedEntity())) {
             usernameField.focus();
         }
     }
    
     @Subscribe
     protected void onBeforeCommit(BeforeCommitChangesEvent event) {
         if (entityStates.isNew(getEditedEntity())) {
             if (!Objects.equals(passwordField.getValue(), confirmPasswordField.getValue())) {
                 notifications.create(Notifications.NotificationType.WARNING)
                         .withCaption(messageBundle.getMessage("passwordsDoNotMatch"))
                         .show();
                 event.preventCommit();
             }
             getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue()));
         }
     }
    
     @Subscribe
     public void onInit(InitEvent event) {
         timeZoneField.setOptionsList(Arrays.asList(TimeZone.getAvailableIDs()));
         tenantField.setOptionsList(multitenancyUiSupport.getTenantOptions());
     }
    

    }

  4. Updated log-in screen as suggested

    @UiController(“st_LoginScreen”)
    @UiDescriptor(“login-screen.xml”)
    @Route(path = “login”, root = true)
    public class LoginScreen extends Screen {

     @Autowired
     private TextField<String> usernameField;
    
     @Autowired
     private PasswordField passwordField;
    
     @Autowired
     private CheckBox rememberMeCheckBox;
    
     @Autowired
     private ComboBox<Locale> localesField;
    
     @Autowired
     private Notifications notifications;
    
     @Autowired
     private Messages messages;
    
     @Autowired
     private MessageTools messageTools;
    
     @Autowired
     private LoginScreenSupport loginScreenSupport;
    
     @Autowired
     private UiLoginProperties loginProperties;
    
     @Autowired
     private JmixApp app;
    
     private final Logger log = LoggerFactory.getLogger(LoginScreen.class);
    
     @Autowired
     private MultitenancyUiSupport multitenancyUiSupport;
    
     @Autowired
     private UrlRouting urlRouting;
    
    
    
     @Subscribe
     private void onInit(InitEvent event) {
         usernameField.focus();
         initLocalesField();
         initDefaultCredentials();
     }
    
     private void initLocalesField() {
         localesField.setOptionsMap(messageTools.getAvailableLocalesMap());
         localesField.setValue(app.getLocale());
         localesField.addValueChangeListener(this::onLocalesFieldValueChangeEvent);
     }
    
     private void onLocalesFieldValueChangeEvent(HasValue.ValueChangeEvent<Locale> event) {
         //noinspection ConstantConditions
         app.setLocale(event.getValue());
         UiControllerUtils.getScreenContext(this).getScreens()
                 .create(this.getClass(), OpenMode.ROOT)
                 .show();
     }
    
     private void initDefaultCredentials() {
         String defaultUsername = loginProperties.getDefaultUsername();
         if (!StringUtils.isBlank(defaultUsername) && !"<disabled>".equals(defaultUsername)) {
             usernameField.setValue(defaultUsername);
         } else {
             usernameField.setValue("");
         }
    
         String defaultPassword = loginProperties.getDefaultPassword();
         if (!StringUtils.isBlank(defaultPassword) && !"<disabled>".equals(defaultPassword)) {
             passwordField.setValue(defaultPassword);
         } else {
             passwordField.setValue("");
         }
     }
    
     @Subscribe("submit")
     private void onSubmitActionPerformed(Action.ActionPerformedEvent event) {
         login();
     }
    
     private void login() {
         String username = usernameField.getValue();
         String password = passwordField.getValue();
    
         if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
             notifications.create(Notifications.NotificationType.WARNING)
                     .withCaption(messages.getMessage(getClass(), "emptyUsernameOrPassword"))
                     .show();
             return;
         }
    
         // add tenantId prefix if it was provided in the URL
         username = multitenancyUiSupport.getUsernameByUrl(username, urlRouting);
    
         try {
             loginScreenSupport.authenticate(
                     AuthDetails.of(username, password)
                             .withLocale(localesField.getValue())
                             .withRememberMe(rememberMeCheckBox.isChecked()), this);
         } catch (BadCredentialsException | DisabledException | LockedException e) {
             log.info("Login failed", e);
             notifications.create(Notifications.NotificationType.ERROR)
                     .withCaption(messages.getMessage(getClass(), "loginFailed"))
                     .withDescription(messages.getMessage(getClass(), "badCredentials"))
                     .show();
         }
     }
    

    }

  5. Only one thing I didn’t find to update is the following recommendation in the user guide:

Configuring Security

When configuring roles for tenant users, exclude tenant-id attributes from entity attribute policies, so users won’t see them. For example, if the Customer entity is tenant-specific and has tenant attribute annotated with @TenantId, the role that gives access to the entity should list the attributes explicitly and omit tenant:

@ResourceRole(name = "Users", code = "users", scope = "UI")
public interface UsersRole {
    // ...

    @EntityAttributePolicy(
            entityClass = Customer.class, attributes = {"region", "name", "version", "id"},
            action = EntityAttributePolicyAction.MODIFY)
    @EntityPolicy(entityClass = Customer.class, actions = EntityPolicyAction.ALL)
    void customer();

Thanks for your help.

Do you provide tenant id of users as described here?

I looked at but not sure. Do I have to use the user name during log-in like this “t2|alice” ? I was trying just “alice”