keycloak-aplcache

username mapper

5/8/2015 9:41:31 PM

Changes

Details

diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index f4ddeae..e402e04 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -74,6 +74,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> 
     }
 
     @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context) {
+
+    }
+
+    @Override
     public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
 
     }
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
index 28286ee..898cee4 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java
@@ -18,8 +18,10 @@
 package org.keycloak.broker.provider;
 
 import org.keycloak.Config;
+import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
 
 import java.io.InputStream;
 import java.util.HashMap;
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 15b83e8..092f23b 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,8 +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;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -28,4 +30,9 @@ public abstract class AbstractIdentityProviderMapper implements IdentityProvider
     public void postInit(KeycloakSessionFactory factory) {
 
     }
+
+    @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, 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 7bee936..f90f803 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
@@ -32,6 +32,7 @@ public class BrokeredIdentityContext {
 
     private String id;
     private String username;
+    private String modelUsername;
     private String email;
     private String firstName;
     private String lastName;
@@ -59,6 +60,11 @@ public class BrokeredIdentityContext {
         this.id = id;
     }
 
+    /**
+     * Username in remote idp
+     *
+     * @return
+     */
     public String getUsername() {
         return username;
     }
@@ -67,6 +73,19 @@ public class BrokeredIdentityContext {
         this.username = username;
     }
 
+    /**
+     * username to store in UserModel
+     *
+     * @return
+     */
+    public String getModelUsername() {
+        return modelUsername;
+    }
+
+    public void setModelUsername(String modelUsername) {
+        this.modelUsername = modelUsername;
+    }
+
     public String getEmail() {
         return email;
     }
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
index 2c503bb..47037fa 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.broker.provider;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -46,6 +47,8 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
         public Response authenticated(BrokeredIdentityContext context);
     }
 
+
+    void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
     void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context);
     void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
     void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
old mode 100644
new mode 100755
index 1f1bdcc..db2570d
--- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
@@ -18,6 +18,8 @@
 package org.keycloak.broker.provider;
 
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ProviderFactory;
 
 import java.io.InputStream;
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 4017499..d20aa00 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
@@ -19,7 +19,37 @@ public interface IdentityProviderMapper extends Provider, ProviderFactory<Identi
     String getDisplayCategory();
     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.
+     *
+     * @param session
+     * @param realm
+     * @param mapperModel
+     * @param context
+     */
+    void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
+
+    /**
+     * Called after UserModel is created for first time for this user.
+     *
+     * @param session
+     * @param realm
+     * @param user
+     * @param mapperModel
+     * @param context
+     */
     void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
+
+    /**
+     * Called when this user has logged in before and has already been imported.
+     *
+     * @param session
+     * @param realm
+     * @param user
+     * @param mapperModel
+     * @param context
+     */
     void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
 
 
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
index b59a427..8d9f44a 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
@@ -17,10 +17,15 @@
  */
 package org.keycloak.broker.oidc;
 
+import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 
 import java.io.InputStream;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -50,4 +55,5 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
         return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
 
     }
-}
+
+ }
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
index f7ebf94..85ec561 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
@@ -33,8 +33,12 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
         return null;
     }
 
-    protected Object getClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+    public static Object getClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String claim = mapperModel.getConfig().get(CLAIM);
+        return getClaimValue(context, claim);
+    }
+
+    public static Object getClaimValue(BrokeredIdentityContext context, String claim) {
         {  // search access token
             JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
             if (token != null) {
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
new file mode 100755
index 0000000..b648f5f
--- /dev/null
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java
@@ -0,0 +1,109 @@
+package org.keycloak.broker.oidc.mappers;
+
+import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
+import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UsernameTemplateMapper extends AbstractClaimMapper {
+
+    public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    public static final String TEMPLATE = "template";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(TEMPLATE);
+        property.setLabel("Template");
+        property.setHelpText("Template to use to format the username to import.  Substitutions are enclosed in ${}.  For example: '${ALIAS}.${CLAIM.sub}'.  ALIAS is the provider alias.  CLAIM.<NAME> references an ID or Access token claim.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
+        configProperties.add(property);
+    }
+
+    public static final String PROVIDER_ID = "oidc-username-idp-mapper";
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String[] getCompatibleProviders() {
+        return COMPATIBLE_PROVIDERS;
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return "Preprocessor";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Username Template Importer";
+    }
+
+    @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) {
+    }
+
+    static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
+
+    @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String template = mapperModel.getConfig().get(TEMPLATE);
+        Matcher m = substitution.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String variable = m.group(1);
+            if (variable.equals("ALIAS")) {
+                m.appendReplacement(sb, context.getIdpConfig().getAlias());
+            } else if (variable.equals("UUID")) {
+                m.appendReplacement(sb, KeycloakModelUtils.generateId());
+            } else if (variable.startsWith("CLAIM.")) {
+                String name = variable.substring("CLAIM.".length());
+                Object value = AbstractClaimMapper.getClaimValue(context, name);
+                if (value == null) value = "";
+                m.appendReplacement(sb, value.toString());
+            } else {
+                m.appendReplacement(sb, m.group(1));
+            }
+
+        }
+        m.appendTail(sb);
+        String username = sb.toString();
+        context.setModelUsername(username);
+
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Format the username to import.";
+    }
+}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
index ca0c1bb..c959aec 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -17,11 +17,15 @@
  */
 package org.keycloak.broker.oidc;
 
+import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.jose.jwk.JWK;
 import org.keycloak.jose.jwk.JWKParser;
+import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
 import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
@@ -30,6 +34,7 @@ import org.keycloak.util.JsonSerialization;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.PublicKey;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -89,7 +94,7 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
 
                 }
             } catch (IOException e) {
-                throw new RuntimeException("F   ailed to query JWKSet from: " + uri, e);
+                throw new RuntimeException("Failed to query JWKSet from: " + uri, e);
             }
 
         }
@@ -99,4 +104,5 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
     protected static boolean keyTypeSupported(String type) {
         return type != null && type.equals("RSA");
     }
+
 }
diff --git a/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
index de86af2..62e6741 100755
--- a/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
+++ b/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
@@ -1,3 +1,4 @@
 org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
 org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
-org.keycloak.broker.oidc.mappers.UserAttributeMapper
\ No newline at end of file
+org.keycloak.broker.oidc.mappers.UserAttributeMapper
+org.keycloak.broker.oidc.mappers.UsernameTemplateMapper
\ No newline at end of file
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
new file mode 100755
index 0000000..0ed0c1b
--- /dev/null
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java
@@ -0,0 +1,133 @@
+package org.keycloak.broker.saml.mappers;
+
+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.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.assertion.SubjectType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
+
+    public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    public static final String TEMPLATE = "template";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(TEMPLATE);
+        property.setLabel("Template");
+        property.setHelpText("Template to use to format the username to import.  Substitutions are enclosed in ${}.  For example: '${ALIAS}.${NAMEID}'.  ALIAS is the provider alias.  NAMEID is that SAML name id assertion.  ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        property.setDefaultValue("${ALIAS}.${NAMEID}");
+        configProperties.add(property);
+    }
+
+    public static final String PROVIDER_ID = "saml-username-idp-mapper";
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String[] getCompatibleProviders() {
+        return COMPATIBLE_PROVIDERS;
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return "Preprocessor";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Username Template Importer";
+    }
+
+    @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) {
+
+    }
+    static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
+
+    @Override
+    public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+        String template = mapperModel.getConfig().get(TEMPLATE);
+        Matcher m = substitution.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String variable = m.group(1);
+            if (variable.equals("ALIAS")) {
+                m.appendReplacement(sb, context.getIdpConfig().getAlias());
+            } else if (variable.equals("UUID")) {
+                m.appendReplacement(sb, KeycloakModelUtils.generateId());
+            } else if (variable.equals("NAMEID")) {
+                SubjectType subject = assertion.getSubject();
+                SubjectType.STSubType subType = subject.getSubType();
+                NameIDType subjectNameID = (NameIDType) subType.getBaseID();
+                m.appendReplacement(sb, subjectNameID.getValue());
+            } else if (variable.startsWith("ATTRIBUTE.")) {
+                String name = variable.substring("ATTRIBUTE.".length());
+                String value = "";
+                for (AttributeStatementType statement : assertion.getAttributeStatements()) {
+                    for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
+                        AttributeType attr = choice.getAttribute();
+                        if (name.equals(attr.getName()) || name.equals(attr.getFriendlyName())) {
+                            List<Object> attributeValue = attr.getAttributeValue();
+                            if (attributeValue != null && !attributeValue.isEmpty()) {
+                                value = attributeValue.get(0).toString();
+                            }
+                            break;
+                        }
+                    }
+                }
+                m.appendReplacement(sb, value);
+            } else {
+                m.appendReplacement(sb, m.group(1));
+            }
+
+        }
+        m.appendTail(sb);
+        context.setModelUsername(sb.toString());
+
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Format the username to import.";
+    }
+
+}
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
index 98ad908..1cd28fd 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
@@ -18,13 +18,17 @@
 package org.keycloak.broker.saml;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.broker.saml.mappers.UsernameTemplateMapper;
 import org.keycloak.dom.saml.v2.metadata.EndpointType;
 import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
 import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
 import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
 import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
 import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
 import org.keycloak.saml.common.exceptions.ParsingException;
 import org.keycloak.saml.common.util.DocumentUtil;
@@ -150,4 +154,5 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
     public String getId() {
         return PROVIDER_ID;
     }
+
 }
diff --git a/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
index 9de7273..89cdf92 100755
--- a/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
+++ b/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
@@ -1,2 +1,3 @@
 org.keycloak.broker.saml.mappers.AttributeToRoleMapper
-org.keycloak.broker.saml.mappers.UserAttributeMapper
\ No newline at end of file
+org.keycloak.broker.saml.mappers.UserAttributeMapper
+org.keycloak.broker.saml.mappers.UsernameTemplateMapper
\ No newline at end of file
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index 010eae4..ed0831d 100755
--- a/core/src/test/java/org/keycloak/JsonParserTest.java
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -4,6 +4,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.codehaus.jackson.annotate.JsonAnyGetter;
@@ -90,4 +91,21 @@ public class JsonParserTest {
         Assert.assertEquals(100, config.getCorsMaxAge());
         Assert.assertEquals(200, config.getConnectionPoolSize());
     }
+
+    static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
+
+    @Test
+    public void testSub() {
+        String pattern = "${ALIAS}.${CRAP}";
+        Matcher m = substitution.matcher(pattern);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            System.out.println("GROUP: " + m.group(1));
+            m.appendReplacement(sb, m.group(1));
+
+        }
+        m.appendTail(sb);
+        System.out.println(sb.toString());
+    }
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 5264f17..d812a34 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -186,4 +186,21 @@ public class UserEntity {
     public void setFederationLink(String federationLink) {
         this.federationLink = federationLink;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        UserEntity that = (UserEntity) o;
+
+        if (!id.equals(that.id)) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index d0af512..dee433e 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -86,12 +86,21 @@ public class JpaUserProvider implements UserProvider {
     }
 
     private void removeUser(UserEntity user) {
+        String id = user.getId();
         em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate();
         em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate();
         em.createNamedQuery("deleteUserConsentRolesByUser").setParameter("user", user).executeUpdate();
         em.createNamedQuery("deleteUserConsentProtMappersByUser").setParameter("user", user).executeUpdate();
         em.createNamedQuery("deleteUserConsentsByUser").setParameter("user", user).executeUpdate();
-        em.remove(user);
+        em.flush();
+        // not sure why i have to do a clear() here.  I was getting some messed up errors that Hibernate couldn't
+        // un-delete the UserEntity.
+        em.clear();
+        user = em.find(UserEntity.class, id);
+        if (user != null) {
+            em.remove(user);
+        }
+        em.flush();
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/services/managers/UserManager.java b/services/src/main/java/org/keycloak/services/managers/UserManager.java
index c6732b7..3c0252c 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserManager.java
@@ -17,11 +17,11 @@ public class UserManager {
     }
 
     public boolean removeUser(RealmModel realm, UserModel user) {
+        UserSessionProvider sessions = session.sessions();
+        if (sessions != null) {
+            sessions.onUserRemoved(realm, user);
+        }
         if (session.users().removeUser(realm, user)) {
-            UserSessionProvider sessions = session.sessions();
-            if (sessions != null) {
-                sessions.onUserRemoved(realm, user);
-            }
             return true;
         }
         return false;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
index ec33725..ecedae7 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java
@@ -129,7 +129,8 @@ public class IdentityProvidersResource {
         this.auth.requireManage();
 
         try {
-            this.realm.addIdentityProvider(RepresentationToModel.toModel(representation));
+            IdentityProviderModel identityProvider = RepresentationToModel.toModel(representation);
+            this.realm.addIdentityProvider(identityProvider);
 
             return Response.created(uriInfo.getAbsolutePathBuilder().path(representation.getProviderId()).build()).build();
         } catch (ModelDuplicateException e) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index a8119ce..e6660ee 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -44,6 +44,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.AccessToken;
@@ -249,6 +250,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         }
 
         ClientSessionModel clientSession = clientCode.getClientSession();
+        context.getIdp().preprocessFederatedIdentity(session, realmModel, context);
+        Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
+        if (mappers != null) {
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            for (IdentityProviderMapperModel mapper : mappers) {
+                IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
+                target.preprocessFederatedIdentity(session, realmModel, mapper, context);
+            }
+        }
+
         FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(),
                 context.getUsername(), context.getToken());
 
@@ -492,18 +503,24 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
             fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS);
             throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_EMAIL_EXISTS);
         }
-
-        String username = context.getUsername();
-        if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isEmpty(context.getEmail())) {
-            username = context.getEmail();
-        } else if (username == null) {
-            username = context.getIdpConfig().getAlias() + "." + context.getId();
-        } else {
-            username = context.getIdpConfig().getAlias() + "." + context.getUsername();
+        String username = context.getModelUsername();
+        if (username == null) {
+            username = context.getUsername();
+            if (this.realmModel.isRegistrationEmailAsUsername() && !Validation.isEmpty(context.getEmail())) {
+                username = context.getEmail();
+            } else if (username == null) {
+                username = context.getIdpConfig().getAlias() + "." + context.getId();
+            } else {
+                username = context.getIdpConfig().getAlias() + "." + context.getUsername();
+            }
         }
-        if (username != null) {
-            username = username.trim();
+        if (username == null) {
+            LOGGER.warn("Unknown username");
+            fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS);
+            throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS);
+
         }
+        username = username.trim();
 
         existingUser = this.session.users().getUserByUsername(username, this.realmModel);
 
diff --git a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
index d9db0e9..40a883b 100755
--- a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
+++ b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
@@ -33,7 +33,8 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
 
             BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
 
-            user.setUsername(getJsonProperty(profile, "login"));
+            String username = getJsonProperty(profile, "login");
+            user.setUsername(username);
             user.setName(getJsonProperty(profile, "name"));
             user.setEmail(getJsonProperty(profile, "email"));
             user.setIdpConfig(getConfig());
diff --git a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
index c031443..5c5265e 100755
--- a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
+++ b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
@@ -60,7 +60,8 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
 
             BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
 
-			user.setUsername(extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl")));
+            String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
+            user.setUsername(username);
 			user.setName(getJsonProperty(profile, "formattedName"));
 			user.setEmail(getJsonProperty(profile, "emailAddress"));
             user.setIdpConfig(getConfig());
diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
index 3e9cc8c..280753b 100755
--- a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
+++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
@@ -69,7 +69,8 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
 
             BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
 
-			user.setUsername(extractUsernameFromProfileURL(getJsonProperty(profile, "link")));
+            String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link"));
+            user.setUsername(username);
 			user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
 			// email is not provided
 			// user.setEmail(getJsonProperty(profile, "email"));