keycloak-uncached

KEYCLOAK-2172 Make Identity broker User Attribute mappers

12/1/2015 10:15:06 AM

Changes

Details

diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java
index ee327c3..f79e8d7 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java
@@ -1,10 +1,10 @@
 package org.keycloak.broker.provider;
 
-import org.keycloak.broker.provider.IdentityProviderMapper;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -35,4 +35,9 @@ public abstract class AbstractIdentityProviderMapper implements IdentityProvider
     public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
 
     }
+
+    @Override
+    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+
+    }
 }
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java b/broker/core/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
index 6052f57..95417fd 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
@@ -18,9 +18,13 @@
 package org.keycloak.broker.provider;
 
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.IdentityProviderModel;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -152,6 +156,22 @@ public class BrokeredIdentityContext {
         this.contextData = contextData;
     }
 
+    // Set the attribute, which will be available on "Update profile" page and in authenticators
+    public void setUserAttribute(String attributeName, String attributeValue) {
+        List<String> list = new ArrayList<>();
+        list.add(attributeValue);
+        getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, list);
+    }
+
+    public String getUserAttribute(String attributeName) {
+        List<String> userAttribute = (List<String>) getContextData().get(Constants.USER_ATTRIBUTES_PREFIX + attributeName);
+        if (userAttribute == null || userAttribute.isEmpty()) {
+            return null;
+        } else {
+            return userAttribute.get(0);
+        }
+    }
+
     public String getFirstName() {
         return firstName;
     }
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java b/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java
index 3f8fcf2..5beb0ab 100644
--- a/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/DefaultDataMarshaller.java
@@ -1,6 +1,7 @@
 package org.keycloak.broker.provider;
 
 import java.io.IOException;
+import java.util.List;
 
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.util.JsonSerialization;
@@ -26,15 +27,20 @@ public class DefaultDataMarshaller implements IdentityProviderDataMarshaller {
 
     @Override
     public <T> T deserialize(String serialized, Class<T> clazz) {
-        if (clazz.equals(String.class)) {
-            return clazz.cast(serialized);
-        } else {
-            byte[] bytes = Base64Url.decode(serialized);
-            try {
-                return JsonSerialization.readValue(bytes, clazz);
-            } catch (IOException ioe) {
-                throw new RuntimeException(ioe);
+        try {
+            if (clazz.equals(String.class)) {
+                return clazz.cast(serialized);
+            } else {
+                byte[] bytes = Base64Url.decode(serialized);
+                if (List.class.isAssignableFrom(clazz)) {
+                    List list = JsonSerialization.readValue(bytes, List.class);
+                    return clazz.cast(list);
+                } else {
+                    return JsonSerialization.readValue(bytes, clazz);
+                }
             }
+        }  catch (IOException ioe) {
+            throw new RuntimeException(ioe);
         }
     }
 }
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java
index d182d6f..7ab9944 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java
@@ -69,10 +69,10 @@ public class HardcodedAttributeMapper extends AbstractIdentityProviderMapper {
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(ATTRIBUTE);
         String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
-        user.setSingleAttribute(attribute, attributeValue);
+        context.setUserAttribute(attribute, attributeValue);
     }
 
     @Override
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
index 676e6b1..656af48 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
@@ -67,7 +67,7 @@ public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProvide
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(ATTRIBUTE);
         String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
         context.getClientSession().setUserSessionNote(attribute, attributeValue);
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java
index b76a6af..7599fae 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java
@@ -20,8 +20,10 @@ public interface IdentityProviderMapper extends Provider, ProviderFactory<Identi
     String getDisplayType();
 
     /**
-     * Called to determine what keycloak username and email to use to process the login request from the external IDP
-     * Usually used to map BrokeredIdentityContet.username or email.
+     * Called to determine what keycloak username and email to use to process the login request from the external IDP.
+     * It's called before "FirstBrokerLogin" flow, so can be used to map attributes to BrokeredIdentityContext ( BrokeredIdentityContext.setUserAttribute ),
+     * which will be available on "Review Profile" page and in authenticators during FirstBrokerLogin flow
+     *
      *
      * @param session
      * @param realm
@@ -31,7 +33,7 @@ public interface IdentityProviderMapper extends Provider, ProviderFactory<Identi
     void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
 
     /**
-     * Called after UserModel is created for first time for this user.
+     * Called after UserModel is created for first time for this user. Called after "FirstBrokerLogin" flow
      *
      * @param session
      * @param realm
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
index 4d2a4e2..5d2147e 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
@@ -69,8 +69,8 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
 	 * @param user context to store profile data into
 	 * @param profile to store into context
 	 * @param provider identification of social provider to be used in log dump
-	 * 
-	 * @see #importNewUser(KeycloakSession, RealmModel, UserModel, IdentityProviderMapperModel, BrokeredIdentityContext)
+	 *
+	 * @see #preprocessFederatedIdentity(KeycloakSession, RealmModel, IdentityProviderMapperModel, BrokeredIdentityContext)
 	 * @see BrokeredIdentityContext#getContextData()
 	 */
 	public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile, String provider) {
@@ -100,17 +100,17 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
 	}
 
 	@Override
-	public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+	public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
 		String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE);
 		if (attribute == null || attribute.trim().isEmpty()) {
-			logger.debug("Attribute is not configured");
+			logger.warnf("Attribute is not configured for mapper %s", mapperModel.getName());
 			return;
 		}
 		attribute = attribute.trim();
 
 		String value = getJsonValue(mapperModel, context);
 		if (value != null) {
-			user.setSingleAttribute(attribute, value);
+			context.setUserAttribute(attribute, value);
 		}
 	}
 
@@ -123,13 +123,13 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
 
 		String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD);
 		if (jsonField == null || jsonField.trim().isEmpty()) {
-			logger.debug("JSON field path is not configured");
+			logger.warnf("JSON field path is not configured for mapper %s", mapperModel.getName());
 			return null;
 		}
 		jsonField = jsonField.trim();
 
 		if (jsonField.startsWith(JSON_PATH_DELIMITER) || jsonField.endsWith(JSON_PATH_DELIMITER) || jsonField.startsWith("[")) {
-			logger.debug("JSON field path is invalid " + jsonField);
+			logger.warnf("JSON field path is invalid %s", jsonField);
 			return null;
 		}
 
@@ -138,7 +138,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
 		String value = getJsonValue(profileJsonNode, jsonField);
 
 		if (value == null) {
-			logger.debug("User profile JSON value '" + jsonField + "' is not available.");
+			logger.debugf("User profile JSON value '%s' is not available.", jsonField);
 		}
 
 		return value;
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
index b810f48..6599388 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
@@ -70,11 +70,11 @@ public class UserAttributeMapper extends AbstractClaimMapper {
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getClaimValue(mapperModel, context);
         if (value != null) {
-            user.setSingleAttribute(attribute, value.toString());
+            context.setUserAttribute(attribute, value.toString());
         }
     }
 
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java
index d246086..a3eafa9 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java
@@ -66,10 +66,6 @@ public class UsernameTemplateMapper extends AbstractClaimMapper {
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
-    }
-
-    @Override
     public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
     }
 
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
index 8d5ccf7..a751b33 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
@@ -80,11 +80,11 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getAttribute(mapperModel, context);
         if (value != null) {
-            user.setSingleAttribute(attribute, value.toString());
+            context.setUserAttribute(attribute, value.toString());
         }
     }
 
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java
index 0a44e4b..28aa00d 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java
@@ -72,11 +72,6 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
     }
 
     @Override
-    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
-
-    }
-
-    @Override
     public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
 
     }
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
index b2d6b1d..0783529 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java
@@ -47,8 +47,9 @@ public class ProfileBean {
         this.user = user;
         this.formData = formData;
 
-        if (user.getAttributes() != null) {
-            for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
+        Map<String, List<String>> modelAttrs = user.getAttributes();
+        if (modelAttrs != null) {
+            for (Map.Entry<String, List<String>> attr : modelAttrs.entrySet()) {
                 List<String> attrValue = attr.getValue();
                 if (attrValue != null && attrValue.size() > 0) {
                     attributes.put(attr.getKey(), attrValue.get(0));
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index 6437cd5..0c52929 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -28,4 +28,7 @@ public interface Constants {
 
     String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
     String KEY = "key";
+
+    // Prefix for user attributes used in various "context"data maps
+    public static final String USER_ATTRIBUTES_PREFIX = "user.attributes.";
 }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 22c1582..3a105c4 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -432,7 +432,7 @@ public class DefaultAuthenticationFlows {
             if (browserFlow == null) {
                 browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
             }
-            
+
             List<AuthenticationExecutionModel> browserExecutions = new LinkedList<>();
             KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions);
             for (AuthenticationExecutionModel browserExecution : browserExecutions) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
index 7e3990b..968831c 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
@@ -1,6 +1,8 @@
 package org.keycloak.authentication.authenticators.broker.util;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -14,6 +16,7 @@ import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.common.util.reflections.Reflections;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelException;
@@ -162,44 +165,52 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
         this.contextData = contextData;
     }
 
+    @JsonIgnore
     @Override
     public Map<String, List<String>> getAttributes() {
         Map<String, List<String>> result = new HashMap<>();
 
         for (Map.Entry<String, ContextDataEntry> entry : this.contextData.entrySet()) {
-            if (entry.getKey().startsWith("user.attributes.")) {
-                ContextDataEntry ctxEntry = entry.getValue();
-                String asString = ctxEntry.getData();
-                try {
-                    List<String> asList = JsonSerialization.readValue(asString, List.class);
-                    result.put(entry.getKey().substring(16), asList);
-                } catch (IOException ioe) {
-                    throw new RuntimeException(ioe);
-                }
+            if (entry.getKey().startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
+                String attrName = entry.getKey().substring(16); // length of USER_ATTRIBUTES_PREFIX
+                List<String> asList = getAttribute(attrName);
+                result.put(attrName, asList);
             }
         }
 
         return result;
     }
 
+    @JsonIgnore
+    @Override
+    public void setSingleAttribute(String name, String value) {
+        List<String> list = new ArrayList<>();
+        list.add(value);
+        setAttribute(name, list);
+    }
+
+    @JsonIgnore
     @Override
     public void setAttribute(String key, List<String> value) {
         try {
-            String listStr = JsonSerialization.writeValueAsString(value);
+            byte[] listBytes = JsonSerialization.writeValueAsBytes(value);
+            String listStr = Base64Url.encode(listBytes);
             ContextDataEntry ctxEntry = ContextDataEntry.create(List.class.getName(), listStr);
-            this.contextData.put("user.attributes." + key, ctxEntry);
+            this.contextData.put(Constants.USER_ATTRIBUTES_PREFIX + key, ctxEntry);
         } catch (IOException ioe) {
             throw new RuntimeException(ioe);
         }
     }
 
+    @JsonIgnore
     @Override
     public List<String> getAttribute(String key) {
-        ContextDataEntry ctxEntry = this.contextData.get("user.attributes." + key);
+        ContextDataEntry ctxEntry = this.contextData.get(Constants.USER_ATTRIBUTES_PREFIX + key);
         if (ctxEntry != null) {
             try {
                 String asString = ctxEntry.getData();
-                List<String> asList = JsonSerialization.readValue(asString, List.class);
+                byte[] asBytes = Base64Url.decode(asString);
+                List<String> asList = JsonSerialization.readValue(asBytes, List.class);
                 return asList;
             } catch (IOException ioe) {
                 throw new RuntimeException(ioe);
@@ -209,6 +220,17 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
         }
     }
 
+    @JsonIgnore
+    @Override
+    public String getFirstAttribute(String name) {
+        List<String> attrs = getAttribute(name);
+        if (attrs == null || attrs.isEmpty()) {
+            return null;
+        } else {
+            return attrs.get(0);
+        }
+    }
+
     public BrokeredIdentityContext deserialize(KeycloakSession session, ClientSessionModel clientSession) {
         BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId());
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
index 2acef37..a88d173 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java
@@ -31,8 +31,12 @@ public interface UpdateProfileContext {
 
     Map<String, List<String>> getAttributes();
 
+    void setSingleAttribute(String name, String value);
+
     void setAttribute(String key, List<String> value);
 
+    String getFirstAttribute(String name);
+
     List<String> getAttribute(String key);
 
 }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
index 55d6dda..94a1151 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java
@@ -70,11 +70,21 @@ public class UserUpdateProfileContext implements UpdateProfileContext {
     }
 
     @Override
+    public void setSingleAttribute(String name, String value) {
+        user.setSingleAttribute(name, value);
+    }
+
+    @Override
     public void setAttribute(String key, List<String> value) {
         user.setAttribute(key, value);
     }
 
     @Override
+    public String getFirstAttribute(String name) {
+        return user.getFirstAttribute(name);
+    }
+
+    @Override
     public List<String> getAttribute(String key) {
         return user.getAttribute(key);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
index 9fb60ec..194ad2d 100755
--- a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
+++ b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
@@ -5,6 +5,7 @@ import java.util.List;
 
 import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
 import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
+import org.keycloak.models.Constants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 
@@ -29,11 +30,12 @@ public class AttributeFormDataProcessor {
 
     public static void process(MultivaluedMap<String, String> formData, RealmModel realm, UpdateProfileContext user) {
         for (String key : formData.keySet()) {
-            if (!key.startsWith("user.attributes.")) continue;
-            String attribute = key.substring("user.attributes.".length());
+            if (!key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) continue;
+            String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
 
             // Need to handle case when attribute has multiple values, but in UI was displayed just first value
-            List<String> modelValue = new ArrayList<>(user.getAttribute(attribute));
+            List<String> modelVal = user.getAttribute(attribute);
+            List<String> modelValue = modelVal==null ? new ArrayList<String>() : new ArrayList<>(modelVal);
 
             int index = 0;
             for (String value : formData.get(key)) {