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:
-
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. -
Create a Spring bean to convert the incoming SAML assertion into a Jmix user object. The simplest approach is to extend
BaseSamlUserMapperand 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 Assertionare copied into your user object. In the example, thePositionattribute from theAssertionis stored in thepositionfield ofMyUser.3 In this method authorities are assigned to the user. Instead of implementing that logic directly, the example delegates it to the SamlAssertionRolesMapperinterface and, therefore, to the default implementation, which isDefaultSamlAssertionRolesMapper.
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:
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
PositionisManager, the user receives theedit-contractsandview-archiveroles. -
otherwise, the user receives the
view-contractsrole.
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:
-
Make the User entity compatible with the Jmix SAML add-on by extending the
JmixSamlUserEntityabstract 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 { //... } -
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.