keycloak-uncached

Merge pull request #1160 from patriot1burke/master oidc

4/17/2015 7:36:57 PM

Changes

pom.xml 2(+1 -1)

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
new file mode 100755
index 0000000..15b83e8
--- /dev/null
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java
@@ -0,0 +1,31 @@
+package org.keycloak.broker.provider;
+
+import org.keycloak.broker.provider.IdentityProviderMapper;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractIdentityProviderMapper implements IdentityProviderMapper {
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public IdentityProviderMapper create(KeycloakSession session) {
+        return null;
+    }
+
+    @Override
+    public void init(org.keycloak.Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index 14626c5..479422e 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -1,11 +1,14 @@
 package org.keycloak.broker.oidc;
 
 import org.keycloak.broker.oidc.util.SimpleHttp;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
 import org.keycloak.representations.adapters.action.AdminAction;
 import org.keycloak.representations.adapters.action.LogoutAction;
 import org.keycloak.services.managers.AuthenticationManager;
@@ -23,6 +26,8 @@ import java.security.PublicKey;
  */
 public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
 
+    public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
+
     public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
         super(config);
     }
@@ -32,6 +37,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
         return new KeycloakEndpoint(callback, realm, event);
     }
 
+    @Override
+    protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
+        JsonWebToken access = validateToken(idpKey, response.getToken());
+        context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
+    }
+
     protected class KeycloakEndpoint extends OIDCEndpoint {
         public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
             super(callback, realm, event);
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/RoleMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/RoleMapper.java
new file mode 100755
index 0000000..ff30b2f
--- /dev/null
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/RoleMapper.java
@@ -0,0 +1,230 @@
+package org.keycloak.broker.oidc.mappers;
+
+import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
+import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
+import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.JsonWebToken;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RoleMapper extends AbstractIdentityProviderMapper {
+
+    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 ROLE = "role";
+    public static final String CLAIM = "claim";
+
+    public static final String ID_TOKEN_CLAIM = "id.token.claim";
+
+    public static final String ACCESS_TOKEN_CLAIM = "access.token.claim";
+
+    public static final String CLAIM_VALUE = "claim.value";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(CLAIM);
+        property.setLabel("Claim");
+        property.setHelpText("Name of claim to search for in token.  You can reference nested claims using a '.', i.e. 'address.locality'.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(CLAIM_VALUE);
+        property.setLabel("Claim Value");
+        property.setHelpText("Value the claim must have.  If the claim is an array, then the value must be contained in the array.");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(ID_TOKEN_CLAIM);
+        property.setLabel("ID Token Claim");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property.setDefaultValue("true");
+        property.setHelpText("If this claim is in ID Token, apply role.");
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(ACCESS_TOKEN_CLAIM);
+        property.setLabel("Access Token Claim");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property.setDefaultValue("true");
+        property.setHelpText("If this claim is in Access Token, apply role.");
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(ROLE);
+        property.setLabel("Role");
+        property.setHelpText("Role to grant to user.  To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        configProperties.add(property);
+    }
+
+    public static final String PROVIDER_ID = "oidc-role-idp-mapper";
+
+    public static String[] parseRole(String role) {
+        int scopeIndex = role.indexOf('.');
+        if (scopeIndex > -1) {
+            String appName = role.substring(0, scopeIndex);
+            role = role.substring(scopeIndex + 1);
+            String[] rtn = {appName, role};
+            return rtn;
+        } else {
+            String[] rtn = {null, role};
+            return rtn;
+
+        }
+    }
+
+    public static Object getClaimValue(JsonWebToken token, String claim) {
+        String[] split = claim.split("\\.");
+        Map<String, Object> jsonObject = token.getOtherClaims();
+        for (int i = 0; i < split.length; i++) {
+            if (i == split.length - 1) {
+                return jsonObject.get(split[i]);
+            } else {
+                Object val = jsonObject.get(split[i]);
+                if (!(val instanceof Map)) return null;
+                jsonObject = (Map<String, Object>)val;
+            }
+        }
+        return null;
+    }
+
+
+
+
+
+    @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 "Role Mapper";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Role Mapper";
+    }
+
+    @Override
+    public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String roleName = mapperModel.getConfig().get(ROLE);
+        if (isClaimPresent(mapperModel, context)) {
+            RoleModel role = getRoleFromString(realm, roleName);
+            if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
+            user.grantRole(role);
+        }
+    }
+
+    protected RoleModel getRoleFromString(RealmModel realm, String roleName) {
+        String[] parsedRole = parseRole(roleName);
+        RoleModel role = null;
+        if (parsedRole[0] == null) {
+            role = realm.getRole(parsedRole[1]);
+        } else {
+            ClientModel client = realm.getClientByClientId(parsedRole[0]);
+            role = client.getRole(parsedRole[1]);
+        }
+        return role;
+    }
+
+    protected boolean isClaimPresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        boolean searchAccess = Boolean.valueOf(mapperModel.getConfig().get(ACCESS_TOKEN_CLAIM));
+        boolean searchId = Boolean.valueOf(mapperModel.getConfig().get(ID_TOKEN_CLAIM));
+        String claim = mapperModel.getConfig().get(CLAIM);
+        String desiredValue = mapperModel.getConfig().get(CLAIM_VALUE);
+
+        if (searchAccess) {
+            JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
+            if (token != null) {
+                Object value = getClaimValue(token, claim);
+                if (valueEquals(desiredValue, value)) return true;
+            }
+
+        }
+        if (searchId) {
+            JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
+            if (token != null) {
+                Object value = getClaimValue(token, claim);
+                if (valueEquals(desiredValue, value)) return true;
+            }
+
+        }
+        return false;
+    }
+
+    public boolean valueEquals(String desiredValue, Object value) {
+        if (value instanceof String) {
+            if (desiredValue.equals(value)) return true;
+        } else if (value instanceof Double) {
+            try {
+                if (Double.valueOf(desiredValue).equals(value)) return true;
+            } catch (Exception e) {
+
+            }
+        } else if (value instanceof Integer) {
+            try {
+                if (Integer.valueOf(desiredValue).equals(value)) return true;
+            } catch (Exception e) {
+
+            }
+        } else if (value instanceof Boolean) {
+            try {
+                if (Boolean.valueOf(desiredValue).equals(value)) return true;
+            } catch (Exception e) {
+
+            }
+        } else if (value instanceof List) {
+            List list = (List)value;
+            for (Object val : list) {
+                return valueEquals(desiredValue, val);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+        String roleName = mapperModel.getConfig().get(ROLE);
+        if (!isClaimPresent(mapperModel, context)) {
+            RoleModel role = getRoleFromString(realm, roleName);
+            if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
+            user.deleteRoleMapping(role);
+        }
+
+    }
+
+    @Override
+    public String getHelpText() {
+        return "If a claim exists, grant the user the specified realm or application role.";
+    }
+
+}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 2d0c16b..745e16d 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -95,13 +95,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
     }
 
-    protected boolean verify(JWSInput jws, PublicKey key) {
-        if (key == null) return true;
-        if (!getConfig().isValidateSignature()) return true;
-        return RSAProvider.verify(jws, key);
-
-    }
-
     protected class OIDCEndpoint extends Endpoint {
         public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
             super(callback, realm, event);
@@ -160,6 +153,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return authorizationUrl;
     }
 
+    protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
+
+    }
+
     @Override
     protected BrokeredIdentityContext getFederatedIdentity(String response) {
         AccessTokenResponse tokenResponse = null;
@@ -175,7 +172,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
 
 
-        JsonWebToken idToken = validateIdToken(key, encodedIdToken);
+        JsonWebToken idToken = validateToken(key, encodedIdToken);
 
         try {
             String id = idToken.getSubject();
@@ -197,6 +194,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             }
             identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
             identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
+            processAccessTokenResponse(identity, key, tokenResponse);
 
             identity.setId(id);
             identity.setName(name);
@@ -236,23 +234,34 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return accessToken;
     }
 
-   private JsonWebToken validateIdToken(PublicKey key, String encodedToken) {
+    protected boolean verify(JWSInput jws, PublicKey key) {
+        if (key == null) return true;
+        if (!getConfig().isValidateSignature()) return true;
+        return RSAProvider.verify(jws, key);
+
+    }
+
+    protected JsonWebToken validateToken(PublicKey key, String encodedToken) {
         if (encodedToken == null) {
-            throw new IdentityBrokerException("No id_token from server.");
+            throw new IdentityBrokerException("No token from server.");
         }
 
         try {
             JWSInput jws = new JWSInput(encodedToken);
             if (!verify(jws, key)) {
-                throw new IdentityBrokerException("IDToken signature validation failed");
+                throw new IdentityBrokerException("token signature validation failed");
             }
-            JsonWebToken idToken = jws.readJsonContent(JsonWebToken.class);
+            JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
 
-            String aud = idToken.getAudience();
-            String iss = idToken.getIssuer();
+            String aud = token.getAudience();
+            String iss = token.getIssuer();
 
             if (aud != null && !aud.equals(getConfig().getClientId())) {
-                throw new IdentityBrokerException("Wrong audience from id_token..");
+                throw new IdentityBrokerException("Wrong audience from token.");
+            }
+
+            if (!token.isActive()) {
+                throw new IdentityBrokerException("Token is no longer valid");
             }
 
             String trustedIssuers = getConfig().getIssuer();
@@ -262,15 +271,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
                 for (String trustedIssuer : issuers) {
                     if (iss != null && iss.equals(trustedIssuer.trim())) {
-                        return idToken;
+                        return token;
                     }
                 }
 
-                throw new IdentityBrokerException("Wrong issuer from id_token. Got: " + iss + " expected: " + getConfig().getIssuer());
+                throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
             }
-            return idToken;
+            return token;
         } catch (IOException e) {
-            throw new IdentityBrokerException("Could not decode id token.", e);
+            throw new IdentityBrokerException("Could not decode token.", e);
         }
     }
 
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
new file mode 100755
index 0000000..98ce5f7
--- /dev/null
+++ b/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
@@ -0,0 +1 @@
+org.keycloak.broker.oidc.mappers.RoleMapper
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderMapperTypeRepresentation.java
index c76f6ee..81fdf0f 100755
--- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderMapperTypeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderMapperTypeRepresentation.java
@@ -1,5 +1,6 @@
 package org.keycloak.representations.idm;
 
+import java.util.LinkedList;
 import java.util.List;
 
 /**
@@ -12,7 +13,7 @@ public class IdentityProviderMapperTypeRepresentation {
     protected String category;
     protected String helpText;
 
-    protected List<ConfigPropertyRepresentation> properties;
+    protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
 
     public String getId() {
         return id;
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index 1f51c21..010eae4 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.Pattern;
 
 import org.codehaus.jackson.annotate.JsonAnyGetter;
 import org.codehaus.jackson.annotate.JsonAnySetter;
@@ -12,6 +13,7 @@ import org.codehaus.jackson.annotate.JsonUnwrapped;
 import org.junit.Assert;
 import org.junit.Test;
 import org.keycloak.representations.IDToken;
+import org.keycloak.representations.JsonWebToken;
 import org.keycloak.representations.adapters.config.AdapterConfig;
 import org.keycloak.util.JsonSerialization;
 
@@ -21,6 +23,32 @@ import org.keycloak.util.JsonSerialization;
 public class JsonParserTest {
 
     @Test
+    public void regex() throws Exception {
+        Pattern p = Pattern.compile(".*(?!\\.pdf)");
+        if (p.matcher("foo.pdf").matches()) {
+            System.out.println(".pdf no match");
+        }
+        if (p.matcher("foo.txt").matches()) {
+            System.out.println("foo.txt matches");
+
+        }
+
+    }
+
+    @Test
+    public void testOtherClaims() throws Exception {
+        String json = "{ \"floatData\" : 555.5," +
+                "\"boolData\": true, " +
+                "\"intData\": 1234," +
+                "\"array\": [ \"val\", \"val2\"] }";
+        JsonWebToken token = JsonSerialization.readValue(json, JsonWebToken.class);
+        System.out.println(token.getOtherClaims().get("floatData").getClass().getName());
+        System.out.println(token.getOtherClaims().get("boolData").getClass().getName());
+        System.out.println(token.getOtherClaims().get("intData").getClass().getName());
+        System.out.println(token.getOtherClaims().get("array").getClass().getName());
+    }
+
+    @Test
     public void testUnwrap() throws Exception {
         // just experimenting with unwrapped and any properties
         IDToken test = new IDToken();
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 1fe3440..e696714 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -206,6 +206,58 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'RealmIdentityProviderExportCtrl'
         })
+        .when('/realms/:realm/identity-provider-mappers/:alias/mappers', {
+            templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mappers.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                identityProvider : function(IdentityProviderLoader) {
+                    return IdentityProviderLoader();
+                },
+                mapperTypes : function(IdentityProviderMapperTypesLoader) {
+                    return IdentityProviderMapperTypesLoader();
+                },
+                mappers : function(IdentityProviderMappersLoader) {
+                    return IdentityProviderMappersLoader();
+                }
+            },
+            controller : 'IdentityProviderMapperListCtrl'
+        })
+        .when('/realms/:realm/identity-provider-mappers/:alias/mappers/:mapperId', {
+            templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                identityProvider : function(IdentityProviderLoader) {
+                    return IdentityProviderLoader();
+                },
+                mapperTypes : function(IdentityProviderMapperTypesLoader) {
+                    return IdentityProviderMapperTypesLoader();
+                },
+                mapper : function(IdentityProviderMapperLoader) {
+                    return IdentityProviderMapperLoader();
+                }
+            },
+            controller : 'IdentityProviderMapperCtrl'
+        })
+        .when('/create/identity-provider-mappers/:realm/:alias', {
+            templateUrl : function(params){ return resourceUrl + '/partials/identity-provider-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                identityProvider : function(IdentityProviderLoader) {
+                    return IdentityProviderLoader();
+                },
+                mapperTypes : function(IdentityProviderMapperTypesLoader) {
+                    return IdentityProviderMapperTypesLoader();
+                }
+            },
+            controller : 'IdentityProviderMapperCreateCtrl'
+        })
+
         .when('/realms/:realm/default-roles', {
             templateUrl : resourceUrl + '/partials/realm-default-roles.html',
             resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 2324d48..13941c3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -1066,30 +1066,6 @@ module.controller('ClientClusteringNodeCtrl', function($scope, client, Client, C
     }
 });
 
-module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, serverInfo,
-                                                    ClientProtocolMappersByProtocol,
-                                                    $http, $location, Dialog, Notifications) {
-    $scope.realm = realm;
-    $scope.client = client;
-    if (client.protocol == null) {
-        client.protocol = 'openid-connect';
-    }
-
-    var protocolMappers = serverInfo.protocolMapperTypes[client.protocol];
-    var mapperTypes = {};
-    for (var i = 0; i < protocolMappers.length; i++) {
-        mapperTypes[protocolMappers[i].id] = protocolMappers[i];
-    }
-    $scope.mapperTypes = mapperTypes;
-
-
-    var updateMappers = function() {
-        $scope.mappers = ClientProtocolMappersByProtocol.query({realm : realm.realm, client : client.id, protocol : client.protocol});
-    };
-
-    updateMappers();
-});
-
 module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client, serverInfo,
                                                             ClientProtocolMappersByProtocol,
                                                             $http, $location, Dialog, Notifications) {
@@ -1152,6 +1128,30 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client
 
 });
 
+module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, serverInfo,
+                                                           ClientProtocolMappersByProtocol,
+                                                           $http, $location, Dialog, Notifications) {
+    $scope.realm = realm;
+    $scope.client = client;
+    if (client.protocol == null) {
+        client.protocol = 'openid-connect';
+    }
+
+    var protocolMappers = serverInfo.protocolMapperTypes[client.protocol];
+    var mapperTypes = {};
+    for (var i = 0; i < protocolMappers.length; i++) {
+        mapperTypes[protocolMappers[i].id] = protocolMappers[i];
+    }
+    $scope.mapperTypes = mapperTypes;
+
+
+    var updateMappers = function() {
+        $scope.mappers = ClientProtocolMappersByProtocol.query({realm : realm.realm, client : client.id, protocol : client.protocol});
+    };
+
+    updateMappers();
+});
+
 module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo, client, mapper, ClientProtocolMapper, Notifications, Dialog, $location) {
     $scope.realm = realm;
     $scope.client = client;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 72c3f79..c5479e0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -803,7 +803,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
             });
     };
     $scope.$watch('fromUrl.data', function(newVal, oldVal){
-        console.log('watch fromUrl: ' + newVal + " " + oldVal);
         if ($scope.fromUrl.data && $scope.fromUrl.data.length > 0) {
             $scope.importUrl = true;
         } else{
@@ -1408,3 +1407,99 @@ module.controller('RealmBruteForceCtrl', function($scope, Realm, realm, $http, $
 });
 
 
+module.controller('IdentityProviderMapperListCtrl', function($scope, realm, identityProvider, mapperTypes, mappers) {
+    $scope.realm = realm;
+    $scope.identityProvider = identityProvider;
+    $scope.mapperTypes = mapperTypes;
+    $scope.mappers = mappers;
+});
+
+module.controller('IdentityProviderMapperCtrl', function($scope, realm,  identityProvider, mapperTypes, mapper, IdentityProviderMapper, Notifications, Dialog, $location) {
+    $scope.realm = realm;
+    $scope.identityProvider = identityProvider;
+    $scope.create = false;
+    $scope.mapper = angular.copy(mapper);
+    $scope.changed = false;
+    $scope.mapperType = mapperTypes[mapper.identityProviderMapper];
+    $scope.$watch(function() {
+        return $location.path();
+    }, function() {
+        $scope.path = $location.path().substring(1).split("/");
+    });
+
+    $scope.$watch('mapper', function() {
+        if (!angular.equals($scope.mapper, mapper)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.save = function() {
+        IdentityProviderMapper.update({
+            realm : realm.realm,
+            client: client.id,
+            mapperId : mapper.id
+        }, $scope.mapper, function() {
+            $scope.changed = false;
+            mapper = angular.copy($scope.mapper);
+            $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + mapper.id);
+            Notifications.success("Your changes have been saved.");
+        });
+    };
+
+    $scope.reset = function() {
+        $scope.mapper = angular.copy(mapper);
+        $scope.changed = false;
+    };
+
+    $scope.cancel = function() {
+        //$location.url("/realms");
+        window.history.back();
+    };
+
+    $scope.remove = function() {
+        Dialog.confirmDelete($scope.mapper.name, 'mapper', function() {
+            IdentityProviderMapper.remove({ realm: realm.realm, alias: mapper.identityProviderAlias, mapperId : $scope.mapper.id }, function() {
+                Notifications.success("The mapper has been deleted.");
+                $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers");
+            });
+        });
+    };
+
+});
+
+module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, identityProvider, mapperTypes, IdentityProviderMapper, Notifications, Dialog, $location) {
+    $scope.realm = realm;
+    $scope.identityProvider = identityProvider;
+    $scope.create = true;
+    $scope.mapper = { identityProviderAlias: identityProvider.alias, config: {}};
+    $scope.mapperTypes = mapperTypes;
+
+    $scope.$watch(function() {
+        return $location.path();
+    }, function() {
+        $scope.path = $location.path().substring(1).split("/");
+    });
+
+    $scope.save = function() {
+        $scope.mapper.identityProviderMapper = $scope.mapperType.id;
+        IdentityProviderMapper.save({
+            realm : realm.realm, alias: identityProvider.alias
+        }, $scope.mapper, function(data, headers) {
+            var l = headers().location;
+            var id = l.substring(l.lastIndexOf("/") + 1);
+            $location.url("/realms/" + realm.realm + '/identity-provider-mappers/' + identityProvider.alias + "/mappers/" + id);
+            Notifications.success("Mapper has been created.");
+        });
+    };
+
+    $scope.cancel = function() {
+        //$location.url("/realms");
+        window.history.back();
+    };
+
+
+});
+
+
+
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index b6e0541..bf36307 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -266,4 +266,33 @@ module.factory('IdentityProviderFactoryLoader', function(Loader, IdentityProvide
             provider_id: $route.current.params.provider_id
         }
     });
-});
\ No newline at end of file
+});
+
+module.factory('IdentityProviderMapperTypesLoader', function(Loader, IdentityProviderMapperTypes, $route, $q) {
+    return Loader.get(IdentityProviderMapperTypes, function () {
+        return {
+            realm: $route.current.params.realm,
+            alias: $route.current.params.alias
+        }
+    });
+});
+
+module.factory('IdentityProviderMappersLoader', function(Loader, IdentityProviderMappers, $route, $q) {
+    return Loader.query(IdentityProviderMappers, function () {
+        return {
+            realm: $route.current.params.realm,
+            alias: $route.current.params.alias
+        }
+    });
+});
+
+module.factory('IdentityProviderMapperLoader', function(Loader, IdentityProviderMapper, $route, $q) {
+    return Loader.get(IdentityProviderMapper, function () {
+        return {
+            realm: $route.current.params.realm,
+            alias: $route.current.params.alias,
+            mapperId: $route.current.params.mapperId
+        }
+    });
+});
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 3d1cd8d..6703973 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1004,4 +1004,27 @@ module.factory('IdentityProviderFactory', function($resource) {
         realm : '@realm',
         provider_id : '@provider_id'
     });
-});
\ No newline at end of file
+});
+
+module.factory('IdentityProviderMapperTypes', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mapper-types', {
+        realm : '@realm',
+        alias : '@alias'
+    });
+});
+
+module.factory('IdentityProviderMappers', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mappers', {
+        realm : '@realm',
+        alias : '@alias'
+    });
+});
+
+module.factory('IdentityProviderMapper', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/mappers/:mapperId', {
+        realm : '@realm',
+        alias : '@alias',
+        mapperId: '@mapperId'
+    });
+});
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
new file mode 100755
index 0000000..4e1fdc9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
@@ -0,0 +1,86 @@
+<div class="bs-sidebar col-sm-3 " data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
+<div id="content-area" class="col-sm-9" role="main">
+    <kc-navigation-client></kc-navigation-client>
+    <div id="content">
+        <ol class="breadcrumb" data-ng-show="create">
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Identity Provider Mappers</a></li>
+            <li class="active">Create IdentityProvider Mapper</li>
+        </ol>
+
+        <ol class="breadcrumb" data-ng-hide="create">
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Identity Provider Mappers</a></li>
+            <li class="active">{{mapper.name}}</li>
+        </ol>
+        <h2 class="pull-left" data-ng-hide="create">{{mapper.name}} Identity Provider Mapper</h2>
+        <h2 class="pull-left" data-ng-show="create">Create Identity Provider Mapper</h2>
+        <p class="subtitle"><span class="required">*</span> Required fields</p>
+        <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+
+            <fieldset>
+                <div class="form-group clearfix" data-ng-show="!create">
+                    <label class="col-sm-2 control-label" for="mapperId">ID </label>
+                    <div class="col-sm-4">
+                        <input class="form-control" id="mapperId" type="text" ng-model="mapper.id" readonly>
+                    </div>
+                </div>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="name">Name <span class="required">*</span></label>
+                    <div class="col-sm-4">
+                        <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
+                    </div>
+                    <span tooltip-placement="right" tooltip="Name of the mapper." class="fa fa-info-circle"></span>
+                </div>
+                <div class="form-group" data-ng-show="create">
+                    <label class="col-sm-2 control-label" for="mapperTypeCreate">Mapper Type</label>
+                    <div class="col-sm-6">
+                        <div class="select-kc">
+                            <select id="mapperTypeCreate"
+                                    ng-model="mapperType"
+                                    ng-options="mapperType.name for (mapperKey, mapperType) in mapperTypes">
+                            </select>
+                        </div>
+                    </div>
+                    <span tooltip-placement="right" tooltip="{{mapperType.helpText}}" class="fa fa-info-circle"></span>
+                </div>
+                <div class="form-group clearfix" data-ng-hide="create">
+                    <label class="col-sm-2 control-label" for="mapperType">Mapper Type</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" id="mapperType" type="text" ng-model="mapperType.name" data-ng-readonly="true">
+                    </div>
+                    <span tooltip-placement="right" tooltip="{{mapperType.helpText}}" class="fa fa-info-circle"></span>
+                </div>
+                <div data-ng-repeat="option in mapperType.properties" class="form-group">
+                    <label class="col-sm-2 control-label">{{option.label}}</label>
+
+                    <div class="col-sm-4" data-ng-hide="option.type == 'boolean' || option.type == 'List'">
+                        <input class="form-control" type="text" data-ng-model="mapper.config[ option.name ]" >
+                    </div>
+                    <div class="col-sm-4" data-ng-show="option.type == 'boolean'">
+                        <input ng-model="mapper.config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchmodel />
+                    </div>
+                    <div class="col-sm-4" data-ng-show="option.type == 'List'">
+                        <select ng-model="mapper.config[ option.name ]" ng-options="data for data in option.defaultValue">
+                            <option value="" selected> Select one... </option>
+                        </select>
+                    </div>
+                    <span tooltip-placement="right" tooltip="{{option.helpText}}" class="fa fa-info-circle"></span>
+                </div>
+
+            </fieldset>
+            <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
+                <button kc-cancel data-ng-click="cancel()">Cancel</button>
+                <button kc-save>Save</button>
+            </div>
+
+            <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
+                <button kc-reset data-ng-show="changed">Clear changes</button>
+                <button kc-save  data-ng-show="changed">Save</button>
+                <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+            </div>
+        </form>
+    </div>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
new file mode 100755
index 0000000..2449609
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
@@ -0,0 +1,46 @@
+<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
+<div id="content-area" class="col-md-9" role="main">
+    <kc-navigation-client></kc-navigation-client>
+    <div id="content">
+        <ol class="breadcrumb">
+            <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
+            <li class="active"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}} Provider</a></li>
+            <li class="active">{{identityProvider.alias}} Mappers</li>
+        </ol>
+        <h2><span>{{realm.realm}} </span> {{identityProvider.alias}} Identity Provider Mappers  <span tooltip-placement="right" tooltip="Identity Provider Mappers perform transformation on tokens and documents.  They an do things like map external tokens and claims into role grants and user attributes." class="fa fa-info-circle"></span></h2>
+        <table class="table table-striped table-bordered">
+            <thead>
+            <tr>
+                <th class="kc-table-actions" colspan="4">
+                    <div class="search-comp clearfix">
+                        <input type="text" placeholder="Search..." class="form-control search" data-ng-model="search.name"
+                               onkeyup="if(event.keyCode == 13){$(this).next('button').click();}">
+                        <button type="submit" class="kc-icon-search" tooltip-placement="right"
+                                tooltip="Search by mapper name.">
+                            Icon: search
+                        </button>
+                    </div>
+                    <div class="pull-right">
+                        <a class="btn btn-primary" href="#/create/identity-provider-mappers/{{realm.realm}}/{{identityProvider.alias}}">Create</a>
+                    </div>
+                </th>
+            </tr>
+            <tr data-ng-hide="mappers.length == 0">
+                <th>Name</th>
+                <th>Category</th>
+                <th>Type</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-repeat="mapper in mappers | filter:search">
+                <td><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
+                <td>{{mapperTypes[mapper.identityProviderMapper].category}}</td>
+                <td>{{mapperTypes[mapper.identityProviderMapper].name}}</td>
+            </tr>
+            <tr data-ng-show="mappers.length == 0">
+                <td>No mappers available</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index 67b0a68..0d1d027 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -185,6 +185,7 @@
                 </fieldset>
 
                 <div class="pull-right form-actions">
+                    <a data-ng-show="!newIdentityProvider" class="btn btn-lg btn-primary" href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">Mappers</a>
                     <button kc-save data-ng-show="changed">Save</button>
                     <button type="submit" data-ng-click="cancel()" data-ng-show="changed" class="btn btn-lg btn-default">Cancel</button>
                     <button kc-delete data-ng-click="remove()" data-ng-show="!newIdentityProvider">Delete</button>

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 87ac607..326ec90 100755
--- a/pom.xml
+++ b/pom.xml
@@ -588,7 +588,7 @@
                     <version>2.16</version>
                     <configuration>
                         <forkMode>once</forkMode>
-                        <argLine>-Xms512m -Xmx512m -XX:MaxPermSize=256m</argLine>
+                        <argLine>-Xms512m -Xmx1024m -XX:MaxPermSize=512m</argLine>
                     </configuration>
                 </plugin>
                 <plugin>
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
index fcc1d63..35cce00 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -43,8 +43,10 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author Pedro Igor
@@ -194,10 +196,10 @@ public class IdentityProviderResource {
     @GET
     @Path("mapper-types")
     @NoCache
-    public List<IdentityProviderMapperTypeRepresentation> getMapperTypes() {
+    public Map<String, IdentityProviderMapperTypeRepresentation> getMapperTypes() {
         this.auth.requireView();
         KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
-        List<IdentityProviderMapperTypeRepresentation> types = new LinkedList<>();
+        Map<String, IdentityProviderMapperTypeRepresentation> types = new HashMap<>();
         List<ProviderFactory> factories = sessionFactory.getProviderFactories(IdentityProviderMapper.class);
         for (ProviderFactory factory : factories) {
             IdentityProviderMapper mapper = (IdentityProviderMapper)factory;
@@ -218,7 +220,7 @@ public class IdentityProviderResource {
                         propRep.setHelpText(prop.getHelpText());
                         rep.getProperties().add(propRep);
                     }
-                    types.add(rep);
+                    types.put(rep.getId(), rep);
 
                 }
             }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 91ced7f..1d99e1f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -24,6 +24,7 @@ package org.keycloak.testsuite.account;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
@@ -156,6 +157,11 @@ public class AccountTest {
         });
     }
 
+    //@Test
+    public void ideTesting() throws Exception {
+        Thread.sleep(100000000);
+    }
+
     @Test
     public void returnToAppFromQueryParam() {
         driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app");