Additional Setup

The following instructions describe how to extend the default Jmix SAML configuration. Use them after completing Keycloak SAML Setup or the equivalent basic configuration for another identity provider.

Mapping SAML Attributes

Before you map attributes in the application, make sure they are included in the SAML assertion. See Add SAML attributes in Keycloak or the equivalent steps for your identity provider.

If the SAML assertion contains information about the user, such as their name, position, department, or other profile details, you can make it available in your application during the user session.

In order to do that:

  1. Create a class that extends DefaultJmixSamlUserDetails:

    public class MyUser extends DefaultJmixSamlUserDetails {
    
        private String position; (1)
    
        public String getPosition() {
            return position;
        }
    
        public void setPosition(String position) {
            this.position = position;
        }
    }
    1 An extra field that corresponds to an additional SAML attribute.
  2. Create a Spring bean to convert the incoming SAML assertion into a Jmix user object. The simplest approach is to extend BaseSamlUserMapper and override its methods.

    @Component
    public class MySamlUserMapper extends BaseSamlUserMapper<MyUser> {
    
        @Autowired
        protected SamlAssertionRolesMapper rolesMapper;
    
        @Override
        protected MyUser initJmixUser(Assertion assertion) { (1)
            return new MyUser();
        }
    
        @Override
        protected void populateUserAttributes(Assertion assertion, OpenSaml4AuthenticationProvider.ResponseToken responseToken, MyUser jmixUser) { (2)
            Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
            List<Object> rawValues = assertionAttributes.get("Position");
            String positionValue = CollectionUtils.isNotEmpty(rawValues) ? rawValues.get(0).toString() : null;
            jmixUser.setPosition(positionValue);
            System.out.println(positionValue);
        }
    
        @Override
        protected void populateUserAuthorities(Assertion assertion, MyUser jmixUser) { (3)
            Collection<? extends GrantedAuthority> grantedAuthorities = rolesMapper.toGrantedAuthorities(assertion);
            jmixUser.setAuthorities(grantedAuthorities);
        }
    }
    1 This method creates the Jmix user object that will represent the authenticated user.
    2 Here, values from the SAML Assertion are copied into your user object. In the example, the Position attribute from the Assertion is stored in the position field of MyUser.
    3 In this method authorities are assigned to the user. Instead of implementing that logic directly, the example delegates it to the SamlAssertionRolesMapper interface and, therefore, to the default implementation, which is DefaultSamlAssertionRolesMapper.

Using Different Roles Attribute

By default, DefaultSamlAssertionRolesMapper looks for an attribute named Role in the SAML Assertion. That attribute is expected to contain a collection of role names. For each role name, Jmix searches for matching resource roles and row-level roles. If matching roles are found, the corresponding granted authorities are added to the user.

If your identity provider sends roles in an attribute other than Role, you can change the attribute name with the following Jmix SAML property:

application.properties
jmix.saml.default-saml-assertion-roles-mapper.roles-assertion-attribute=MyRole

This tells the default mapper to read role names from MyRole instead of Role.

Creating Custom Role Mapper

If you need more control, you can create your own role mapper. This is useful when your identity provider does not send Jmix role codes directly, but instead sends some other values that you want to translate into appropriate roles.

For example, a value such as Manager might come from Position attribute, and your mapper can use it to assign the appropriate Jmix roles. To implement this, extend BaseSamlAssertionRolesMapper and override its getResourceRolesCodes() and getRowLevelRolesCodes() methods:

@Component
public class MySamlAssertionRolesMapper extends BaseSamlAssertionRolesMapper {

    @Override
    protected Collection<String> getResourceRolesCodes(Assertion assertion) {
        Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
        List<Object> rawPositionAttributeValues = assertionAttributes.get("Position");

        Collection<String> jmixRoleCodes = new HashSet<>();
        rawPositionAttributeValues.stream()
                .map(Object::toString)
                .forEach(position -> {
                    if ("Manager".equals(position)) {
                        jmixRoleCodes.add("edit-contracts");
                        jmixRoleCodes.add("view-archive");
                    } else {
                        jmixRoleCodes.add("view-contracts");
                    }
                });

        return jmixRoleCodes;
    }

    @Override
    protected Collection<String> getRowLevelRoleCodes(Assertion assertion) {
        // Do something for row-level role codes
        return List.of();
    }
}

In this example, the user’s Position value determines which Jmix resource roles are assigned:

  • if Position is Manager, the user receives the edit-contracts and view-archive roles.

  • otherwise, the user receives the view-contracts role.

Persist Users to Database

By default, the Jmix SAML setup keeps authenticated users in memory only. If you want SAML users to be stored in the database, complete the following steps:

  1. Make the User entity compatible with the Jmix SAML add-on by extending the JmixSamlUserEntity abstract class:

    @JmixEntity
    @Entity
    @Table(name = "USER_", indexes = {
            @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
    })
    public class User extends JmixSamlUserEntity implements JmixUserDetails, HasTimeZone {
    
        //...
    
    }
  2. Register a user mapper based on SynchronizingSamlUserMapper; this superclass stores and updates the user in the database and can synchronize role assignments to the database:

    @Component
    public class MySynchronizingSamlUserMapper extends SynchronizingSamlUserMapper<User> {
    
        public MySynchronizingSamlUserMapper() {
            super();
            setSynchronizeRoleAssignments(true); (1)
        }
    
        @Override
        protected Class<User> getApplicationUserClass() {
            return User.class;
        }
    
        @Override
        protected void populateUserAttributes(Assertion assertion, OpenSaml4AuthenticationProvider.ResponseToken responseToken, User jmixUser) {
            String username = SamlAssertionUtils.getUsername(assertion);
            Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
            String firstNameValue = getStringAttributeValue(assertionAttributes, "FirstName", username);
            String lastNameValue = getStringAttributeValue(assertionAttributes, "LastName", username);
    
            jmixUser.setUsername(username);
            jmixUser.setFirstName(firstNameValue);
            jmixUser.setLastName(lastNameValue);
        }
    
        protected String getStringAttributeValue(Map<String, List<Object>> assertionAttributes, String attributeName, String username) {
            List<Object> rawValues = assertionAttributes.get(attributeName);
            return CollectionUtils.isNotEmpty(rawValues)
                    ? rawValues.get(0).toString()
                    : "%s (%s)".formatted(attributeName, username);
        }
    }
    1 When set to true, role assignments are also stored to the database.