diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
new file mode 100644
index 0000000000..4856fb6473
--- /dev/null
+++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) eHealth
+ */
+package org.keycloak.broker.saml.mappers;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.common.util.CollectionUtil;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author Frederik Libert
+ *
+ */
+public class UserAttributeStatementMapper extends AbstractIdentityProviderMapper {
+
+ private static final String USER_ATTR_LOCALE = "locale";
+
+ private static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+ private static final List CONFIG_PROPERTIES = new ArrayList<>();
+
+ public static final String ATTRIBUTE_NAME_PATTERN = "attribute.name.pattern";
+
+ public static final String USER_ATTRIBUTE_FIRST_NAME = "user.attribute.firstName";
+
+ public static final String USER_ATTRIBUTE_LAST_NAME = "user.attribute.lastName";
+
+ public static final String USER_ATTRIBUTE_EMAIL = "user.attribute.email";
+
+ public static final String USER_ATTRIBUTE_LANGUAGE = "user.attribute.language";
+
+ private static final String USE_FRIENDLY_NAMES = "use.friendly.names";
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName(ATTRIBUTE_NAME_PATTERN);
+ property.setLabel("Attribute Name Pattern");
+ property.setHelpText("Pattern of attribute names in assertion that must be mapped. Leave blank to map all attributes.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_FIRST_NAME);
+ property.setLabel("User Attribute FirstName");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property firstName.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_LAST_NAME);
+ property.setLabel("User Attribute LastName");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property lastName.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_EMAIL);
+ property.setLabel("User Attribute Email");
+ property.setHelpText("Define which saml Attribute must be mapped to the User property email.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USER_ATTRIBUTE_LANGUAGE);
+ property.setLabel("User Attribute Language");
+ property.setHelpText("Define which saml Attribute must be mapped to the User attribute locale.");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(USE_FRIENDLY_NAMES);
+ property.setLabel("Use Attribute Friendly Name");
+ property.setHelpText("Define which name to give to each mapped user attribute: name or friendlyName.");
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ CONFIG_PROPERTIES.add(property);
+ }
+
+ public static final String PROVIDER_ID = "saml-user-attributestatement-idp-mapper";
+
+ @Override
+ public List getConfigProperties() {
+ return CONFIG_PROPERTIES;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS.clone();
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "AttributeStatement Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "AttributeStatement Importer";
+ }
+
+ @Override
+ public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+ String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+ String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+ String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+ String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+ Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+ List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+ for (AttributeType a : attributesInContext) {
+ String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+ List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+ if (!attributeValuesInContext.isEmpty()) {
+ // set as attribute anyway
+ context.setUserAttribute(attribute, attributeValuesInContext);
+ // set as special field ?
+ if (Objects.equals(attribute, emailAttribute)) {
+ setIfNotEmpty(context::setEmail, attributeValuesInContext);
+ } else if (Objects.equals(attribute, firstNameAttribute)) {
+ setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, lastNameAttribute)) {
+ setIfNotEmpty(context::setLastName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, langAttribute)) {
+ context.setUserAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+ String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME);
+ String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME);
+ String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL);
+ String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE);
+ Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES));
+ List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel));
+
+ Set assertedUserAttributes = new HashSet();
+ for (AttributeType a : attributesInContext) {
+ String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName();
+ List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
+ List currentAttributeValues = user.getAttributes().get(attribute);
+ if (attributeValuesInContext == null) {
+ // attribute no longer sent by brokered idp, remove it
+ user.removeAttribute(attribute);
+ } else if (currentAttributeValues == null) {
+ // new attribute sent by brokered idp, add it
+ user.setAttribute(attribute, attributeValuesInContext);
+ } else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) {
+ // attribute sent by brokered idp has different values as before, update it
+ user.setAttribute(attribute, attributeValuesInContext);
+ }
+ if (Objects.equals(attribute, emailAttribute)) {
+ setIfNotEmpty(context::setEmail, attributeValuesInContext);
+ } else if (Objects.equals(attribute, firstNameAttribute)) {
+ setIfNotEmpty(context::setFirstName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, lastNameAttribute)) {
+ setIfNotEmpty(context::setLastName, attributeValuesInContext);
+ } else if (Objects.equals(attribute, langAttribute)) {
+ if(attributeValuesInContext == null) {
+ user.removeAttribute(USER_ATTR_LOCALE);
+ } else {
+ user.setAttribute(USER_ATTR_LOCALE, attributeValuesInContext);
+ }
+ assertedUserAttributes.add(USER_ATTR_LOCALE);
+ }
+ // Mark attribute as handled
+ assertedUserAttributes.add(attribute);
+ }
+ // Remove user attributes that were not referenced in assertion.
+ user.getAttributes().keySet().stream().filter(a -> !assertedUserAttributes.contains(a)).forEach(a -> user.removeAttribute(a));
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Import all saml attributes found in attributestatements in assertion into user properties or attributes.";
+ }
+
+ private Optional getAttributePattern(IdentityProviderMapperModel mapperModel) {
+ String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN);
+ return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null);
+ }
+
+ private List findAttributesInContext(BrokeredIdentityContext context, Optional attributePattern) {
+ AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+
+ return assertion.getAttributeStatements().stream()//
+ .flatMap(statement -> statement.getAttributes().stream())//
+ .filter(item -> !attributePattern.isPresent() || attributePattern.get().matcher(item.getAttribute().getName()).matches())//
+ .map(ASTChoiceType::getAttribute)//
+ .collect(Collectors.toList());
+ }
+
+ private void setIfNotEmpty(Consumer consumer, List values) {
+ if (values != null && !values.isEmpty()) {
+ consumer.accept(values.get(0));
+ }
+ }
+
+}