keycloak-aplcache

merge conflicts

9/30/2016 8:19:12 PM

Changes

.gitignore 3(+3 -0)

Details

.gitignore 3(+3 -0)

diff --git a/.gitignore b/.gitignore
index 44612eb..6d655dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,6 @@ catalog.xml
 #########
 target
 
+# Maven shade
+#############
+*dependency-reduced-pom.xml
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
index 8e06d5a..8d07c00 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java
@@ -17,9 +17,17 @@
 
 package org.keycloak.adapters.authentication;
 
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Map;
+
 import org.keycloak.OAuth2Constants;
 import org.keycloak.adapters.AdapterUtils;
 import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKBuilder;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.JsonWebToken;
 import org.keycloak.common.util.KeystoreUtil;
 import org.keycloak.common.util.Time;
 import org.keycloak.jose.jws.JWSBuilder;
@@ -38,7 +46,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
 
     public static final String PROVIDER_ID = "jwt";
 
-    private PrivateKey privateKey;
+    private KeyPair keyPair;
+    private JWK publicKeyJwk;
+
     private int tokenTimeout;
 
     @Override
@@ -46,8 +56,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
         return PROVIDER_ID;
     }
 
-    public void setPrivateKey(PrivateKey privateKey) {
-        this.privateKey = privateKey;
+    public void setupKeyPair(KeyPair keyPair) {
+        this.keyPair = keyPair;
+        this.publicKeyJwk = JWKBuilder.create().rs256(keyPair.getPublic());
     }
 
     public void setTokenTimeout(int tokenTimeout) {
@@ -58,6 +69,10 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
         return tokenTimeout;
     }
 
+    public PublicKey getPublicKey() {
+        return keyPair.getPublic();
+    }
+
     @Override
     public void init(KeycloakDeployment deployment, Object config) {
         if (config == null || !(config instanceof Map)) {
@@ -88,7 +103,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
         if (clientKeyAlias == null) {
             clientKeyAlias = deployment.getResourceName();
         }
-        this.privateKey = KeystoreUtil.loadPrivateKeyFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
+
+        KeyPair keyPair = KeystoreUtil.loadKeyPairFromKeystore(clientKeystoreFile, clientKeystorePassword, clientKeyPassword, clientKeyAlias, clientKeystoreFormat);
+        setupKeyPair(keyPair);
 
         this.tokenTimeout = asInt(cfg, "token-timeout", 10);
     }
@@ -120,8 +137,9 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
     public String createSignedRequestToken(String clientId, String realmInfoUrl) {
         JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
         return new JWSBuilder()
+                .kid(publicKeyJwk.getKeyId())
                 .jsonContent(jwt)
-                .rsa256(privateKey);
+                .rsa256(keyPair.getPrivate());
     }
 
     protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
index b5506a5..25c899e 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
@@ -17,11 +17,20 @@
 
 package org.keycloak.adapters;
 
+import java.security.PublicKey;
+
 import org.jboss.logging.Logger;
+import org.keycloak.adapters.authentication.ClientCredentialsProvider;
+import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
 import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
 import org.keycloak.adapters.spi.HttpFacade;
 import org.keycloak.adapters.spi.UserSessionManagement;
 import org.keycloak.common.util.StreamUtil;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKBuilder;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.representations.VersionRepresentation;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
@@ -85,6 +94,10 @@ public class PreAuthActionsHandler {
             if (!resolveDeployment()) return true;
             handleTestAvailable();
             return true;
+        } else if (requestUri.endsWith(AdapterConstants.K_JWKS)) {
+            if (!resolveDeployment()) return true;
+            handleJwksRequest();
+            return true;
         }
         return false;
     }
@@ -244,4 +257,26 @@ public class PreAuthActionsHandler {
         }
     }
 
+    protected void handleJwksRequest() {
+        try {
+            JSONWebKeySet jwks = new JSONWebKeySet();
+            ClientCredentialsProvider clientCredentialsProvider = deployment.getClientAuthenticator();
+
+            // For now, just get signature key from JWT provider. We can add more if we support encryption etc.
+            if (clientCredentialsProvider instanceof JWTClientCredentialsProvider) {
+                PublicKey publicKey = ((JWTClientCredentialsProvider) clientCredentialsProvider).getPublicKey();
+                JWK jwk = JWKBuilder.create().rs256(publicKey);
+                jwks.setKeys(new JWK[] { jwk });
+            } else {
+                jwks.setKeys(new JWK[] {});
+            }
+
+            facade.getResponse().setStatus(200);
+            facade.getResponse().setHeader("Content-Type", "application/json");
+            JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), jwks);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
 }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
index cdb59f5..502d0c0 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java
@@ -71,8 +71,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
                     sendRequest(deployment);
                     lastRequestTime = currentTime;
                 } else {
-                    // TODO: debug
-                    log.infof("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
+                    log.debugf("Won't send request to realm jwks url. Last request time was %d", lastRequestTime);
                 }
             }
         }
@@ -83,9 +82,9 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
 
 
     private void sendRequest(KeycloakDeployment deployment) {
-        // Send the request
-        // TODO: trace or remove?
-        log.infof("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
+        if (log.isTraceEnabled()) {
+            log.tracef("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName());
+        }
 
         HttpGet getMethod = new HttpGet(deployment.getJwksUrl());
         try {
@@ -93,8 +92,9 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
 
             Map<String, PublicKey> publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
 
-            // TODO: Debug with condition
-            log.infof("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
+            if (log.isDebugEnabled()) {
+                log.debugf("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString());
+            }
 
             // Update current keys
             currentKeys.clear();
diff --git a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
index 88ecd19..dad2a31 100755
--- a/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
+++ b/common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
@@ -22,8 +22,10 @@ import org.keycloak.common.constants.GenericConstants;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
+import java.security.KeyPair;
 import java.security.KeyStore;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -49,7 +51,7 @@ public class KeystoreUtil {
         return trustStore;
     }
 
-    public static PrivateKey loadPrivateKeyFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) {
+    public static KeyPair loadKeyPairFromKeystore(String keystoreFile, String storePassword, String keyPassword, String keyAlias, KeystoreFormat format) {
         InputStream stream = FindFile.findFile(keystoreFile);
 
         try {
@@ -61,11 +63,12 @@ public class KeystoreUtil {
             }
 
             keyStore.load(stream, storePassword.toCharArray());
-            PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
-            if (key == null) {
+            PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
+            if (privateKey == null) {
                 throw new RuntimeException("Couldn't load key with alias '" + keyAlias + "' from keystore");
             }
-            return key;
+            PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
+            return new KeyPair(publicKey, privateKey);
         } catch (Exception e) {
             throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
         }
diff --git a/core/src/main/java/org/keycloak/constants/AdapterConstants.java b/core/src/main/java/org/keycloak/constants/AdapterConstants.java
index 6458d90..ea1086e 100755
--- a/core/src/main/java/org/keycloak/constants/AdapterConstants.java
+++ b/core/src/main/java/org/keycloak/constants/AdapterConstants.java
@@ -29,6 +29,7 @@ public interface AdapterConstants {
     public static final String K_PUSH_NOT_BEFORE = "k_push_not_before";
     public static final String K_TEST_AVAILABLE = "k_test_available";
     public static final String K_QUERY_BEARER_TOKEN = "k_query_bearer_token";
+    public static final String K_JWKS = "k_jwks";
 
     // This param name is defined again in Keycloak Subsystem class
     // org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor.  We have this value in
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
index 824ff55..ae37ebf 100644
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
@@ -34,6 +34,7 @@ public class JWKBuilder {
     public static final String DEFAULT_PUBLIC_KEY_USE = "sig";
     public static final String DEFAULT_MESSAGE_DIGEST = "SHA-256";
 
+    private String kid;
 
     private JWKBuilder() {
     }
@@ -42,11 +43,18 @@ public class JWKBuilder {
         return new JWKBuilder();
     }
 
+    public JWKBuilder kid(String kid) {
+        this.kid = kid;
+        return this;
+    }
+
     public JWK rs256(PublicKey key) {
         RSAPublicKey rsaKey = (RSAPublicKey) key;
 
         RSAPublicJWK k = new RSAPublicJWK();
-        k.setKeyId(createKeyId(key));
+
+        String kid = this.kid != null ? this.kid : createKeyId(key);
+        k.setKeyId(kid);
         k.setKeyType(RSAPublicJWK.RSA);
         k.setAlgorithm(RSAPublicJWK.RS256);
         k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE);
@@ -56,7 +64,7 @@ public class JWKBuilder {
         return k;
     }
 
-    private String createKeyId(Key key) {
+    public static String createKeyId(Key key) {
         try {
             return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded()));
         } catch (NoSuchAlgorithmException e) {
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 2a7d37b..cf52423 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -81,6 +81,8 @@ public interface OAuth2Constants {
 
     String MAX_AGE = "max_age";
 
+    String JWT = "JWT";
+
 
 }
 
diff --git a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
index 03539ff..d10616c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CertificateRepresentation.java
@@ -27,6 +27,7 @@ public class CertificateRepresentation {
     protected String privateKey;
     protected String publicKey;
     protected String certificate;
+    protected String kid;
 
     public String getPrivateKey() {
         return privateKey;
@@ -52,5 +53,11 @@ public class CertificateRepresentation {
         this.certificate = certificate;
     }
 
+    public String getKid() {
+        return kid;
+    }
 
+    public void setKid(String kid) {
+        this.kid = kid;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
index 2466f12..40b5301 100755
--- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
@@ -25,6 +25,7 @@ import java.util.Map;
 public class IdentityProviderRepresentation {
 
     protected String alias;
+    protected String displayName;
     protected String internalId;
     protected String providerId;
     protected boolean enabled = true;
@@ -176,4 +177,12 @@ public class IdentityProviderRepresentation {
         this.trustEmail = trustEmail;
     }
 
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
 }
diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java
index dbf6cd3..8db20e8 100644
--- a/core/src/main/java/org/keycloak/util/JWKSUtils.java
+++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java
@@ -42,4 +42,15 @@ public class JWKSUtils {
 
         return result;
     }
+
+    public static JWK getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
+        for (JWK jwk : keySet.getKeys()) {
+            JWKParser parser = JWKParser.create(jwk);
+            if (parser.getJwk().getPublicKeyUse().equals(requestedUse.asString()) && parser.isKeyTypeSupported(jwk.getKeyType())) {
+                return jwk;
+            }
+        }
+
+        return null;
+    }
 }
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index d67ce63..856980a 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -89,6 +89,10 @@
                 <local-cache name="loginFailures"/>
                 <local-cache name="authorization"/>
                 <local-cache name="work"/>
+                <local-cache name="keys">
+                    <eviction max-entries="1000" strategy="LRU"/>
+                    <expiration max-idle="3600000" />
+                </local-cache>
             </cache-container>
             <xsl:apply-templates select="node()|@*"/>
         </xsl:copy>
diff --git a/distribution/server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/cli/keycloak-install.cli
index dbb2c32..d62bcf1 100644
--- a/distribution/server-overlay/cli/keycloak-install.cli
+++ b/distribution/server-overlay/cli/keycloak-install.cli
@@ -10,5 +10,8 @@ embed-server --server-config=standalone.xml
 /subsystem=infinispan/cache-container=keycloak/local-cache=work:add()
 /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
 /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU)
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
 /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
 run-batch --file=default-keycloak-subsys-config.cli
\ No newline at end of file
diff --git a/distribution/server-overlay/cli/keycloak-install-ha.cli b/distribution/server-overlay/cli/keycloak-install-ha.cli
index bc7d863..17fd5f0 100644
--- a/distribution/server-overlay/cli/keycloak-install-ha.cli
+++ b/distribution/server-overlay/cli/keycloak-install-ha.cli
@@ -10,5 +10,8 @@ embed-server --server-config=standalone-ha.xml
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC")
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
+/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
 /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
 run-batch --file=default-keycloak-subsys-config.cli
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 7b5f1d9..8ad75fd 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -17,6 +17,8 @@
 
 package org.keycloak.connections.infinispan;
 
+import java.util.concurrent.TimeUnit;
+
 import org.infinispan.configuration.cache.CacheMode;
 import org.infinispan.configuration.cache.Configuration;
 import org.infinispan.configuration.cache.ConfigurationBuilder;
@@ -98,7 +100,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
             cacheManager = (EmbeddedCacheManager) new InitialContext().lookup(cacheContainerLookup);
             containerManaged = true;
 
-            cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(true, InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
+            cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
             cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
 
             long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
@@ -106,9 +108,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
                 maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
             }
 
-            cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(true, maxEntries));
+            cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
             cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
             cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
+            cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
+
             logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
         } catch (Exception e) {
             throw new RuntimeException("Failed to retrieve cache container", e);
@@ -116,6 +120,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
     }
 
     protected void initEmbedded() {
+
+
+        
         GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
 
         boolean clustered = config.getBoolean("clustered", false);
@@ -176,7 +183,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
         counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(false, InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME, getRevisionCacheConfig(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX));
         cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME, true);
 
         long maxEntries = cacheManager.getCache(InfinispanConnectionProvider.USER_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
@@ -184,19 +191,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
             maxEntries = InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX;
         }
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(false, maxEntries));
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(maxEntries));
         cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
+
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
+        cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
     }
 
-    private Configuration getRevisionCacheConfig(boolean managed, long maxEntries) {
+    private Configuration getRevisionCacheConfig(long maxEntries) {
         ConfigurationBuilder cb = new ConfigurationBuilder();
         cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL);
 
-        // Workaround: Use Dummy manager even in managed ( wildfly/eap ) environment. Without this workaround, there is an issue in EAP7 overlay.
-        // After start+end revisions batch is left the JTA transaction in committed state. This is incorrect and causes other issues afterwards.
-        // TODO: Investigate
-        // if (!managed)
-            cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
+        // Use Dummy manager even in managed ( wildfly/eap ) environment. We don't want infinispan to participate in global transaction
+        cb.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
 
         cb.transaction().lockingMode(LockingMode.PESSIMISTIC);
 
@@ -204,4 +211,11 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         return cb.build();
     }
 
+    protected Configuration getKeysCacheConfig() {
+        ConfigurationBuilder cb = new ConfigurationBuilder();
+        cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
+        cb.expiration().maxIdle(InfinispanConnectionProvider.KEYS_CACHE_MAX_IDLE_SECONDS, TimeUnit.SECONDS);
+        return cb.build();
+    }
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 143056c..1468460 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -39,6 +39,10 @@ public interface InfinispanConnectionProvider extends Provider {
     String WORK_CACHE_NAME = "work";
     String AUTHORIZATION_CACHE_NAME = "authorization";
 
+    String KEYS_CACHE_NAME = "keys";
+    int KEYS_CACHE_DEFAULT_MAX = 500;
+    int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
+
 
     <K, V> Cache<K, V> getCache(String name);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java
new file mode 100644
index 0000000..9010b16
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProvider.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.infinispan;
+
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
+import org.keycloak.keys.KeyLoader;
+import org.keycloak.keys.KeyStorageProvider;
+
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanKeyStorageProvider implements KeyStorageProvider {
+
+    private static final Logger log = Logger.getLogger(InfinispanKeyStorageProvider.class);
+
+    private final Cache<String, PublicKeysEntry> keys;
+
+    private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress;
+
+    private final int minTimeBetweenRequests ;
+
+    public InfinispanKeyStorageProvider(Cache<String, PublicKeysEntry> keys,  Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
+        this.keys = keys;
+        this.tasksInProgress = tasksInProgress;
+        this.minTimeBetweenRequests = minTimeBetweenRequests;
+    }
+
+
+    @Override
+    public PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader) {
+        // Check if key is in cache
+        PublicKeysEntry entry = keys.get(modelKey);
+        if (entry != null) {
+            PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
+            if (publicKey != null) {
+                return publicKey;
+            }
+        }
+
+        int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
+        int currentTime = Time.currentTime();
+
+        // Check if we are allowed to send request
+        if (currentTime > lastRequestTime + minTimeBetweenRequests) {
+
+            WrapperCallable wrapperCallable = new WrapperCallable(modelKey, loader);
+            FutureTask<PublicKeysEntry> task = new FutureTask<>(wrapperCallable);
+            FutureTask<PublicKeysEntry> existing = tasksInProgress.putIfAbsent(modelKey, task);
+
+            if (existing == null) {
+                task.run();
+            } else {
+                task = existing;
+            }
+
+            try {
+                entry = task.get();
+
+                // Computation finished. Let's see if key is available
+                PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
+                if (publicKey != null) {
+                    return publicKey;
+                }
+
+            } catch (ExecutionException ee) {
+                throw new RuntimeException("Error when loading public keys", ee);
+            } catch (InterruptedException ie) {
+                throw new RuntimeException("Error. Interrupted when loading public keys", ie);
+            } finally {
+                // Our thread inserted the task. Let's clean
+                if (existing == null) {
+                    tasksInProgress.remove(modelKey);
+                }
+            }
+        } else {
+            log.warnf("Won't load the keys for model '%s' . Last request time was %d", modelKey, lastRequestTime);
+        }
+
+        Set<String> availableKids = entry==null ? Collections.emptySet() : entry.getCurrentKeys().keySet();
+        log.warnf("PublicKey wasn't found in the storage. Requested kid: '%s' . Available kids: '%s'", kid, availableKids);
+
+        return null;
+    }
+
+    private PublicKey getPublicKey(Map<String, PublicKey> publicKeys, String kid) {
+        // Backwards compatibility
+        if (kid == null && !publicKeys.isEmpty()) {
+            return publicKeys.values().iterator().next();
+        } else {
+            return publicKeys.get(kid);
+        }
+    }
+
+
+    @Override
+    public void close() {
+
+    }
+
+
+    private class WrapperCallable implements Callable<PublicKeysEntry> {
+
+        private final String modelKey;
+        private final KeyLoader delegate;
+
+        public WrapperCallable(String modelKey, KeyLoader delegate) {
+            this.modelKey = modelKey;
+            this.delegate = delegate;
+        }
+
+        @Override
+        public PublicKeysEntry call() throws Exception {
+            PublicKeysEntry entry = keys.get(modelKey);
+
+            int lastRequestTime = entry==null ? 0 : entry.getLastRequestTime();
+            int currentTime = Time.currentTime();
+
+            // Check again if we are allowed to send request. There is a chance other task was already finished and removed from tasksInProgress in the meantime.
+            if (currentTime > lastRequestTime + minTimeBetweenRequests) {
+
+                Map<String, PublicKey> publicKeys = delegate.loadKeys();
+
+                if (log.isDebugEnabled()) {
+                    log.debugf("Public keys retrieved successfully for model %s. New kids: %s", modelKey, publicKeys.keySet().toString());
+                }
+
+                entry = new PublicKeysEntry(currentTime, publicKeys);
+
+                keys.put(modelKey, entry);
+            }
+            return entry;
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java
new file mode 100644
index 0000000..3a4ba5a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.infinispan;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.FutureTask;
+
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.keys.KeyStorageProvider;
+import org.keycloak.keys.KeyStorageProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanKeyStorageProviderFactory implements KeyStorageProviderFactory {
+
+    private static final Logger log = Logger.getLogger(InfinispanKeyStorageProviderFactory.class);
+
+    public static final String PROVIDER_ID = "infinispan";
+
+    private Cache<String, PublicKeysEntry> keysCache;
+
+    private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
+
+    private int minTimeBetweenRequests;
+
+    @Override
+    public KeyStorageProvider create(KeycloakSession session) {
+        lazyInit(session);
+        return new InfinispanKeyStorageProvider(keysCache, tasksInProgress, minTimeBetweenRequests);
+    }
+
+    private void lazyInit(KeycloakSession session) {
+        if (keysCache == null) {
+            synchronized (this) {
+                if (keysCache == null) {
+                    this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+        minTimeBetweenRequests = config.getInt("minTimeBetweenRequests", 10);
+        log.debugf("minTimeBetweenRequests is %d", minTimeBetweenRequests);
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java
new file mode 100644
index 0000000..cb0561c
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/PublicKeysEntry.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.infinispan;
+
+import java.io.Serializable;
+import java.security.PublicKey;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PublicKeysEntry implements Serializable {
+
+    private final int lastRequestTime;
+
+    private final Map<String, PublicKey> currentKeys;
+
+    public PublicKeysEntry(int lastRequestTime, Map<String, PublicKey> currentKeys) {
+        this.lastRequestTime = lastRequestTime;
+        this.currentKeys = currentKeys;
+    }
+
+    public int getLastRequestTime() {
+        return lastRequestTime;
+    }
+
+    public Map<String, PublicKey> getCurrentKeys() {
+        return currentKeys;
+    }
+}
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory
new file mode 100644
index 0000000..bed25ea
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.keys.KeyStorageProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.keys.infinispan.InfinispanKeyStorageProviderFactory
\ No newline at end of file
diff --git a/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java b/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java
new file mode 100644
index 0000000..99a5f07
--- /dev/null
+++ b/model/infinispan/src/test/java/org/keycloak/models/keys/infinispan/InfinispanKeyStorageProviderTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.keys.infinispan;
+
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.infinispan.Cache;
+import org.infinispan.configuration.cache.Configuration;
+import org.infinispan.configuration.cache.ConfigurationBuilder;
+import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.eviction.EvictionStrategy;
+import org.infinispan.eviction.EvictionType;
+import org.infinispan.manager.DefaultCacheManager;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.keys.KeyLoader;
+import org.keycloak.keys.infinispan.InfinispanKeyStorageProvider;
+import org.keycloak.keys.infinispan.PublicKeysEntry;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanKeyStorageProviderTest {
+
+    private Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
+
+    Cache<String, PublicKeysEntry> keys = getKeysCache();
+    Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
+    int minTimeBetweenRequests = 10;
+
+    @Before
+    public void before() {
+        Time.setOffset(0);
+    }
+
+    @After
+    public void after() {
+        Time.setOffset(0);
+    }
+
+    @Test
+    public void testConcurrency() throws Exception {
+        // Just one thread will execute the task
+        List<Thread> threads = new LinkedList<>();
+        for (int i=0 ; i<10 ; i++) {
+            Thread t = new Thread(new SampleWorker("model1"));
+            threads.add(t);
+        }
+        startAndJoinAll(threads);
+        Assert.assertEquals(counters.get("model1").get(), 1);
+        threads.clear();
+
+        // model1 won't be executed due to lastRequestTime. model2 will be executed just with one thread
+        for (int i=0 ; i<10 ; i++) {
+            Thread t = new Thread(new SampleWorker("model1"));
+            threads.add(t);
+        }
+        for (int i=0 ; i<10 ; i++) {
+            Thread t = new Thread(new SampleWorker("model2"));
+            threads.add(t);
+        }
+        startAndJoinAll(threads);
+        Assert.assertEquals(counters.get("model1").get(), 1);
+        Assert.assertEquals(counters.get("model2").get(), 1);
+        threads.clear();
+
+        // Increase time offset
+        Time.setOffset(20);
+
+        // Time updated. So another thread should successfully run loader for both model1 and model2
+        for (int i=0 ; i<10 ; i++) {
+            Thread t = new Thread(new SampleWorker("model1"));
+            threads.add(t);
+        }
+        for (int i=0 ; i<10 ; i++) {
+            Thread t = new Thread(new SampleWorker("model2"));
+            threads.add(t);
+        }
+        startAndJoinAll(threads);
+        Assert.assertEquals(counters.get("model1").get(), 2);
+        Assert.assertEquals(counters.get("model2").get(), 2);
+        threads.clear();
+    }
+
+
+    private void startAndJoinAll(List<Thread> threads) throws Exception {
+        for (Thread t : threads) {
+            t.start();
+        }
+        for (Thread t : threads) {
+            t.join();
+        }
+    }
+
+
+    private class SampleWorker implements Runnable {
+
+
+        private final String modelKey;
+
+        private SampleWorker(String modelKey) {
+            this.modelKey = modelKey;
+        }
+
+        @Override
+        public void run() {
+            InfinispanKeyStorageProvider provider = new InfinispanKeyStorageProvider(keys, tasksInProgress, minTimeBetweenRequests);
+            provider.getPublicKey(modelKey, "kid1", new SampleLoader(modelKey));
+        }
+
+    }
+
+
+    private class SampleLoader implements KeyLoader {
+
+        private final String modelKey;
+
+        private SampleLoader(String modelKey) {
+            this.modelKey = modelKey;
+        }
+
+        @Override
+        public Map<String, PublicKey> loadKeys() throws Exception {
+            counters.putIfAbsent(modelKey, new AtomicInteger(0));
+            AtomicInteger currentCounter = counters.get(modelKey);
+
+            currentCounter.incrementAndGet();
+            return Collections.emptyMap();
+        }
+    }
+
+
+    protected Cache<String, PublicKeysEntry> getKeysCache() {
+        GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
+        gcb.globalJmxStatistics().allowDuplicateDomains(true);
+
+        final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
+
+        ConfigurationBuilder cb = new ConfigurationBuilder();
+        cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
+        Configuration cfg = cb.build();
+
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, cfg);
+        return cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
index 53856fd..d8a9d7e 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
@@ -58,6 +58,9 @@ public class IdentityProviderEntity {
     @Column(name="PROVIDER_ALIAS")
     private String alias;
 
+    @Column(name="PROVIDER_DISPLAY_NAME")
+    private String displayName;
+
     @Column(name="ENABLED")
     private boolean enabled;
 
@@ -181,6 +184,14 @@ public class IdentityProviderEntity {
         this.trustEmail = trustEmail;
     }
 
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 71abbad..21aa8d0 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1270,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
 
         for (IdentityProviderEntity entity: entities) {
             IdentityProviderModel identityProviderModel = new IdentityProviderModel();
-
             identityProviderModel.setProviderId(entity.getProviderId());
             identityProviderModel.setAlias(entity.getAlias());
+            identityProviderModel.setDisplayName(entity.getDisplayName());
+
             identityProviderModel.setInternalId(entity.getInternalId());
             Map<String, String> config = entity.getConfig();
             Map<String, String> copy = new HashMap<>();
@@ -1309,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
 
         entity.setInternalId(KeycloakModelUtils.generateId());
         entity.setAlias(identityProvider.getAlias());
+        entity.setDisplayName(identityProvider.getDisplayName());
         entity.setProviderId(identityProvider.getProviderId());
         entity.setEnabled(identityProvider.isEnabled());
         entity.setStoreToken(identityProvider.isStoreToken());
@@ -1342,6 +1344,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
             if (entity.getInternalId().equals(identityProvider.getInternalId())) {
                 entity.setAlias(identityProvider.getAlias());
+                entity.setDisplayName(identityProvider.getDisplayName());
                 entity.setEnabled(identityProvider.isEnabled());
                 entity.setTrustEmail(identityProvider.isTrustEmail());
                 entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
index 6a3f9fe..a73ee6b 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml
@@ -33,8 +33,9 @@
 
          <dropColumn tableName="USER_ENTITY" columnName="TOTP" />
 
-
-
+         <addColumn tableName="IDENTITY_PROVIDER">
+             <column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
+         </addColumn>
      </changeSet>
 
 
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index f88fe52..4ebfca8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -912,6 +912,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
 
             identityProviderModel.setProviderId(entity.getProviderId());
             identityProviderModel.setAlias(entity.getAlias());
+            identityProviderModel.setDisplayName(entity.getDisplayName());
             identityProviderModel.setInternalId(entity.getInternalId());
             Map<String, String> config = entity.getConfig();
             Map<String, String> copy = new HashMap<>();
@@ -948,6 +949,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
 
         entity.setInternalId(KeycloakModelUtils.generateId());
         entity.setAlias(identityProvider.getAlias());
+        entity.setDisplayName(identityProvider.getDisplayName());
         entity.setProviderId(identityProvider.getProviderId());
         entity.setEnabled(identityProvider.isEnabled());
         entity.setTrustEmail(identityProvider.isTrustEmail());
@@ -978,6 +980,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
             if (entity.getInternalId().equals(identityProvider.getInternalId())) {
                 entity.setAlias(identityProvider.getAlias());
+                entity.setDisplayName(identityProvider.getDisplayName());
                 entity.setEnabled(identityProvider.isEnabled());
                 entity.setTrustEmail(identityProvider.isTrustEmail());
                 entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
index 1cfeb19..e536fc8 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/IdentityProviderEntity.java
@@ -26,6 +26,7 @@ public class IdentityProviderEntity {
 
     private String internalId;
     private String alias;
+    private String displayName;
     private String providerId;
     private String name;
     private boolean enabled;
@@ -134,6 +135,14 @@ public class IdentityProviderEntity {
         this.trustEmail = trustEmail;
     }
 
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index 5532606..8f57133 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -32,9 +32,11 @@ import javax.ws.rs.core.UriInfo;
  */
 public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
 
+    protected final KeycloakSession session;
     private final C config;
 
-    public AbstractIdentityProvider(C config) {
+    public AbstractIdentityProvider(KeycloakSession session, C config) {
+        this.session = session;
         this.config = config;
     }
 
diff --git a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
index 93ef720..c189316 100755
--- a/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java
@@ -39,10 +39,11 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
      * <p>Creates an {@link IdentityProvider} based on the configuration contained in
      * <code>model</code>.</p>
      *
+     * @param session
      * @param model The configuration to be used to create the identity provider.
      * @return
      */
-    T create(IdentityProviderModel model);
+    T create(KeycloakSession session, IdentityProviderModel model);
 
     /**
      * <p>Creates an {@link IdentityProvider} based on the configuration from
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java b/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
new file mode 100644
index 0000000..aa5631d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyLoader.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import java.security.PublicKey;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface KeyLoader {
+
+    Map<String, PublicKey> loadKeys() throws Exception;
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java
new file mode 100644
index 0000000..3033f2b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import java.security.PublicKey;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface KeyStorageProvider extends Provider {
+
+
+    /**
+     * Get public key to verify messages signed by particular client. Used for example during JWT client authentication
+     *
+     * @param modelKey
+     * @param kid
+     * @param loader
+     * @return
+     */
+    PublicKey getPublicKey(String modelKey, String kid, KeyLoader loader);
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java
new file mode 100644
index 0000000..2ae9f1d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface KeyStorageProviderFactory extends ProviderFactory<KeyStorageProvider> {
+}
diff --git a/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java b/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java
new file mode 100644
index 0000000..162e186
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/keys/KeyStorageSpi.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeyStorageSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return "keyStorage";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return KeyStorageProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return KeyStorageProviderFactory.class;
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
index 2425c7d..41a1e41 100755
--- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
@@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
 
     private String postBrokerLoginFlowId;
 
+    private String displayName;
+
     /**
      * <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
      * in the map are understood by the identity provider implementation.</p>
@@ -70,6 +72,7 @@ public class IdentityProviderModel implements Serializable {
         this.internalId = model.getInternalId();
         this.providerId = model.getProviderId();
         this.alias = model.getAlias();
+        this.displayName = model.getDisplayName();
         this.config = new HashMap<String, String>(model.getConfig());
         this.enabled = model.isEnabled();
         this.trustEmail = model.isTrustEmail();
@@ -169,5 +172,13 @@ public class IdentityProviderModel implements Serializable {
     public void setTrustEmail(boolean trustEmail) {
         this.trustEmail = trustEmail;
     }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
     
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
index 8d6d5fd..c4b9735 100644
--- a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
@@ -1,13 +1,34 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.models;
 
 /**
- * Denotes an executable Script with metadata.
+ * A representation of a Script with some additional meta-data.
  *
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 public interface ScriptModel {
 
     /**
+     * MIME-Type for JavaScript
+     */
+    String TEXT_JAVASCRIPT = "text/javascript";
+
+    /**
      * Returns the unique id of the script. {@literal null} for ad-hoc created scripts.
      */
     String getId();
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index 6774b6f..c291c42 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -18,6 +18,8 @@
 package org.keycloak.models.utils;
 
 import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.common.util.CertificateUtils;
 import org.keycloak.common.util.PemUtils;
@@ -685,4 +687,21 @@ public final class KeycloakModelUtils {
         }
 
     }
+
+    public static String getIdentityProviderDisplayName(KeycloakSession session, IdentityProviderModel provider) {
+        String displayName = provider.getDisplayName();
+        if (displayName != null && !displayName.isEmpty()) {
+            return displayName;
+        }
+
+        SocialIdentityProviderFactory providerFactory = (SocialIdentityProviderFactory) session.getKeycloakSessionFactory()
+                .getProviderFactory(SocialIdentityProvider.class, provider.getProviderId());
+        if (providerFactory != null) {
+            return providerFactory.getName();
+        } else {
+            return provider.getAlias();
+        }
+    }
+
+
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 36b1d2b..193bbc2 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -626,6 +626,7 @@ public class ModelToRepresentation {
         providerRep.setInternalId(identityProviderModel.getInternalId());
         providerRep.setProviderId(identityProviderModel.getProviderId());
         providerRep.setAlias(identityProviderModel.getAlias());
+        providerRep.setDisplayName(identityProviderModel.getDisplayName());
         providerRep.setEnabled(identityProviderModel.isEnabled());
         providerRep.setStoreToken(identityProviderModel.isStoreToken());
         providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 5e60d43..54acd0e 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1523,6 +1523,7 @@ public class RepresentationToModel {
 
         identityProviderModel.setInternalId(representation.getInternalId());
         identityProviderModel.setAlias(representation.getAlias());
+        identityProviderModel.setDisplayName(representation.getDisplayName());
         identityProviderModel.setProviderId(representation.getProviderId());
         identityProviderModel.setEnabled(representation.isEnabled());
         identityProviderModel.setTrustEmail(representation.isTrustEmail());
diff --git a/server-spi/src/main/java/org/keycloak/scripting/Script.java b/server-spi/src/main/java/org/keycloak/scripting/Script.java
index 2e81372..ef86902 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/Script.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/Script.java
@@ -1,8 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import org.keycloak.models.ScriptModel;
 
 /**
+ * A {@link ScriptModel} which holds some meta-data.
+ *
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 public class Script implements ScriptModel {
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
index 9d55195..9613eb6 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
@@ -1,17 +1,33 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import javax.script.Bindings;
 
 /**
  * Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}.
- *
+ * <p>Used by {@link ScriptingProvider}</p>
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 @FunctionalInterface
 public interface ScriptBindingsConfigurer {
 
     /**
-     * A default {@link ScriptBindingsConfigurer} leaves the Bindings empty.
+     * A default {@link ScriptBindingsConfigurer} that provides no Bindings.
      */
     ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
 
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
index e912ca9..2063bd2 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import org.keycloak.models.ScriptModel;
@@ -11,7 +27,7 @@ import javax.script.ScriptException;
  */
 public class ScriptExecutionException extends RuntimeException {
 
-    public ScriptExecutionException(ScriptModel script, ScriptException se) {
-        super("Error executing script '" + script.getName() + "'", se);
+    public ScriptExecutionException(ScriptModel script, Exception ex) {
+        super("Could not execute script '" + script.getName() + "' problem was: " + ex.getMessage(), ex);
     }
 }
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 163120b..67bad5a 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import org.keycloak.models.ScriptModel;
@@ -13,20 +29,23 @@ import javax.script.ScriptEngine;
 public interface ScriptingProvider extends Provider {
 
     /**
-     * Returns an {@link InvocableScript} based on the given {@link ScriptModel}.
-     * <p>The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
+     * Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
+     * <p>The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
      *
-     * @param script             the script to wrap
+     * @param scriptModel        the scriptModel to wrap
      * @param bindingsConfigurer populates the {@link javax.script.Bindings}
      * @return
      */
-    InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer);
+    InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
 
     /**
-     * Returns an {@link InvocableScript} based on the given {@link ScriptModel} with an {@link ScriptBindingsConfigurer#EMPTY} {@code ScriptBindingsConfigurer}.
-     * @see #prepareScript(ScriptModel, ScriptBindingsConfigurer)
-     * @param script
+     * Creates a new {@link ScriptModel} instance.
+     *
+     * @param realmId
+     * @param scriptName
+     * @param scriptCode
+     * @param scriptDescription
      * @return
      */
-    InvocableScript prepareScript(ScriptModel script);
+    ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription);
 }
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 889c7a4..5b1f529 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -65,3 +65,4 @@ org.keycloak.policy.PasswordPolicyManagerSpi
 org.keycloak.transaction.TransactionManagerLookupSpi
 org.keycloak.credential.hash.PasswordHashSpi
 org.keycloak.credential.CredentialSpi
+org.keycloak.keys.KeyStorageSpi
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
index 895f035..85a217f 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
@@ -1,23 +1,82 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.authentication.authenticators.browser;
 
 import org.jboss.logging.Logger;
 import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.ScriptModel;
 import org.keycloak.models.UserModel;
-import org.keycloak.scripting.InvocableScript;
-import org.keycloak.scripting.Script;
-import org.keycloak.scripting.ScriptBindingsConfigurer;
+import org.keycloak.scripting.InvocableScriptAdapter;
+import org.keycloak.scripting.ScriptExecutionException;
 import org.keycloak.scripting.ScriptingProvider;
 
-import javax.script.Bindings;
-import javax.script.ScriptException;
 import java.util.Map;
 
 /**
  * An {@link Authenticator} that can execute a configured script during authentication flow.
- * <p>scripts must provide </p>
+ * <p>
+ * Scripts must at least provide one of the following functions:
+ * <ol>
+ * <li>{@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}</li>
+ * <li>{@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}</li>
+ * </ol>
+ * </p>
+ * <p>
+ * Custom {@link Authenticator Authenticator's} should at least provide the {@code authenticate(..)} function.
+ * The following script {@link javax.script.Bindings} are available for convenient use within script code.
+ * <ol>
+ * <li>{@code script} the {@link ScriptModel} to access script metadata</li>
+ * <li>{@code realm} the {@link RealmModel}</li>
+ * <li>{@code user} the current {@link UserModel}</li>
+ * <li>{@code session} the active {@link KeycloakSession}</li>
+ * <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
+ * <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
+ * </ol>
+ * </p>
+ * <p>
+ * Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
+ * or {@code action(context)} function.
+ * <p>
+ * An example {@link ScriptBasedAuthenticator} definition could look as follows:
+ * <pre>
+ * {@code
+ *
+ *   AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+ *
+ *   function authenticate(context) {
+ *
+ *     LOG.info(script.name + " --> trace auth for: " + user.username);
+ *
+ *     if (   user.username === "tester"
+ *         && user.getAttribute("someAttribute")
+ *         && user.getAttribute("someAttribute").contains("someValue")) {
+ *
+ *         context.failure(AuthenticationFlowError.INVALID_USER);
+ *         return;
+ *     }
+ *
+ *     context.success();
+ *   }
+ * }
+ * </pre>
  *
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
@@ -29,58 +88,51 @@ public class ScriptBasedAuthenticator implements Authenticator {
     static final String SCRIPT_NAME = "scriptName";
     static final String SCRIPT_DESCRIPTION = "scriptDescription";
 
-    static final String ACTION = "action";
-    static final String AUTHENTICATE = "authenticate";
-    static final String TEXT_JAVASCRIPT = "text/javascript";
+    static final String ACTION_FUNCTION_NAME = "action";
+    static final String AUTHENTICATE_FUNCTION_NAME = "authenticate";
 
     @Override
     public void authenticate(AuthenticationFlowContext context) {
-        tryInvoke(AUTHENTICATE, context);
+        tryInvoke(AUTHENTICATE_FUNCTION_NAME, context);
     }
 
     @Override
     public void action(AuthenticationFlowContext context) {
-        tryInvoke(ACTION, context);
+        tryInvoke(ACTION_FUNCTION_NAME, context);
     }
 
     private void tryInvoke(String functionName, AuthenticationFlowContext context) {
 
-        InvocableScript script = getInvocableScript(context);
+        if (!hasAuthenticatorConfig(context)) {
+            // this is an empty not yet configured script authenticator
+            // we mark this execution as success to not lock out users due to incompletely configured authenticators.
+            context.success();
+            return;
+        }
+
+        InvocableScriptAdapter invocableScriptAdapter = getInvocableScriptAdapter(context);
 
-        if (!script.hasFunction(functionName)) {
+        if (!invocableScriptAdapter.isDefined(functionName)) {
             return;
         }
 
         try {
-            //should context be wrapped in a readonly wrapper?
-            script.invokeFunction(functionName, context);
-        } catch (ScriptException | NoSuchMethodException e) {
+            //should context be wrapped in a read-only wrapper?
+            invocableScriptAdapter.invokeFunction(functionName, context);
+        } catch (ScriptExecutionException e) {
             LOGGER.error(e);
+            context.failure(AuthenticationFlowError.INTERNAL_ERROR);
         }
     }
 
-    private InvocableScript getInvocableScript(final AuthenticationFlowContext context) {
-
-        final Script script = createAdhocScriptFromContext(context);
-
-        ScriptBindingsConfigurer bindingsConfigurer = new ScriptBindingsConfigurer() {
-
-            @Override
-            public void configureBindings(Bindings bindings) {
-
-                bindings.put("script", script);
-                bindings.put("LOG", LOGGER);
-            }
-        };
-
-        ScriptingProvider scripting = context.getSession().scripting();
-
-        //how to deal with long running scripts -> timeout?
-
-        return scripting.prepareScript(script, bindingsConfigurer);
+    private boolean hasAuthenticatorConfig(AuthenticationFlowContext context) {
+        return context != null
+                && context.getAuthenticatorConfig() != null
+                && context.getAuthenticatorConfig().getConfig() != null
+                && !context.getAuthenticatorConfig().getConfig().isEmpty();
     }
 
-    private Script createAdhocScriptFromContext(AuthenticationFlowContext context) {
+    private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) {
 
         Map<String, String> config = context.getAuthenticatorConfig().getConfig();
 
@@ -90,21 +142,35 @@ public class ScriptBasedAuthenticator implements Authenticator {
 
         RealmModel realm = context.getRealm();
 
-        return new Script(null /* scriptId */, realm.getId(), scriptName, TEXT_JAVASCRIPT, scriptCode, scriptDescription);
+        ScriptingProvider scripting = context.getSession().scripting();
+
+        //TODO lookup script by scriptId instead of creating it every time
+        ScriptModel script = scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
+
+        //how to deal with long running scripts -> timeout?
+        return scripting.prepareInvocableScript(script, bindings -> {
+            bindings.put("script", script);
+            bindings.put("realm", context.getRealm());
+            bindings.put("user", context.getUser());
+            bindings.put("session", context.getSession());
+            bindings.put("httpRequest", context.getHttpRequest());
+            bindings.put("LOG", LOGGER);
+        });
     }
 
     @Override
     public boolean requiresUser() {
-        return false;
+        return true;
     }
 
     @Override
     public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
-        return false;
+        return true;
     }
 
     @Override
     public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+        //TODO make RequiredActions configurable in the script
         //NOOP
     }
 
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
index b25af8f..0528154 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
@@ -1,5 +1,23 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.authentication.authenticators.browser;
 
+import org.apache.commons.io.IOUtils;
+import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.authentication.Authenticator;
 import org.keycloak.authentication.AuthenticatorFactory;
@@ -8,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.provider.ProviderConfigProperty;
 
+import java.io.IOException;
 import java.util.List;
 
 import static java.util.Arrays.asList;
@@ -24,7 +43,9 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
  */
 public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
 
-    static final String PROVIDER_ID = "auth-script-based";
+    private static final Logger LOGGER = Logger.getLogger(ScriptBasedAuthenticatorFactory.class);
+
+    public static final String PROVIDER_ID = "auth-script-based";
 
     static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
             AuthenticationExecutionModel.Requirement.REQUIRED,
@@ -77,7 +98,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
 
     @Override
     public boolean isUserSetupAllowed() {
-        return false;
+        return true;
     }
 
     @Override
@@ -87,12 +108,12 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
 
     @Override
     public String getDisplayType() {
-        return "Script-based Authentication";
+        return "Script";
     }
 
     @Override
     public String getHelpText() {
-        return "Script based authentication.";
+        return "Script based authentication. Allows to define custom authentication logic via JavaScript.";
     }
 
     @Override
@@ -114,9 +135,16 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
         script.setType(SCRIPT_TYPE);
         script.setName(SCRIPT_CODE);
         script.setLabel("Script Source");
-        script.setDefaultValue("//enter your script here");
-        script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate' that accepts a context (AuthenticationFlowContext) parameter." +
-                "This authenticator exposes the following additional variables: 'script', 'LOG'");
+
+        String scriptTemplate = "//enter your script code here";
+        try {
+            scriptTemplate = IOUtils.toString(getClass().getResource("/scripts/authenticator-template.js"));
+        } catch (IOException ioe) {
+            LOGGER.warn(ioe);
+        }
+        script.setDefaultValue(scriptTemplate);
+        script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate(context)' that accepts a context (AuthenticationFlowContext) parameter.\n" +
+                "This authenticator exposes the following additional variables: 'script', 'realm', 'user', 'session', 'httpRequest', 'LOG'");
 
         return asList(name, description, script);
     }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index ad9b0ef..aec70dc 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -23,6 +23,7 @@ import org.keycloak.authentication.ClientAuthenticationFlowContext;
 import org.keycloak.common.util.Time;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.keys.loader.KeyStorageManager;
 import org.keycloak.models.AuthenticationExecutionModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ModelException;
@@ -120,7 +121,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
             }
 
             // Get client key and validate signature
-            PublicKey clientPublicKey = getSignatureValidationKey(client, context);
+            PublicKey clientPublicKey = getSignatureValidationKey(client, context, jws);
             if (clientPublicKey == null) {
                 // Error response already set to context
                 return;
@@ -161,13 +162,14 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
         }
     }
 
-    protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
-        try {
-            return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX);
-        } catch (ModelException me) {
-            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage());
+    protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context, JWSInput jws) {
+        PublicKey publicKey = KeyStorageManager.getClientPublicKey(context.getSession(), client, jws);
+        if (publicKey == null) {
+            Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Unable to load public key");
             context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
             return null;
+        } else {
+            return publicKey;
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 322c332..c301371 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -73,8 +73,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type";
 
 
-    public AbstractOAuth2IdentityProvider(C config) {
-        super(config);
+    public AbstractOAuth2IdentityProvider(KeycloakSession session, C config) {
+        super(session, config);
 
         if (config.getDefaultScope() == null || config.getDefaultScope().isEmpty()) {
             config.setDefaultScope(getDefaultScopes());
diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index af3078c..1f2871b 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -23,6 +23,7 @@ import org.keycloak.constants.AdapterConstants;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessTokenResponse;
@@ -46,8 +47,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
 
     public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
 
-    public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
-        super(config);
+    public KeycloakOIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
+        super(session, config);
     }
 
     @Override
@@ -56,8 +57,8 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
     }
 
     @Override
-    protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
-        JsonWebToken access = validateToken(idpKey, response.getToken());
+    protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
+        JsonWebToken access = validateToken(response.getToken());
         context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
     }
 
@@ -76,13 +77,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
                 logger.warn("Failed to verify logout request");
                 return Response.status(400).build();
             }
-            PublicKey key = getExternalIdpKey();
-            if (key != null) {
-                if (!verify(token, key)) {
-                    logger.warn("Failed to verify logout request");
-                    return Response.status(400).build();
-                }
+
+            if (!verify(token)) {
+                logger.warn("Failed to verify logout request");
+                return Response.status(400).build();
             }
+
             LogoutAction action = null;
             try {
                 action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
index 56f24a2..87e7b73 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
@@ -36,8 +36,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
     }
 
     @Override
-    public KeycloakOIDCIdentityProvider create(IdentityProviderModel model) {
-        return new KeycloakOIDCIdentityProvider(new OIDCIdentityProviderConfig(model));
+    public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new KeycloakOIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index d513e72..c83d03f 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -32,6 +32,7 @@ import org.keycloak.events.EventType;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
 import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.keys.loader.KeyStorageManager;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -70,8 +71,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
     public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
 
-    public OIDCIdentityProvider(OIDCIdentityProviderConfig config) {
-        super(config);
+    public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
+        super(session, config);
 
         String defaultScope = config.getDefaultScope();
 
@@ -85,21 +86,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return new OIDCEndpoint(callback, realm, event);
     }
 
-    protected PublicKey getExternalIdpKey() {
-        String signingCert = getConfig().getCertificateSignatureVerifier();
-        try {
-            if (signingCert != null && !signingCert.trim().equals("")) {
-                return PemUtils.decodeCertificate(signingCert).getPublicKey();
-            } else if (getConfig().getPublicKeySignatureVerifier() != null && !getConfig().getPublicKeySignatureVerifier().trim().equals("")) {
-                return PemUtils.decodePublicKey(getConfig().getPublicKeySignatureVerifier());
-            }
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        return null;
-
-    }
-
     protected class OIDCEndpoint extends Endpoint {
         public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
             super(callback, realm, event);
@@ -232,7 +218,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return authorizationUrl;
     }
 
-    protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
+    protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
 
     }
 
@@ -244,14 +230,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         } catch (IOException e) {
             throw new IdentityBrokerException("Could not decode access token response.", e);
         }
-        PublicKey key = getExternalIdpKey();
-        String accessToken = verifyAccessToken(key, tokenResponse);
+        String accessToken = verifyAccessToken(tokenResponse);
 
         String encodedIdToken = tokenResponse.getIdToken();
 
-
-
-        JsonWebToken idToken = validateToken(key, encodedIdToken);
+        JsonWebToken idToken = validateToken(encodedIdToken);
 
         try {
             String id = idToken.getSubject();
@@ -273,7 +256,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);
+            processAccessTokenResponse(identity, tokenResponse);
 
             identity.setId(id);
             identity.setName(name);
@@ -304,7 +287,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         }
     }
 
-    private String verifyAccessToken(PublicKey key, AccessTokenResponse tokenResponse) {
+    private String verifyAccessToken(AccessTokenResponse tokenResponse) {
         String accessToken = tokenResponse.getToken();
 
         if (accessToken == null) {
@@ -313,14 +296,15 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return accessToken;
     }
 
-    protected boolean verify(JWSInput jws, PublicKey key) {
-        if (key == null) return true;
+    protected boolean verify(JWSInput jws) {
         if (!getConfig().isValidateSignature()) return true;
-        return RSAProvider.verify(jws, key);
 
+        PublicKey publicKey = KeyStorageManager.getIdentityProviderPublicKey(session, session.getContext().getRealm(), getConfig(), jws);
+
+        return publicKey != null && RSAProvider.verify(jws, publicKey);
     }
 
-    protected JsonWebToken validateToken(PublicKey key, String encodedToken) {
+    protected JsonWebToken validateToken(String encodedToken) {
         if (encodedToken == null) {
             throw new IdentityBrokerException("No token from server.");
         }
@@ -328,7 +312,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         JsonWebToken token;
         try {
             JWSInput jws = new JWSInput(encodedToken);
-            if (!verify(jws, key)) {
+            if (!verify(jws)) {
                 throw new IdentityBrokerException("token signature validation failed");
             }
             token = jws.readJsonContent(JsonWebToken.class);
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
index d747190..e7fdfd2 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -17,12 +17,18 @@
 package org.keycloak.broker.oidc;
 
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
  */
 public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
 
+    private static final String JWKS_URL = "jwksUrl";
+
+    private static final String USE_JWKS_URL = "useJwksUrl";
+
+
     public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
         super(identityProviderModel);
     }
@@ -46,13 +52,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
     public void setLogoutUrl(String url) {
         getConfig().put("logoutUrl", url);
     }
-    public String getCertificateSignatureVerifier() {
-        return getConfig().get("certificateSignatureVerifier");
-    }
 
-    public void setCertificateSignatureVerifier(String signingCertificate) {
-        getConfig().put("certificateSignatureVerifier", signingCertificate);
-    }
     public String getPublicKeySignatureVerifier() {
         return getConfig().get("publicKeySignatureVerifier");
     }
@@ -69,6 +69,22 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
         getConfig().put("validateSignature", String.valueOf(validateSignature));
     }
 
+    public boolean isUseJwksUrl() {
+        return Boolean.valueOf(getConfig().get(USE_JWKS_URL));
+    }
+
+    public void setUseJwksUrl(boolean useJwksUrl) {
+        getConfig().put(USE_JWKS_URL, String.valueOf(useJwksUrl));
+    }
+
+    public String getJwksUrl() {
+        return getConfig().get(JWKS_URL);
+    }
+
+    public void setJwksUrl(String jwksUrl) {
+        getConfig().put(JWKS_URL, jwksUrl);
+    }
+
     public boolean isBackchannelSupported() {
         return Boolean.valueOf(getConfig().get("backchannelSupported"));
     }
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
index b42b3e1..fcb41a4 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -19,12 +19,14 @@ package org.keycloak.broker.oidc;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKParser;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
-import org.keycloak.protocol.oidc.utils.JWKSUtils;
+import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
 import org.keycloak.services.ServicesLogger;
+import org.keycloak.util.JWKSUtils;
 import org.keycloak.util.JsonSerialization;
 
 import java.io.IOException;
@@ -47,8 +49,8 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
     }
 
     @Override
-    public OIDCIdentityProvider create(IdentityProviderModel model) {
-        return new OIDCIdentityProvider(new OIDCIdentityProviderConfig(model));
+    public OIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new OIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model));
     }
 
     @Override
@@ -75,24 +77,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
         config.setTokenUrl(rep.getTokenEndpoint());
         config.setUserInfoUrl(rep.getUserinfoEndpoint());
         if (rep.getJwksUri() != null) {
-            sendJwksRequest(session, rep, config);
+            config.setValidateSignature(true);
+            config.setUseJwksUrl(true);
+            config.setJwksUrl(rep.getJwksUri());
         }
         return config.getConfig();
     }
 
-    protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
-        try {
-            JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri());
-            PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
-            if (key == null) {
-                logger.supportedJwkNotFound(JWK.Use.SIG.asString());
-            } else {
-                config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
-                config.setValidateSignature(true);
-            }
-        } catch (IOException e) {
-            throw new RuntimeException("Failed to query JWKSet from: " + rep.getJwksUri(), e);
-        }
-    }
-
 }
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index 3d9a536..f7db32b 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -56,8 +56,8 @@ import java.security.PublicKey;
  */
 public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> {
     protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class);
-    public SAMLIdentityProvider(SAMLIdentityProviderConfig config) {
-        super(config);
+    public SAMLIdentityProvider(KeycloakSession session, SAMLIdentityProviderConfig config) {
+        super(session, config);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
index 2617995..714c47e 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
@@ -50,8 +50,8 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
     }
 
     @Override
-    public SAMLIdentityProvider create(IdentityProviderModel model) {
-        return new SAMLIdentityProvider(new SAMLIdentityProviderConfig(model));
+    public SAMLIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new SAMLIdentityProvider(session, new SAMLIdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
index c2544e7..96f4257 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
@@ -22,8 +22,9 @@ import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
-import org.keycloak.services.Urls;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.Urls;
 
 import javax.ws.rs.core.UriBuilder;
 import java.net.URI;
@@ -69,7 +70,8 @@ public class AccountFederatedIdentityBean {
                         .queryParam("stateChecker", stateChecker)
                         .build().toString();
 
-                FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, provider.getAlias(), provider.getAlias(), actionUrl,
+                String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
+                FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, displayName, provider.getAlias(), provider.getAlias(), actionUrl,
                 		  															provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
                 orderedSet.add(entry);
             }
@@ -105,10 +107,12 @@ public class AccountFederatedIdentityBean {
 		private final String providerName;
         private final String actionUrl;
         private final String guiOrder;
+        private final String displayName;
 
-		public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String providerId, String providerName, String actionUrl, String guiOrder
-				) {
+        public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String displayName, String providerId,
+                                      String providerName, String actionUrl, String guiOrder) {
             this.federatedIdentityModel = federatedIdentityModel;
+            this.displayName = displayName;
             this.providerId = providerId;
             this.providerName = providerName;
             this.actionUrl = actionUrl;
@@ -142,6 +146,11 @@ public class AccountFederatedIdentityBean {
         public String getGuiOrder() {
             return guiOrder;
         }
+
+        public String getDisplayName() {
+            return displayName;
+        }
+
     }
     
 	public static class IdentityProviderComparator implements Comparator<FederatedIdentityEntry> {
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index 0f48ae4..9f60404 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
 
             List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
             identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
-            attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
+            attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
 
             attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
 
@@ -398,7 +398,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
 
             List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
             identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
-            attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
+            attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
 
             attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
             attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
@@ -425,7 +425,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
         }
     }
 
-
     @Override
     public Response createLogin() {
         return createResponse(LoginFormsPages.LOGIN);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
index 8301660..87b935a 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
@@ -17,7 +17,9 @@
 package org.keycloak.forms.login.freemarker.model;
 
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.services.Urls;
 
 import javax.ws.rs.core.UriInfo;
@@ -35,12 +37,13 @@ import java.util.TreeSet;
 public class IdentityProviderBean {
 
     private boolean displaySocial;
-
     private List<IdentityProvider> providers;
     private RealmModel realm;
+    private final KeycloakSession session;
 
-    public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
+    public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
         this.realm = realm;
+        this.session = session;
 
         if (!identityProviders.isEmpty()) {
             Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
@@ -59,7 +62,10 @@ public class IdentityProviderBean {
 
     private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
         String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
-        orderedSet.add(new IdentityProvider(identityProvider.getAlias(), identityProvider.getProviderId(), loginUrl,
+        String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
+
+        orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
+                displayName, identityProvider.getProviderId(), loginUrl,
                 identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null));
     }
 
@@ -77,9 +83,11 @@ public class IdentityProviderBean {
         private final String providerId; // This refer to providerType (facebook, google, etc.)
         private final String loginUrl;
         private final String guiOrder;
+        private final String displayName;
 
-        public IdentityProvider(String alias, String providerId, String loginUrl, String guiOrder) {
+        public IdentityProvider(String alias, String displayName, String providerId, String loginUrl, String guiOrder) {
             this.alias = alias;
+            this.displayName = displayName;
             this.providerId = providerId;
             this.loginUrl = loginUrl;
             this.guiOrder = guiOrder;
@@ -100,6 +108,10 @@ public class IdentityProviderBean {
         public String getGuiOrder() {
             return guiOrder;
         }
+
+        public String getDisplayName() {
+            return displayName;
+        }
     }
 
     public static class IdentityProviderComparator implements Comparator<IdentityProvider> {
@@ -112,7 +124,7 @@ public class IdentityProviderBean {
 
         @Override
         public int compare(IdentityProvider o1, IdentityProvider o2) {
-            
+
             int o1order = parseOrder(o1);
             int o2order = parseOrder(o2);
 
@@ -120,7 +132,7 @@ public class IdentityProviderBean {
                 return 1;
             else if (o1order < o2order)
                 return -1;
-            
+
             return 1;
         }
 
@@ -134,6 +146,5 @@ public class IdentityProviderBean {
             }
             return 10000;
         }
-
     }
 }
diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java
new file mode 100644
index 0000000..39524ce
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.loader;
+
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Map;
+
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKBuilder;
+import org.keycloak.keys.KeyLoader;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
+import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
+import org.keycloak.representations.idm.CertificateRepresentation;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.util.CertificateInfoHelper;
+import org.keycloak.util.JWKSUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientPublicKeyLoader implements KeyLoader {
+
+    protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+    private final KeycloakSession session;
+    private final ClientModel client;
+
+    public ClientPublicKeyLoader(KeycloakSession session, ClientModel client) {
+        this.session = session;
+        this.client = client;
+    }
+
+
+    @Override
+    public Map<String, PublicKey> loadKeys() throws Exception {
+        OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
+        if (config.isUseJwksUrl()) {
+            String jwksUrl = config.getJwksUrl();
+            JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
+            return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
+        } else {
+            try {
+                CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
+                PublicKey publicKey = getSignatureValidationKey(certInfo);
+
+                // Check if we have kid in DB, generate otherwise
+                String kid = certInfo.getKid() != null ? certInfo.getKid() : JWKBuilder.createKeyId(publicKey);
+                return Collections.singletonMap(kid, publicKey);
+            } catch (ModelException me) {
+                logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
+                return Collections.emptyMap();
+            }
+
+        }
+    }
+
+    private static PublicKey getSignatureValidationKey(CertificateRepresentation certInfo) throws ModelException {
+        String encodedCertificate = certInfo.getCertificate();
+        String encodedPublicKey = certInfo.getPublicKey();
+
+        if (encodedCertificate == null && encodedPublicKey == null) {
+            throw new ModelException("Client doesn't have certificate or publicKey configured");
+        }
+
+        if (encodedCertificate != null && encodedPublicKey != null) {
+            throw new ModelException("Client has both publicKey and certificate configured");
+        }
+
+        if (encodedCertificate != null) {
+            X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
+            return clientCert.getPublicKey();
+        } else {
+            return KeycloakModelUtils.getPublicKey(encodedPublicKey);
+        }
+    }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java b/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java
new file mode 100644
index 0000000..219afc4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/loader/KeyStorageManager.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.loader;
+
+import java.security.PublicKey;
+
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.keys.KeyStorageProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeyStorageManager {
+
+    public static PublicKey getClientPublicKey(KeycloakSession session, ClientModel client, JWSInput input) {
+        String kid = input.getHeader().getKeyId();
+
+        KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
+
+        String modelKey = getModelKey(client);
+        ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client);
+        return keyStorage.getPublicKey(modelKey, kid, loader);
+    }
+
+    private static String getModelKey(ClientModel client) {
+        return client.getRealm().getId() + "::client::" + client.getId();
+    }
+
+
+    public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
+        String kid = input.getHeader().getKeyId();
+
+        KeyStorageProvider keyStorage = session.getProvider(KeyStorageProvider.class);
+
+        String modelKey = getModelKey(realm, idpConfig);
+        OIDCIdentityProviderLoader loader = new OIDCIdentityProviderLoader(session, idpConfig);
+        return keyStorage.getPublicKey(modelKey, kid, loader);
+    }
+
+    private static String getModelKey(RealmModel realm, OIDCIdentityProviderConfig idpConfig) {
+        return realm.getId() + "::idp::" + idpConfig.getInternalId();
+    }
+}
diff --git a/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java
new file mode 100644
index 0000000..79032ff
--- /dev/null
+++ b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderLoader.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.keys.loader;
+
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.Map;
+
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKBuilder;
+import org.keycloak.keys.KeyLoader;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.util.JWKSUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCIdentityProviderLoader implements KeyLoader {
+
+    protected static ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+    private final KeycloakSession session;
+    private final OIDCIdentityProviderConfig config;
+
+    public OIDCIdentityProviderLoader(KeycloakSession session, OIDCIdentityProviderConfig config) {
+        this.session = session;
+        this.config = config;
+    }
+
+    @Override
+    public Map<String, PublicKey> loadKeys() throws Exception {
+        if (config.isUseJwksUrl()) {
+            String jwksUrl = config.getJwksUrl();
+            JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
+            return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
+        } else {
+            try {
+                PublicKey publicKey = getSavedPublicKey();
+                if (publicKey == null) {
+                    return Collections.emptyMap();
+                }
+
+                String kid = JWKBuilder.createKeyId(publicKey);
+                return Collections.singletonMap(kid, publicKey);
+            } catch (Exception e) {
+                logger.warnf(e, "Unable to retrieve publicKey for verify signature of identityProvider '%s' . Error details: %s", config.getAlias(), e.getMessage());
+                return Collections.emptyMap();
+            }
+        }
+    }
+
+    protected PublicKey getSavedPublicKey() throws Exception {
+        if (config.getPublicKeySignatureVerifier() != null && !config.getPublicKeySignatureVerifier().trim().equals("")) {
+            return PemUtils.decodePublicKey(config.getPublicKeySignatureVerifier());
+        } else {
+            logger.warnf("No public key saved on identityProvider %s", config.getAlias());
+            return null;
+        }
+    }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
index faab68d..4469c0f 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java
@@ -52,12 +52,12 @@ public class AuthorizationEndpointRequestParserProcessor {
             }
 
             if (requestParam != null) {
-                new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request);
+                new AuthzEndpointRequestObjectParser(session, requestParam, client).parseRequest(request);
             } else if (requestUriParam != null) {
                 InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam);
                 String retrievedRequest = StreamUtil.readString(is);
 
-                new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request);
+                new AuthzEndpointRequestObjectParser(session, retrievedRequest, client).parseRequest(request);
             }
 
             return request;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
index 616e1db..62d711f 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestObjectParser.java
@@ -22,7 +22,10 @@ import org.keycloak.jose.jws.Algorithm;
 import org.keycloak.jose.jws.JWSHeader;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.keys.KeyStorageProvider;
+import org.keycloak.keys.loader.KeyStorageManager;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.services.util.CertificateInfoHelper;
 import org.keycloak.util.JsonSerialization;
@@ -41,7 +44,7 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
 
     private final Map<String, Object> requestParams;
 
-    public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception {
+    public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
         JWSInput input = new JWSInput(requestObject);
         JWSHeader header = input.getHeader();
 
@@ -54,7 +57,11 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
         if (header.getAlgorithm() == Algorithm.none) {
             this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
         } else if (header.getAlgorithm() == Algorithm.RS256) {
-            PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX);
+            PublicKey clientPublicKey = KeyStorageManager.getClientPublicKey(session, client, input);
+            if (clientPublicKey == null) {
+                throw new RuntimeException("Client public key not found");
+            }
+
             boolean verified = RSAProvider.verify(input, clientPublicKey);
             if (!verified) {
                 throw new RuntimeException("Failed to verify signature on 'request' object");
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
index 756c12a..588a06c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java
@@ -32,6 +32,10 @@ public class OIDCAdvancedConfigWrapper {
 
     private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
 
+    private static final String JWKS_URL = "jwks.url";
+
+    private static final String USE_JWKS_URL = "use.jwks.url";
+
     private final ClientModel clientModel;
     private final ClientRepresentation clientRep;
 
@@ -74,6 +78,23 @@ public class OIDCAdvancedConfigWrapper {
         setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
     }
 
+    public boolean isUseJwksUrl() {
+        String useJwksUrl = getAttribute(USE_JWKS_URL);
+        return Boolean.parseBoolean(useJwksUrl);
+    }
+
+    public void setUseJwksUrl(boolean useJwksUrl) {
+        String val = String.valueOf(useJwksUrl);
+        setAttribute(USE_JWKS_URL, val);
+    }
+
+    public String getJwksUrl() {
+        return getAttribute(JWKS_URL);
+    }
+
+    public void setJwksUrl(String jwksUrl) {
+        setAttribute(JWKS_URL, jwksUrl);
+    }
 
     private String getAttribute(String attrKey) {
         if (clientModel != null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 39c6d10..e8b4e11 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -81,7 +81,6 @@ import java.util.Set;
  */
 public class TokenManager {
     protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
-    private static final String JWT = "JWT";
 
     // Harcoded for now
     Algorithm jwsAlgorithm = Algorithm.RS256;
@@ -621,7 +620,7 @@ public class TokenManager {
 
     public String encodeToken(RealmModel realm, Object token) {
         String encodedToken = new JWSBuilder()
-                .type(JWT)
+                .type(OAuth2Constants.JWT)
                 .kid(realm.getKeyId())
                 .jsonContent(token)
                 .sign(jwsAlgorithm, realm.getPrivateKey());
@@ -747,7 +746,7 @@ public class TokenManager {
 
             AccessTokenResponse res = new AccessTokenResponse();
             if (accessToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(accessToken).sign(jwsAlgorithm, realm.getPrivateKey());
+                String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(accessToken).sign(jwsAlgorithm, realm.getPrivateKey());
                 res.setToken(encodedToken);
                 res.setTokenType("bearer");
                 res.setSessionState(accessToken.getSessionState());
@@ -765,11 +764,11 @@ public class TokenManager {
             }
 
             if (idToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(idToken).sign(jwsAlgorithm, realm.getPrivateKey());
+                String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(idToken).sign(jwsAlgorithm, realm.getPrivateKey());
                 res.setIdToken(encodedToken);
             }
             if (refreshToken != null) {
-                String encodedToken = new JWSBuilder().type(JWT).kid(realm.getKeyId()).jsonContent(refreshToken).sign(jwsAlgorithm, realm.getPrivateKey());
+                String encodedToken = new JWSBuilder().type(OAuth2Constants.JWT).kid(realm.getKeyId()).jsonContent(refreshToken).sign(jwsAlgorithm, realm.getPrivateKey());
                 res.setRefreshToken(encodedToken);
                 if (refreshToken.getExpiration() != 0) {
                     res.setRefreshExpiresIn(refreshToken.getExpiration() - Time.currentTime());
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
index 140ec04..5772f53 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import org.keycloak.models.ScriptModel;
@@ -6,7 +22,6 @@ import javax.script.Bindings;
 import javax.script.ScriptContext;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
-import javax.script.ScriptException;
 
 /**
  * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@@ -18,40 +33,70 @@ public class DefaultScriptingProvider implements ScriptingProvider {
     private final ScriptEngineManager scriptEngineManager;
 
     public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
-        this.scriptEngineManager = scriptEngineManager;
-    }
 
-    @Override
-    public InvocableScript prepareScript(ScriptModel script) {
-        return prepareScript(script, ScriptBindingsConfigurer.EMPTY);
+        if (scriptEngineManager == null) {
+            throw new IllegalStateException("scriptEngineManager must not be null!");
+        }
+
+        this.scriptEngineManager = scriptEngineManager;
     }
 
+    /**
+     * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
+     *
+     * @param scriptModel  must not be {@literal null}
+     * @param bindingsConfigurer must not be {@literal null}
+     * @return
+     */
     @Override
-    public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
+    public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
 
-        if (script == null) {
-            throw new NullPointerException("script must not be null");
+        if (scriptModel == null) {
+            throw new IllegalArgumentException("script must not be null");
         }
 
-        if (script.getCode() == null || script.getCode().trim().isEmpty()) {
+        if (scriptModel.getCode() == null || scriptModel.getCode().trim().isEmpty()) {
             throw new IllegalArgumentException("script must not be null or empty");
         }
 
         if (bindingsConfigurer == null) {
-            throw new NullPointerException("bindingsConfigurer must not be null");
+            throw new IllegalArgumentException("bindingsConfigurer must not be null");
         }
 
-        ScriptEngine engine = lookupScriptEngineFor(script);
+        ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
+
+        return new InvocableScriptAdapter(scriptModel, engine);
+    }
+
+    //TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
+    //TODO allow script lookup by (scriptId)
+    //TODO allow script lookup by (name, realmName)
+
+    @Override
+    public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
+
+        ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
+        return script;
+    }
+
+    /**
+     * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
+     *
+     * @param script
+     * @param bindingsConfigurer
+     * @return
+     */
+    private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
 
-        if (engine == null) {
+        ScriptEngine scriptEngine = lookupScriptEngineFor(script);
+
+        if (scriptEngine == null) {
             throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
         }
 
-        configureBindings(bindingsConfigurer, engine);
-
-        loadScriptIntoEngine(script, engine);
+        configureBindings(bindingsConfigurer, scriptEngine);
 
-        return new InvocableScript(script, engine);
+        return scriptEngine;
     }
 
     private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
@@ -61,15 +106,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
         engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
     }
 
-    private void loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
-
-        try {
-            engine.eval(script.getCode());
-        } catch (ScriptException se) {
-            throw new ScriptExecutionException(script, se);
-        }
-    }
-
+    /**
+     * Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
+     */
     private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
         return scriptEngineManager.getEngineByMimeType(script.getMimeType());
     }
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
index 2e8a431..b00a058 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.keycloak.scripting;
 
 import org.keycloak.Config;
@@ -13,11 +29,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
 
     static final String ID = "script-based-auth";
 
-    private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
-
     @Override
     public ScriptingProvider create(KeycloakSession session) {
-        return new DefaultScriptingProvider(scriptEngineManager);
+        return new DefaultScriptingProvider(ScriptEngineManagerHolder.SCRIPT_ENGINE_MANAGER);
     }
 
     @Override
@@ -39,4 +53,12 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
     public String getId() {
         return ID;
     }
+
+    /**
+     * Holder class for lazy initialization of {@link ScriptEngineManager}.
+     */
+    private static class ScriptEngineManagerHolder {
+
+        private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager();
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index 2024a6f..59f36b2 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -24,6 +24,7 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
 import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
 import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKParser;
 import org.keycloak.jose.jws.Algorithm;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -31,7 +32,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
 import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
-import org.keycloak.protocol.oidc.utils.JWKSUtils;
+import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
 import org.keycloak.protocol.oidc.utils.OIDCResponseType;
 import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
 import org.keycloak.protocol.oidc.utils.SubjectType;
@@ -41,6 +42,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 import org.keycloak.representations.oidc.OIDCClientRepresentation;
 import org.keycloak.services.clientregistration.ClientRegistrationException;
 import org.keycloak.services.util.CertificateInfoHelper;
+import org.keycloak.util.JWKSUtils;
 
 import java.io.IOException;
 import java.net.URI;
@@ -94,19 +96,11 @@ public class DescriptionConverter {
         }
         client.setClientAuthenticatorType(clientAuthFactory.getId());
 
-        PublicKey publicKey = retrievePublicKey(session, clientOIDC);
-        if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) {
+        boolean publicKeySet = setPublicKey(clientOIDC, client);
+        if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && !publicKeySet) {
             throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
         }
 
-        if (publicKey != null) {
-            String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
-
-            CertificateRepresentation rep = new CertificateRepresentation();
-            rep.setPublicKey(publicKeyPem);
-            CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
-        }
-
         OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
         if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
             Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg());
@@ -122,27 +116,39 @@ public class DescriptionConverter {
     }
 
 
-    private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) {
+    private static boolean setPublicKey(OIDCClientRepresentation clientOIDC, ClientRepresentation clientRep) {
         if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
-            return null;
+            return false;
         }
 
         if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
             throw new ClientRegistrationException("Illegal to use both jwks_uri and jwks");
         }
 
-        JSONWebKeySet keySet;
+        OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
+
         if (clientOIDC.getJwks() != null) {
-            keySet = clientOIDC.getJwks();
-        } else {
-            try {
-                keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri());
-            } catch (IOException ioe) {
-                throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe);
+            JSONWebKeySet keySet = clientOIDC.getJwks();
+            JWK publicKeyJWk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
+            if (publicKeyJWk == null) {
+                return false;
+            } else {
+                PublicKey publicKey = JWKParser.create(publicKeyJWk).toPublicKey();
+                String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
+                CertificateRepresentation rep = new CertificateRepresentation();
+                rep.setPublicKey(publicKeyPem);
+                rep.setKid(publicKeyJWk.getKeyId());
+                CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, rep, JWTClientAuthenticator.ATTR_PREFIX);
+
+                configWrapper.setUseJwksUrl(false);
+
+                return true;
             }
+        } else {
+            configWrapper.setUseJwksUrl(true);
+            configWrapper.setJwksUrl(clientOIDC.getJwksUri());
+            return true;
         }
-
-        return JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
     }
 
 
@@ -176,6 +182,9 @@ public class DescriptionConverter {
         if (config.getRequestObjectSignatureAlg() != null) {
             response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
         }
+        if (config.isUseJwksUrl()) {
+            response.setJwksUri(config.getJwksUrl());
+        }
 
         List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
         SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
index 77afc30..aa1f089 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java
@@ -28,15 +28,17 @@ import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
 import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKParser;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.utils.JWKSUtils;
+import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
 import org.keycloak.representations.KeyStoreConfig;
 import org.keycloak.representations.idm.CertificateRepresentation;
 import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.util.CertificateInfoHelper;
+import org.keycloak.util.JWKSUtils;
 import org.keycloak.util.JsonSerialization;
 
 import javax.ws.rs.Consumes;
@@ -148,16 +150,15 @@ public class ClientAttributeCertificateResource {
             throw new NotFoundException("Could not find client");
         }
 
-        CertificateRepresentation info = getCertFromRequest(uriInfo, input);
-
         try {
+            CertificateRepresentation info = getCertFromRequest(input);
             CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
+
+            adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+            return info;
         } catch (IllegalStateException ise) {
             throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
         }
-
-        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
-        return info;
     }
 
     /**
@@ -179,20 +180,19 @@ public class ClientAttributeCertificateResource {
             throw new NotFoundException("Could not find client");
         }
 
-        CertificateRepresentation info = getCertFromRequest(uriInfo, input);
-        info.setPrivateKey(null);
-
         try {
+            CertificateRepresentation info = getCertFromRequest(input);
+            info.setPrivateKey(null);
             CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
+
+            adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
+            return info;
         } catch (IllegalStateException ise) {
             throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
         }
-
-        adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
-        return info;
     }
 
-    private CertificateRepresentation getCertFromRequest(UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+    private CertificateRepresentation getCertFromRequest(MultipartFormDataInput input) throws IOException {
         auth.requireManage();
         CertificateRepresentation info = new CertificateRepresentation();
         Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
@@ -217,10 +217,16 @@ public class ClientAttributeCertificateResource {
         } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
             InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
             JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
-            PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
-            String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
-            info.setPublicKey(publicKeyPem);
-            return info;
+            JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
+            if (publicKeyJwk == null) {
+                throw new IllegalStateException("Certificate not found for use sig");
+            } else {
+                PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
+                String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
+                info.setPublicKey(publicKeyPem);
+                info.setKid(publicKeyJwk.getKeyId());
+                return info;
+            }
         }
 
 
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 af456c6..4c40991 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -228,7 +228,7 @@ public class IdentityProviderResource {
 
         try {
             IdentityProviderFactory factory = getIdentityProviderFactory();
-            return factory.create(identityProviderModel).export(uriInfo, realm, format);
+            return factory.create(session, identityProviderModel).export(uriInfo, realm, format);
         } catch (Exception e) {
             return ErrorResponse.error("Could not export public broker configuration for identity provider [" + identityProviderModel.getProviderId() + "].", Response.Status.NOT_FOUND);
         }
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 fabbae0..9f7e4bf 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -805,7 +805,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
                 throw new IdentityBrokerException("Could not find factory for identity provider [" + alias + "].");
             }
 
-            return providerFactory.create(identityProviderModel);
+            return providerFactory.create(session, identityProviderModel);
         }
 
         throw new IdentityBrokerException("Identity Provider [" + alias + "] not found.");
diff --git a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
index 313ce1d..005ce88 100644
--- a/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/CertificateInfoHelper.java
@@ -37,6 +37,8 @@ public class CertificateInfoHelper {
     public static final String X509CERTIFICATE = "certificate";
     public static final String PUBLIC_KEY = "public.key";
 
+    public static final String KID = "kid";
+
 
     // CLIENT MODEL METHODS
 
@@ -44,11 +46,13 @@ public class CertificateInfoHelper {
         String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
         String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
         String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
+        String kidAttribute = attributePrefix + "." + KID;
 
         CertificateRepresentation rep = new CertificateRepresentation();
         rep.setCertificate(client.getAttribute(certificateAttribute));
         rep.setPublicKey(client.getAttribute(publicKeyAttribute));
         rep.setPrivateKey(client.getAttribute(privateKeyAttribute));
+        rep.setKid(client.getAttribute(kidAttribute));
 
         return rep;
     }
@@ -58,6 +62,7 @@ public class CertificateInfoHelper {
         String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
         String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
         String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
+        String kidAttribute = attributePrefix + "." + KID;
 
         if (rep.getPublicKey() == null && rep.getCertificate() == null) {
             throw new IllegalStateException("Both certificate and publicKey are null!");
@@ -70,6 +75,7 @@ public class CertificateInfoHelper {
         setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
         setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
         setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
+        setOrRemoveAttr(client, kidAttribute, rep.getKid());
     }
 
     private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) {
@@ -81,36 +87,13 @@ public class CertificateInfoHelper {
     }
 
 
-    public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException {
-        CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix);
-
-        String encodedCertificate = certInfo.getCertificate();
-        String encodedPublicKey = certInfo.getPublicKey();
-
-        if (encodedCertificate == null && encodedPublicKey == null) {
-            throw new ModelException("Client doesn't have certificate or publicKey configured");
-        }
-
-        if (encodedCertificate != null && encodedPublicKey != null) {
-            throw new ModelException("Client has both publicKey and certificate configured");
-        }
-
-        // TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
-        if (encodedCertificate != null) {
-            X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
-            return clientCert.getPublicKey();
-        } else {
-            return KeycloakModelUtils.getPublicKey(encodedPublicKey);
-        }
-    }
-
-
     // CLIENT REPRESENTATION METHODS
 
     public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
         String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
         String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
         String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
+        String kidAttribute = attributePrefix + "." + KID;
 
         if (rep.getPublicKey() == null && rep.getCertificate() == null) {
             throw new IllegalStateException("Both certificate and publicKey are null!");
@@ -123,6 +106,7 @@ public class CertificateInfoHelper {
         setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
         setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
         setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
+        setOrRemoveAttr(client, kidAttribute, rep.getKid());
     }
 
     private static void setOrRemoveAttr(ClientRepresentation client, String attrName, String attrValue) {
diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
index 0600114..f76057d 100755
--- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
@@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -37,8 +38,8 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
 	public static final String PROFILE_URL = "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name";
 	public static final String DEFAULT_SCOPE = "email";
 
-	public FacebookIdentityProvider(OAuth2IdentityProviderConfig config) {
-		super(config);
+	public FacebookIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+		super(session, config);
 		config.setAuthorizationUrl(AUTH_URL);
 		config.setTokenUrl(TOKEN_URL);
 		config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java
index 70bcd91..26149a1 100755
--- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProviderFactory.java
@@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
@@ -34,8 +35,8 @@ public class FacebookIdentityProviderFactory extends AbstractIdentityProviderFac
     }
 
     @Override
-    public FacebookIdentityProvider create(IdentityProviderModel model) {
-        return new FacebookIdentityProvider(new OAuth2IdentityProviderConfig(model));
+    public FacebookIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new FacebookIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
index c10af75..cf2a53e 100755
--- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
@@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -37,8 +38,8 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
 	public static final String PROFILE_URL = "https://api.github.com/user";
 	public static final String DEFAULT_SCOPE = "user:email";
 
-	public GitHubIdentityProvider(OAuth2IdentityProviderConfig config) {
-		super(config);
+	public GitHubIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+		super(session, config);
 		config.setAuthorizationUrl(AUTH_URL);
 		config.setTokenUrl(TOKEN_URL);
 		config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java
index e305959..e1c05b8 100755
--- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProviderFactory.java
@@ -18,8 +18,9 @@ package org.keycloak.social.github;
 
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
-import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
@@ -34,8 +35,8 @@ public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFacto
     }
 
     @Override
-    public GitHubIdentityProvider create(IdentityProviderModel model) {
-        return new GitHubIdentityProvider(new OAuth2IdentityProviderConfig(model));
+    public GitHubIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new GitHubIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
index 81b349e..060f51a 100755
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.social.google;
 import org.keycloak.broker.oidc.OIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -30,8 +31,8 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
     public static final String PROFILE_URL = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
     public static final String DEFAULT_SCOPE = "openid profile email";
 
-    public GoogleIdentityProvider(OIDCIdentityProviderConfig config) {
-        super(config);
+    public GoogleIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
+        super(session, config);
         config.setAuthorizationUrl(AUTH_URL);
         config.setTokenUrl(TOKEN_URL);
         config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java
index 0dfbfe3..6db6219 100755
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProviderFactory.java
@@ -18,8 +18,9 @@ package org.keycloak.social.google;
 
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
-import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
@@ -34,8 +35,8 @@ public class GoogleIdentityProviderFactory extends AbstractIdentityProviderFacto
     }
 
     @Override
-    public GoogleIdentityProvider create(IdentityProviderModel model) {
-        return new GoogleIdentityProvider(new OIDCIdentityProviderConfig(model));
+    public GoogleIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new GoogleIdentityProvider(session, new OIDCIdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
index 3327858..3c424f6 100755
--- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
@@ -26,6 +26,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
 
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -45,8 +46,8 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
 	public static final String PROFILE_URL = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,public-profile-url)?format=json";
 	public static final String DEFAULT_SCOPE = "r_basicprofile r_emailaddress";
 
-	public LinkedInIdentityProvider(OAuth2IdentityProviderConfig config) {
-		super(config);
+	public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+		super(session, config);
 		config.setAuthorizationUrl(AUTH_URL);
 		config.setTokenUrl(TOKEN_URL);
 		config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java
index 7dafa33..10b94b4 100755
--- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java
@@ -18,8 +18,9 @@ package org.keycloak.social.linkedin;
 
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
-import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Vlastimil Elias (velias at redhat dot com)
@@ -35,8 +36,8 @@ public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFac
 	}
 
 	@Override
-	public LinkedInIdentityProvider create(IdentityProviderModel model) {
-		return new LinkedInIdentityProvider(new OAuth2IdentityProviderConfig(model));
+	public LinkedInIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+		return new LinkedInIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
 	}
 
 	@Override
diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
index 7702738..a935141 100755
--- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
@@ -28,6 +28,9 @@ import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.models.KeycloakSession;
+
 import java.net.URLEncoder;
 
 /**
@@ -45,8 +48,8 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
     public static final String PROFILE_URL = "https://apis.live.net/v5.0/me";
     public static final String DEFAULT_SCOPE = "wl.basic,wl.emails";
 
-    public MicrosoftIdentityProvider(OAuth2IdentityProviderConfig config) {
-        super(config);
+    public MicrosoftIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+        super(session, config);
         config.setAuthorizationUrl(AUTH_URL);
         config.setTokenUrl(TOKEN_URL);
         config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java
index 15cd91d..eee3e87 100644
--- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProviderFactory.java
@@ -20,6 +20,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Vlastimil Elias (velias at redhat dot com)
@@ -34,8 +35,8 @@ public class MicrosoftIdentityProviderFactory extends AbstractIdentityProviderFa
     }
 
     @Override
-    public MicrosoftIdentityProvider create(IdentityProviderModel model) {
-        return new MicrosoftIdentityProvider(new OAuth2IdentityProviderConfig(model));
+    public MicrosoftIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new MicrosoftIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
index 130a8f3..4f9e4b2 100755
--- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
 
 import java.io.StringWriter;
 import java.net.MalformedURLException;
@@ -46,8 +47,8 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
 	public static final String PROFILE_URL = "https://api.stackexchange.com/2.2/me?order=desc&sort=name&site=stackoverflow";
 	public static final String DEFAULT_SCOPE = "";
 
-	public StackoverflowIdentityProvider(StackOverflowIdentityProviderConfig config) {
-		super(config);
+	public StackoverflowIdentityProvider(KeycloakSession session, StackOverflowIdentityProviderConfig config) {
+		super(session, config);
 		config.setAuthorizationUrl(AUTH_URL);
 		config.setTokenUrl(TOKEN_URL);
 		config.setUserInfoUrl(PROFILE_URL);
diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java
index a0f6cf5..4c8deb9 100755
--- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java
@@ -17,8 +17,9 @@
 package org.keycloak.social.stackoverflow;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
-import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Vlastimil Elias (velias at redhat dot com)
@@ -35,8 +36,8 @@ public class StackoverflowIdentityProviderFactory extends
 	}
 
 	@Override
-	public StackoverflowIdentityProvider create(IdentityProviderModel model) {
-		return new StackoverflowIdentityProvider(new StackOverflowIdentityProviderConfig(model));
+	public StackoverflowIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+		return new StackoverflowIdentityProvider(session, new StackOverflowIdentityProviderConfig(model));
 	}
 
 	@Override
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index 630f832..d0178d6 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -57,8 +57,8 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
         SocialIdentityProvider<OAuth2IdentityProviderConfig> {
 
     protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
-    public TwitterIdentityProvider(OAuth2IdentityProviderConfig config) {
-        super(config);
+    public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
+        super(session, config);
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java
index 1e0504d..74e1a3d 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProviderFactory.java
@@ -18,8 +18,9 @@ package org.keycloak.social.twitter;
 
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
-import org.keycloak.broker.social.SocialIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
@@ -34,8 +35,8 @@ public class TwitterIdentityProviderFactory extends AbstractIdentityProviderFact
     }
 
     @Override
-    public TwitterIdentityProvider create(IdentityProviderModel model) {
-        return new TwitterIdentityProvider(new OAuth2IdentityProviderConfig(model));
+    public TwitterIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new TwitterIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
     }
 
     @Override
diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js
new file mode 100644
index 0000000..73bb124
--- /dev/null
+++ b/services/src/main/resources/scripts/authenticator-template.js
@@ -0,0 +1,37 @@
+/*
+ * Template for JavaScript based authenticator's.
+ * See org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory
+ */
+
+// import enum for error lookup
+AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+
+/**
+ * An example authenticate function.
+ *
+ * The following variables are available for convenience:
+ * user - current user {@see org.keycloak.models.UserModel}
+ * realm - current realm {@see org.keycloak.models.RealmModel}
+ * session - current KeycloakSession {@see org.keycloak.models.KeycloakSession}
+ * httpRequest - current HttpRequest {@see org.jboss.resteasy.spi.HttpRequest}
+ * script - current script {@see org.keycloak.models.ScriptModel}
+ * LOG - current logger {@see org.jboss.logging.Logger}
+ *
+ * You one can extract current http request headers via:
+ * httpRequest.getHttpHeaders().getHeaderString("Forwarded")
+ *
+ * @param context {@see org.keycloak.authentication.AuthenticationFlowContext}
+ */
+function authenticate(context) {
+
+    LOG.info(script.name + " trace auth for: " + user.username);
+
+    var authShouldFail = false;
+    if (authShouldFail) {
+
+        context.failure(AuthenticationFlowError.INVALID_USER);
+        return;
+    }
+
+    context.success();
+}
\ No newline at end of file
diff --git a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
index 4b97d2b..e706f54 100755
--- a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
+++ b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java
@@ -24,6 +24,7 @@ import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -129,7 +130,7 @@ public class AbstractOAuth2IdentityProviderTest {
 	private static class TestProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> {
 
 		public TestProvider(OAuth2IdentityProviderConfig config) {
-			super(config);
+			super(null, config);
 		}
 
 		@Override
diff --git a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
index ff4c368..0598ef6 100755
--- a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
+++ b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
@@ -32,32 +32,32 @@ public class IdentityProviderBeanTest {
     @Test
     public void testIdentityProviderComparator() {
 
-        IdentityProvider o1 = new IdentityProvider("alias1", "id1", "ur1", null);
-        IdentityProvider o2 = new IdentityProvider("alias2", "id2", "ur2", null);
+        IdentityProvider o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", null);
+        IdentityProvider o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", null);
 
         // guiOrder not defined at any object - first is always lower
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
 
         // guiOrder is not a number so it is same as not defined - first is always lower
-        o1 = new IdentityProvider("alias1", "id1", "ur1", "not a number");
+        o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "not a number");
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
 
         // guiOrder is defined for one only to it is always first
-        o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
+        o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
         Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
 
         // guiOrder is defined for both but is same - first is always lower
-        o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
-        o2 = new IdentityProvider("alias2", "id2", "ur2", "0");
+        o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
+        o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "0");
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
 
         // guiOrder is reflected
-        o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
-        o2 = new IdentityProvider("alias2", "id2", "ur2", "1");
+        o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
+        o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "1");
         Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
         Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/IdentityProviderRegistrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/IdentityProviderRegistrationTest.java
index 51470ae..42caa29 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/IdentityProviderRegistrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/IdentityProviderRegistrationTest.java
@@ -64,7 +64,7 @@ public class IdentityProviderRegistrationTest extends AbstractIdentityProviderMo
 
         identityProviderModel.setAlias("custom-provider");
 
-        CustomSocialProvider customSocialProvider = providerFactory.create(identityProviderModel);
+        CustomSocialProvider customSocialProvider = providerFactory.create(this.session, identityProviderModel);
 
         assertNotNull(customSocialProvider);
         IdentityProviderModel config = customSocialProvider.getConfig();
@@ -87,7 +87,7 @@ public class IdentityProviderRegistrationTest extends AbstractIdentityProviderMo
 
         identityProviderModel.setAlias("custom-provider");
 
-        CustomIdentityProvider provider = providerFactory.create(identityProviderModel);
+        CustomIdentityProvider provider = providerFactory.create(this.session, identityProviderModel);
 
         assertNotNull(provider);
         IdentityProviderModel config = provider.getConfig();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index cee8544..dbf30e9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -158,7 +158,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertGoogleIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        GoogleIdentityProvider googleIdentityProvider = new GoogleIdentityProviderFactory().create(identityProvider);
+        GoogleIdentityProvider googleIdentityProvider = new GoogleIdentityProviderFactory().create(session, identityProvider);
         OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig();
 
         assertEquals("model-google", config.getAlias());
@@ -176,7 +176,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertSamlIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        SAMLIdentityProvider samlIdentityProvider = new SAMLIdentityProviderFactory().create(identityProvider);
+        SAMLIdentityProvider samlIdentityProvider = new SAMLIdentityProviderFactory().create(session, identityProvider);
         SAMLIdentityProviderConfig config = samlIdentityProvider.getConfig();
 
         assertEquals("model-saml-signed-idp", config.getAlias());
@@ -196,7 +196,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertOidcIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        OIDCIdentityProvider googleIdentityProvider = new OIDCIdentityProviderFactory().create(identityProvider);
+        OIDCIdentityProvider googleIdentityProvider = new OIDCIdentityProviderFactory().create(session, identityProvider);
         OIDCIdentityProviderConfig config = googleIdentityProvider.getConfig();
 
         assertEquals("model-oidc-idp", config.getAlias());
@@ -210,7 +210,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertFacebookIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
-        FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(identityProvider);
+        FacebookIdentityProvider facebookIdentityProvider = new FacebookIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = facebookIdentityProvider.getConfig();
 
         assertEquals("model-facebook", config.getAlias());
@@ -229,7 +229,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertGitHubIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
-        GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(identityProvider);
+        GitHubIdentityProvider gitHubIdentityProvider = new GitHubIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
 
         assertEquals("model-github", config.getAlias());
@@ -248,7 +248,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(identityProvider);
+        LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = liIdentityProvider.getConfig();
 
         assertEquals("model-linkedin", config.getAlias());
@@ -265,7 +265,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertStackoverflowIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        StackoverflowIdentityProvider soIdentityProvider = new StackoverflowIdentityProviderFactory().create(identityProvider);
+        StackoverflowIdentityProvider soIdentityProvider = new StackoverflowIdentityProviderFactory().create(session, identityProvider);
         StackOverflowIdentityProviderConfig config = soIdentityProvider.getConfig();
 
         assertEquals("model-stackoverflow", config.getAlias());
@@ -283,7 +283,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
     }
 
     private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) {
-        TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(identityProvider);
+        TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig();
 
         assertEquals("model-twitter", config.getAlias());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
index ab195ea..0552875 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
@@ -179,4 +179,5 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
     public void testAccountManagementLinkedIdentityAlreadyExists() {
         super.testAccountManagementLinkedIdentityAlreadyExists();
     }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java
new file mode 100644
index 0000000..910356d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.broker;
+
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.Constants;
+import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest {
+
+    private static final int PORT = 8082;
+
+    private static Keycloak keycloak1;
+    private static Keycloak keycloak2;
+
+    @ClassRule
+    public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
+
+        @Override
+        protected void configureServer(KeycloakServer server) {
+            server.getConfig().setPort(PORT);
+        }
+
+        @Override
+        protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
+            server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
+        }
+
+        @Override
+        protected String[] getTestRealms() {
+            return new String[] { "realm-with-oidc-identity-provider" };
+        }
+    };
+
+    @BeforeClass
+    public static void beforeClazz() {
+        keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
+        keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
+    }
+
+    @Override
+    public void onBefore() {
+        super.onBefore();
+
+        // Enable validate signatures
+        IdentityProviderModel idpModel = getIdentityProviderModel();
+        OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
+        cfg.setValidateSignature(true);
+        getRealm().updateIdentityProvider(cfg);
+
+        brokerServerRule.stopSession(this.session, true);
+        this.session = brokerServerRule.startSession();
+    }
+
+    @Override
+    protected String getProviderId() {
+        return "kc-oidc-idp";
+    }
+
+    @Test
+    public void testSignatureVerificationJwksUrl() throws Exception {
+        // Configure OIDC identity provider with JWKS URL
+        IdentityProviderModel idpModel = getIdentityProviderModel();
+        OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
+        cfg.setUseJwksUrl(true);
+
+        UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT));
+        String jwksUrl = b.build("realm-with-oidc-identity-provider").toString();
+        cfg.setJwksUrl(jwksUrl);
+        getRealm().updateIdentityProvider(cfg);
+
+        brokerServerRule.stopSession(this.session, true);
+        this.session = brokerServerRule.startSession();
+
+        // Check that user is able to login
+        assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
+
+        // Rotate public keys on the parent broker
+        RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
+        realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
+        keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
+
+        // User not able to login now as new keys can't be yet downloaded (10s timeout)
+        loginIDP("test-user");
+        assertTrue(errorPage.isCurrent());
+        assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
+
+        keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
+
+        // Set time offset. New keys can be downloaded. Check that user is able to login.
+        Time.setOffset(20);
+
+        assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
+
+        Time.setOffset(0);
+    }
+
+    @Test
+    public void testSignatureVerificationHardcodedPublicKey() throws Exception {
+        // Configure OIDC identity provider with publicKeySignatureVerifier
+        IdentityProviderModel idpModel = getIdentityProviderModel();
+        OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
+        cfg.setUseJwksUrl(false);
+
+        RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
+        cfg.setPublicKeySignatureVerifier(realm.getPublicKey());
+        getRealm().updateIdentityProvider(cfg);
+
+        brokerServerRule.stopSession(this.session, true);
+        this.session = brokerServerRule.startSession();
+
+        // Check that user is able to login
+        assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
+
+        // Rotate public keys on the parent broker
+        realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
+        keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
+
+        // User not able to login now as new keys can't be yet downloaded (10s timeout)
+        loginIDP("test-user");
+        assertTrue(errorPage.isCurrent());
+        assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
+
+        keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
+
+        // Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
+        Time.setOffset(20);
+
+        loginIDP("test-user");
+        assertTrue(errorPage.isCurrent());
+        assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
+
+        keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
+
+        Time.setOffset(0);
+
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java
index 058f101..7b70924 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProvider.java
@@ -29,8 +29,8 @@ import javax.ws.rs.core.Response;
  */
 public class CustomIdentityProvider extends AbstractIdentityProvider<IdentityProviderModel> {
 
-    public CustomIdentityProvider(IdentityProviderModel config) {
-        super(config);
+    public CustomIdentityProvider(KeycloakSession session, IdentityProviderModel config) {
+        super(session, config);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProviderFactory.java
index 60c7da6..88dab79 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProviderFactory.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/CustomIdentityProviderFactory.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.broker.provider;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 
 /**
  * @author pedroigor
@@ -32,8 +33,8 @@ public class CustomIdentityProviderFactory extends AbstractIdentityProviderFacto
     }
 
     @Override
-    public CustomIdentityProvider create(IdentityProviderModel model) {
-        return new CustomIdentityProvider(model);
+    public CustomIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new CustomIdentityProvider(session, model);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProvider.java
index 869c226..26622b2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProvider.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProvider.java
@@ -30,8 +30,8 @@ import javax.ws.rs.core.Response;
  */
 public class CustomSocialProvider extends AbstractIdentityProvider<IdentityProviderModel> implements SocialIdentityProvider<IdentityProviderModel> {
 
-    public CustomSocialProvider(IdentityProviderModel config) {
-        super(config);
+    public CustomSocialProvider(KeycloakSession session, IdentityProviderModel config) {
+        super(session, config);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProviderFactory.java
index b7ae394..5a0a3c2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProviderFactory.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/provider/social/CustomSocialProviderFactory.java
@@ -18,6 +18,7 @@ package org.keycloak.testsuite.broker.provider.social;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.IdentityProviderModel;
 
 /**
@@ -33,8 +34,8 @@ public class CustomSocialProviderFactory extends AbstractIdentityProviderFactory
     }
 
     @Override
-    public CustomSocialProvider create(IdentityProviderModel model) {
-        return new CustomSocialProvider(model);
+    public CustomSocialProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new CustomSocialProvider(session, model);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
index dba9c15..ab70714 100755
--- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
+++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
@@ -94,6 +94,7 @@
         {
             "alias" : "model-saml-signed-idp",
             "providerId" : "saml",
+            "displayName": "My SAML",
             "enabled": true,
             "config": {
                 "singleSignOnServiceUrl": "http://localhost:8082/auth/realms/realm-with-saml-identity-provider/protocol/saml",
@@ -157,6 +158,7 @@
         {
             "alias" : "model-oidc-idp",
             "providerId" : "oidc",
+            "displayName": "My OIDC",
             "enabled": false,
             "authenticateByDefault" : "false",
             "config": {
@@ -172,6 +174,7 @@
         {
             "alias" : "kc-oidc-idp",
             "providerId" : "keycloak-oidc",
+            "displayName": "My Keycloak OIDC",
             "enabled": true,
             "storeToken" : true,
             "addReadTokenRoleOnCreate": true,
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
index 8b5df6c..6daa6b1 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestingOIDCEndpointsApplicationResource.java
@@ -120,7 +120,8 @@ public class TestingOIDCEndpointsApplicationResource {
             }
 
             PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate();
-            clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey));
+            String kid = JWKBuilder.createKeyId(clientData.getSigningKeyPair().getPublic());
+            clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).rsa256(privateKey));
         } else {
             throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
         }
diff --git a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json b/testsuite/integration-arquillian/test-apps/js-console/example-realm.json
index 4e3adca..e90217a 100755
--- a/testsuite/integration-arquillian/test-apps/js-console/example-realm.json
+++ b/testsuite/integration-arquillian/test-apps/js-console/example-realm.json
@@ -19,6 +19,7 @@
             ],
             "realmRoles": [ "user" ],
             "clientRoles": {
+                "realm-management" : [ "view-realm" ],
                 "account": ["view-profile", "manage-account"]
             }
         },{
diff --git a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html
index 468f98e..1c2b61d 100755
--- a/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html
+++ b/testsuite/integration-arquillian/test-apps/js-console/src/main/webapp/index.html
@@ -29,6 +29,7 @@
     <button onclick="keycloak.register()">Register</button>
     <button onclick="refreshToken(9999)">Refresh Token</button>
     <button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
+    <button onclick="refreshToken(5)">Refresh Token (if <5s validity)</button>
     <button onclick="showError()">Show Error Response</button>
     <button onclick="loadProfile()">Get Profile</button>
     <button onclick="loadUserInfo()">Get User Info</button>
@@ -41,6 +42,13 @@
     <button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
     <button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
     <button onclick="createBearerRequest()">Create Bearer Request</button>
+    <button onclick="output(showTime())">Show current time</button>
+    <input id="timeSkewInput"/>
+    <button onclick="addToTimeSkew()">timeSkew offset</button>
+    <button onclick="refreshTimeSkew()">refresh timeSkew</button>
+    <button onclick="sendBearerToKeycloak()">Bearer to keycloak</button>
+
+
     <select id="flowSelect">
         <option value="standard">standard</option>
         <option value="implicit">implicit</option>
@@ -62,6 +70,9 @@
 <h2>Events</h2>
 <pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
 
+<h2>Info</h2>
+TimeSkew: <div id="timeSkew"></div>
+
 
 <script>
     function loadProfile() {
@@ -135,6 +146,16 @@
         document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
     }
 
+    function addToTimeSkew() {
+        var offset = document.getElementById("timeSkewInput").value;
+        keycloak.timeSkew += parseInt(offset);
+        document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
+    }
+
+    function refreshTimeSkew() {
+        document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
+    }
+
     function createBearerRequest() {
 
         var url = 'http://localhost:8280/js-database/customers';
@@ -167,6 +188,33 @@
         req.send();
     }
 
+    function sendBearerToKeycloak() {
+        var url = 'http://localhost:8180/auth/admin/realms/example/roles';
+        if (window.location.href.indexOf("8543") > -1) {
+            url = url.replace("8180","8543");
+            url = url.replace("http","https");
+        }
+
+        var req = new XMLHttpRequest();
+        req.open('GET', url, true);
+        req.setRequestHeader('Accept', 'application/json');
+        req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
+
+        req.onreadystatechange = function () {
+            if (req.readyState == 4) {
+                if (req.status == 200) {
+                    output('Success');
+                } else if (req.status == 403) {
+                    output('Forbidden');
+                } else if (req.status == 401) {
+                    output('Unauthorized');
+                }
+            }
+        };
+
+        req.send();
+    }
+
     var keycloak;
 
     function keycloakInit() {
@@ -182,6 +230,7 @@
 
         keycloak.onAuthRefreshSuccess = function () {
             event('Auth Refresh Success');
+            document.getElementById("timeSkew").innerHTML = keycloak.timeSkew;
         };
 
         keycloak.onAuthRefreshError = function () {
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
index 428ba07..77bb3b6 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
@@ -27,7 +27,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
@@ -52,7 +52,12 @@ public class AdminAlbumService {
         List<Album> result = this.entityManager.createQuery("from Album").getResultList();
 
         for (Album album : result) {
-            albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
+            //We need to compile this under JDK7 so we can't use lambdas
+            //albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
+
+            if (!albums.containsKey(album.getUserId())) {
+                albums.put(album.getUserId(), Collections.singletonList(album));
+            }
         }
 
         return Response.ok(albums).build();
diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml
index 387ac61..cae4c57 100644
--- a/testsuite/integration-arquillian/test-apps/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/pom.xml
@@ -21,5 +21,18 @@
         <module>photoz</module>
         <module>hello-world-authz-service</module>
         <module>servlet-authz</module>
+        <module>servlets</module>
     </modules>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
 </project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlets/pom.xml b/testsuite/integration-arquillian/test-apps/servlets/pom.xml
new file mode 100644
index 0000000..fc10498
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlets/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>integration-arquillian-test-apps</artifactId>
+        <groupId>org.keycloak.testsuite</groupId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-test-apps-servlets</artifactId>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.ws.rs</groupId>
+            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-spi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-saml-adapter-api-public</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-saml-core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java
index 2a57739..4822c4b 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/JSConsoleTestApp.java
@@ -56,6 +56,8 @@ public class JSConsoleTestApp extends AbstractPageWithInjectedUrl {
     private WebElement refreshTokenButton;
     @FindBy(xpath = "//button[contains(text(),'Refresh Token (if <30s')]")
     private WebElement refreshTokenIfUnder30sButton;
+    @FindBy(xpath = "//button[contains(text(),'Refresh Token (if <5s')]")
+    private WebElement refreshTokenIfUnder5sButton;
     @FindBy(xpath = "//button[text() = 'Get Profile']")
     private WebElement getProfileButton;
 
@@ -73,6 +75,17 @@ public class JSConsoleTestApp extends AbstractPageWithInjectedUrl {
     private WebElement showDetailsButton;
     @FindBy(xpath = "//button[text() = 'Create Bearer Request']")
     private WebElement createBearerRequest;
+    @FindBy(xpath = "//button[text() = 'Bearer to keycloak']")
+    private WebElement createBearerRequestToKeycloakButton;
+    @FindBy(xpath = "//button[text() = 'refresh timeSkew']")
+    private WebElement refreshTimeSkewButton;
+
+    @FindBy(id = "timeSkew")
+    private WebElement timeSkewValue;
+    @FindBy(id = "timeSkewInput")
+    private WebElement timeSkewInput;
+    @FindBy(xpath = "//button[text() = 'timeSkew offset']")
+    private WebElement timeSkewButton;
 
     @FindBy(id = "flowSelect")
     private Select flowSelect;
@@ -104,6 +117,10 @@ public class JSConsoleTestApp extends AbstractPageWithInjectedUrl {
         refreshTokenIfUnder30sButton.click();
     }
 
+    public void refreshTokenIfUnder5s() {
+        refreshTokenIfUnder5sButton.click();
+    }
+
     public void getProfile() {
         getProfileButton.click();
     }
@@ -124,6 +141,10 @@ public class JSConsoleTestApp extends AbstractPageWithInjectedUrl {
         createBearerRequest.click();
     }
 
+    public void createBearerRequestToKeycloak() {
+        createBearerRequestToKeycloakButton.click();
+    }
+
     public void setResponseMode(String value) {
         responseModeSelect.selectByValue(value);
     }
@@ -143,4 +164,18 @@ public class JSConsoleTestApp extends AbstractPageWithInjectedUrl {
     public void showErrorResponse() {
         showErrorButton.click();
     }
+
+    public WebElement getTimeSkewValue() {
+        return timeSkewValue;
+    }
+
+    public void setTimeSkewOffset(int value) {
+        timeSkewInput.clear();
+        timeSkewInput.sendKeys(Integer.toString(value));
+        timeSkewButton.click();
+    }
+
+    public void refreshTimeSkew() {
+        refreshTimeSkewButton.click();
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
index 280eaf8..ea4725a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
@@ -74,6 +74,7 @@ public final class WaitUtils {
     }
 
     public static void pause(long millis) {
+        log.info("Wait: " + millis + "ms");
         try {
             Thread.sleep(millis);
         } catch (InterruptedException ex) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 7a65a0b..0dc7597 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -46,6 +46,7 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
 import org.keycloak.testsuite.pages.ErrorPage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.util.IdentityProviderBuilder;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.RealmBuilder;
 import org.keycloak.testsuite.util.UserBuilder;
@@ -74,6 +75,20 @@ public class AccountTest extends TestRealmKeycloakTest {
                                               .password("password")
                                               .build();
 
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                                              .providerId("github")
+                                              .alias("github")
+                                              .build());
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                                              .providerId("saml")
+                                              .alias("mysaml")
+                                              .build());
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                                              .providerId("oidc")
+                                              .alias("myoidc")
+                                              .displayName("MyOIDC")
+                                              .build());
+
         RealmBuilder.edit(testRealm)
                     .user(user2);
     }
@@ -789,4 +804,13 @@ public class AccountTest extends TestRealmKeycloakTest {
         events.clear();
     }
 
+    @Test
+    public void testIdentityProviderCapitalization(){
+        loginPage.open();
+        Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
+        Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText());
+        Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
+
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java
index a91b26b..481f022 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/AbstractJSConsoleExampleAdapterTest.java
@@ -66,6 +66,8 @@ public abstract class AbstractJSConsoleExampleAdapterTest extends AbstractExampl
     @Page
     private Applications applicationsPage;
 
+    private static int TIME_SKEW_TOLERANCE = 3;
+
     public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
 
     @Deployment(name = JSConsoleTestApp.DEPLOYMENT_NAME)
@@ -351,6 +353,42 @@ public abstract class AbstractJSConsoleExampleAdapterTest extends AbstractExampl
         waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Init Success (Authenticated)");
     }
 
+    @Test
+    public void testUpdateToken() {
+        logInAndInit("standard");
+
+        jsConsoleTestAppPage.setTimeSkewOffset(-33);
+        setTimeOffset(33);
+
+        jsConsoleTestAppPage.refreshTokenIfUnder5s();
+
+        jsConsoleTestAppPage.setTimeSkewOffset(-34);
+        setTimeOffset(67);
+
+        jsConsoleTestAppPage.refreshTokenIfUnder5s();
+        jsConsoleTestAppPage.createBearerRequestToKeycloak();
+        waitUntilElement(jsConsoleTestAppPage.getOutputElement()).text().contains("Success");
+    }
+
+    @Test
+    public void timeSkewTest() {
+        logInAndInit("standard");
+
+        jsConsoleTestAppPage.refreshTimeSkew();
+
+        int timeSkew = Integer.parseInt(jsConsoleTestAppPage.getTimeSkewValue().getText());
+        assertTrue("TimeSkew was: " + timeSkew + ", but should be ~0", timeSkew >= 0 - TIME_SKEW_TOLERANCE);
+        assertTrue("TimeSkew was: " + timeSkew + ", but should be ~0", timeSkew  <= TIME_SKEW_TOLERANCE);
+
+        setTimeOffset(40);
+        jsConsoleTestAppPage.refreshToken();
+        jsConsoleTestAppPage.refreshTimeSkew();
+
+        timeSkew = Integer.parseInt(jsConsoleTestAppPage.getTimeSkewValue().getText());
+        assertTrue("TimeSkew was: " + timeSkew + ", but should be ~-40", timeSkew + 40 >= 0 - TIME_SKEW_TOLERANCE);
+        assertTrue("TimeSkew was: " + timeSkew + ", but should be ~-40", timeSkew + 40  <= TIME_SKEW_TOLERANCE);
+    }
+
     private void setImplicitFlowForClient() {
         ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "js-console");
         ClientRepresentation client = clientResource.toRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
index cfb1818..e742b91 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
@@ -31,6 +31,7 @@ import org.keycloak.common.Version;
 import org.keycloak.common.util.Time;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.models.Constants;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.AccessToken;
@@ -79,12 +80,14 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
 import static org.keycloak.testsuite.util.WaitUtils.pause;
 import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
@@ -277,6 +280,43 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
     }
 
     @Test
+    public void testClientWithJwksUri() throws Exception {
+        // Set client to bad JWKS URI
+        ClientResource clientResource = ApiUtil.findClientResourceByClientId(testRealmResource(), "secure-portal");
+        ClientRepresentation client = clientResource.toRepresentation();
+        OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
+        wrapper.setUseJwksUrl(true);
+        wrapper.setJwksUrl(securePortal + "/bad-jwks-url");
+        clientResource.update(client);
+
+        // Login should fail at the code-to-token
+        securePortal.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        String pageSource = driver.getPageSource();
+        assertCurrentUrlStartsWith(securePortal);
+        assertFalse(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+        // Set client to correct JWKS URI
+        client = clientResource.toRepresentation();
+        wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
+        wrapper.setUseJwksUrl(true);
+        wrapper.setJwksUrl(securePortal + "/" + AdapterConstants.K_JWKS);
+        clientResource.update(client);
+
+        // Login to secure-portal should be fine now. Client keys downloaded from JWKS URI
+        securePortal.navigateTo();
+        assertCurrentUrlEquals(securePortal);
+        pageSource = driver.getPageSource();
+        assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+        // Logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, securePortal.toString()).build("demo").toString();
+        driver.navigate().to(logoutUri);
+    }
+
+    @Test
     public void testLoginSSOAndLogout() {
         // test login to customer-portal which does a bearer request to customer-db
         customerPortal.navigateTo();
@@ -562,7 +602,7 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         token = tokenMinTTLPage.getAccessToken();
         int tokenIssued2 = token.getIssuedAt();
         Assert.assertEquals(tokenIssued1, tokenIssued2);
-        Assert.assertFalse(token.isExpired());
+        assertFalse(token.isExpired());
 
         // Sets 9 minutes offset and assert access token will be refreshed (accessTokenTimeout is 10 minutes, token-min-ttl is 2 minutes. Hence 8 minutes or more should be sufficient)
         setAdapterAndServerTimeOffset(540, tokenMinTTLPage.toString());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 9604d03..c46a620 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -135,7 +135,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.");
         addProviderInfo(result, "auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server.");
         addProviderInfo(result, "auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form.");
-        addProviderInfo(result, "auth-script-based", "Script-based Authentication", "Script based authentication.");
+        addProviderInfo(result, "auth-script-based", "Script", "Script based authentication. Allows to define custom authentication logic via JavaScript.");
         addProviderInfo(result, "auth-spnego", "Kerberos", "Initiates the SPNEGO protocol.  Most often used with Kerberos.");
         addProviderInfo(result, "auth-username-password-form", "Username Password Form",
                 "Validates a username and password from login form.");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
index fb0a863..8407610 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
@@ -205,6 +205,7 @@ public class ConsentsTest extends AbstractKeycloakTest {
         IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
 
         identityProviderRepresentation.setAlias(alias);
+        identityProviderRepresentation.setDisplayName(providerId);
         identityProviderRepresentation.setProviderId(providerId);
         identityProviderRepresentation.setEnabled(true);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
index 13f15f8..c9c4191 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
@@ -179,6 +179,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
         IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
 
         idp.setAlias(id);
+        idp.setDisplayName(id);
         idp.setProviderId(providerId);
         idp.setEnabled(true);
         if (config != null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
index b2369c7..14f8d0a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
@@ -1441,7 +1441,8 @@ public class PermissionsTest extends AbstractKeycloakTest {
         }, Resource.IDENTITY_PROVIDER, false);
         invoke(new InvocationWithResponse() {
             public void invoke(RealmResource realm, AtomicReference<Response> response) {
-                response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch").alias("foo").build()));
+                response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch")
+                        .displayName("nosuch-foo").alias("foo").build()));
             }
         }, Resource.IDENTITY_PROVIDER, true);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
index 1885280..0b568b8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
@@ -126,6 +126,7 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
         IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
 
         identityProviderRepresentation.setAlias(alias);
+        identityProviderRepresentation.setDisplayName(providerId);
         identityProviderRepresentation.setProviderId(providerId);
         identityProviderRepresentation.setEnabled(true);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
index 394ad86..b4ab3e5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -17,32 +17,18 @@
 
 package org.keycloak.testsuite.client;
 
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.message.BasicNameValuePair;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
 import org.keycloak.client.registration.Auth;
 import org.keycloak.client.registration.ClientRegistrationException;
 import org.keycloak.client.registration.HttpErrorException;
 import org.keycloak.common.util.CollectionUtil;
-import org.keycloak.common.util.KeycloakUriBuilder;
-import org.keycloak.constants.ServiceUrlConstants;
-import org.keycloak.jose.jwk.JSONWebKeySet;
 import org.keycloak.jose.jws.Algorithm;
-import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.protocol.oidc.utils.OIDCResponseType;
-import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.JsonWebToken;
 import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
 import org.keycloak.representations.idm.ClientInitialAccessPresentation;
 import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
@@ -50,19 +36,10 @@ import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.oidc.OIDCClientRepresentation;
 import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
-import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
-import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
-import org.keycloak.testsuite.util.OAuthClient;
+
+import java.util.*;
 
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import java.security.PrivateKey;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -237,59 +214,6 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
     }
 
     @Test
-    public void createClientWithJWKS() throws Exception {
-        OIDCClientRepresentation clientRep = createRep();
-
-        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
-        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
-
-        // Generate keys for client
-        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
-        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
-
-        JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
-        clientRep.setJwks(keySet);
-
-        OIDCClientRepresentation response = reg.oidc().create(clientRep);
-        Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
-        Assert.assertNull(response.getClientSecret());
-        Assert.assertNull(response.getClientSecretExpiresAt());
-
-        // Tries to authenticate client with privateKey JWT
-        String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
-        OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
-        Assert.assertEquals(200, accessTokenResponse.getStatusCode());
-        AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
-        Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
-    }
-
-    @Test
-    public void createClientWithJWKSURI() throws Exception {
-        OIDCClientRepresentation clientRep = createRep();
-
-        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
-        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
-
-        // Generate keys for client
-        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
-        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
-
-        clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
-
-        OIDCClientRepresentation response = reg.oidc().create(clientRep);
-        Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
-        Assert.assertNull(response.getClientSecret());
-        Assert.assertNull(response.getClientSecretExpiresAt());
-
-        // Tries to authenticate client with privateKey JWT
-        String signedJwt = getClientSignedJWT(response.getClientId(), generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY));
-        OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
-        Assert.assertEquals(200, accessTokenResponse.getStatusCode());
-        AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
-        Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
-    }
-
-    @Test
     public void testSignaturesRequired() throws Exception {
         OIDCClientRepresentation clientRep = createRep();
         clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
@@ -307,55 +231,6 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
         Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
     }
 
-
-    // Client auth with signedJWT - helper methods
-
-    private String getClientSignedJWT(String clientId, String privateKeyPem) {
-        String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
-
-        PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem);
-
-        // Use token-endpoint as audience as OIDC conformance testsuite is using it too.
-        JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() {
-
-            @Override
-            protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
-                JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
-                String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString();
-                jwt.audience(tokenEndpointUrl);
-                return jwt;
-            }
-
-        };
-        jwtProvider.setPrivateKey(privateKey);
-        jwtProvider.setTokenTimeout(10);
-        return jwtProvider.createSignedRequestToken(clientId, realmInfoUrl);
-
-    }
-
-
-    private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
-        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
-        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
-        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
-        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
-
-        HttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
-        return new OAuthClient.AccessTokenResponse(response);
-    }
-
-    private HttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
-        CloseableHttpClient client = new DefaultHttpClient();
-        try {
-            HttpPost post = new HttpPost(requestUrl);
-            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
-            post.setEntity(formEntity);
-            return client.execute(post);
-        } finally {
-            oauth.closeClient(client);
-        }
-    }
-
     private void createTrustedHost(String name, int count) {
         Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create(name, count, count));
         Assert.assertEquals(201, response.getStatus());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java
new file mode 100644
index 0000000..5a86d59
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCJwksClientRegistrationTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.client;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.jose.jwk.JSONWebKeySet;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
+import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
+import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTest {
+
+    private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
+    private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        super.addTestRealms(testRealms);
+        testRealms.get(0).setPrivateKey(PRIVATE_KEY);
+        testRealms.get(0).setPublicKey(PUBLIC_KEY);
+    }
+
+    @Before
+    public void before() throws Exception {
+        super.before();
+
+        ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
+        reg.auth(Auth.token(token));
+    }
+
+    private OIDCClientRepresentation createRep() {
+        OIDCClientRepresentation client = new OIDCClientRepresentation();
+        client.setClientName("RegistrationAccessTokenTest");
+        client.setClientUri(OAuthClient.APP_ROOT);
+        client.setRedirectUris(Collections.singletonList(oauth.getRedirectUri()));
+        return client;
+    }
+
+
+    @Test
+    public void createClientWithJWKS_generatedKid() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+
+        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+        // Generate keys for client
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+        JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
+        clientRep.setJwks(keySet);
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+        Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
+        Assert.assertNull(response.getClientSecret());
+        Assert.assertNull(response.getClientSecretExpiresAt());
+
+        // Tries to authenticate client with privateKey JWT
+        assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
+    }
+
+
+    // The "kid" is null in the signed JWT. This is backwards compatibility test as in versions prior to 2.3.0, the "kid" wasn't set by JWTClientCredentialsProvider
+    @Test
+    public void createClientWithJWKS_nullKid() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+
+        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+        // Generate keys for client
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+        JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
+        clientRep.setJwks(keySet);
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+
+        // Tries to authenticate client with privateKey JWT
+        assertAuthenticateClientSuccess(generatedKeys, response, null);
+    }
+
+
+    // The "kid" is set manually to some custom value
+    @Test
+    public void createClientWithJWKS_customKid() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+
+        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+        // Generate keys for client
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+        JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
+
+        // Override kid with custom value
+        keySet.getKeys()[0].setKeyId("a1");
+        clientRep.setJwks(keySet);
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+
+        // Tries to authenticate client with privateKey JWT
+        assertAuthenticateClientSuccess(generatedKeys, response, "a1");
+    }
+
+    @Test
+    public void createClientWithJWKSURI() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+
+        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+        // Generate keys for client
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+        clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+        Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
+        Assert.assertNull(response.getClientSecret());
+        Assert.assertNull(response.getClientSecretExpiresAt());
+        Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri());
+
+        // Tries to authenticate client with privateKey JWT
+        assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
+    }
+
+    @Test
+    public void createClientWithJWKSURI_rotateClientKeys() throws Exception {
+        OIDCClientRepresentation clientRep = createRep();
+
+        clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
+        clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.PRIVATE_KEY_JWT);
+
+        // Generate keys for client
+        TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+        Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
+
+        clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
+
+        OIDCClientRepresentation response = reg.oidc().create(clientRep);
+        Assert.assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, response.getTokenEndpointAuthMethod());
+        Assert.assertNull(response.getClientSecret());
+        Assert.assertNull(response.getClientSecretExpiresAt());
+        Assert.assertEquals(response.getJwksUri(), TestApplicationResourceUrls.clientJwksUri());
+
+        // Tries to authenticate client with privateKey JWT
+        assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
+
+        // Add new key to the jwks
+        Map<String, String> generatedKeys2 = oidcClientEndpointsResource.generateKeys();
+
+        // Error should happen. KeyStorageProvider won't yet download new keys because of timeout
+        assertAuthenticateClientError(generatedKeys2, response, KEEP_GENERATED_KID);
+
+        setTimeOffset(20);
+
+        // Now new keys should be successfully downloaded
+        assertAuthenticateClientSuccess(generatedKeys2, response, KEEP_GENERATED_KID);
+    }
+
+
+    // Client auth with signedJWT - helper methods
+
+    private void assertAuthenticateClientSuccess(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception {
+        KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys);
+        String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid);
+        OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
+        Assert.assertEquals(200, accessTokenResponse.getStatusCode());
+        AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
+        Assert.assertEquals(response.getClientId(), accessToken.getAudience()[0]);
+    }
+
+    private void assertAuthenticateClientError(Map<String, String> generatedKeys, OIDCClientRepresentation response, String kid) throws Exception {
+        KeyPair keyPair = getKeyPairFromGeneratedPems(generatedKeys);
+        String signedJwt = getClientSignedJWT(response.getClientId(), keyPair, kid);
+        OAuthClient.AccessTokenResponse accessTokenResponse = doClientCredentialsGrantRequest(signedJwt);
+        Assert.assertEquals(400, accessTokenResponse.getStatusCode());
+        Assert.assertNull(accessTokenResponse.getAccessToken());
+        Assert.assertNotNull(accessTokenResponse.getError());
+    }
+
+    private KeyPair getKeyPairFromGeneratedPems(Map<String, String> generatedKeys) {
+        String privateKeyPem = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
+        String publicKeyPem =  generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
+        PrivateKey privateKey = KeycloakModelUtils.getPrivateKey(privateKeyPem);
+        PublicKey publicKey = KeycloakModelUtils.getPublicKey(publicKeyPem);
+        return new KeyPair(publicKey, privateKey);
+    }
+
+    private static final String KEEP_GENERATED_KID = "KEEP_GENERATED_KID";
+
+    private String getClientSignedJWT(String clientId, KeyPair keyPair, final String kid) {
+        String realmInfoUrl = KeycloakUriBuilder.fromUri(getAuthServerRoot()).path(ServiceUrlConstants.REALM_INFO_PATH).build(REALM_NAME).toString();
+
+        // Use token-endpoint as audience as OIDC conformance testsuite is using it too.
+        JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider() {
+
+            @Override
+            public String createSignedRequestToken(String clientId, String realmInfoUrl) {
+                if (KEEP_GENERATED_KID.equals(kid)) {
+                    return super.createSignedRequestToken(clientId, realmInfoUrl);
+                } else {
+                    JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
+                    return new JWSBuilder()
+                            .kid(kid)
+                            .jsonContent(jwt)
+                            .rsa256(keyPair.getPrivate());
+                }
+            }
+
+            @Override
+            protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
+                JsonWebToken jwt = super.createRequestToken(clientId, realmInfoUrl);
+                String tokenEndpointUrl = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(getAuthServerRoot())).build(REALM_NAME).toString();
+                jwt.audience(tokenEndpointUrl);
+                return jwt;
+            }
+
+        };
+        jwtProvider.setupKeyPair(keyPair);
+        jwtProvider.setTokenTimeout(10);
+        return jwtProvider.createSignedRequestToken(clientId, realmInfoUrl);
+
+    }
+
+
+    private OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
+        List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
+        parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
+
+        HttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
+        return new OAuthClient.AccessTokenResponse(response);
+    }
+
+
+    private HttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
+        CloseableHttpClient client = new DefaultHttpClient();
+        try {
+            HttpPost post = new HttpPost(requestUrl);
+            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+            post.setEntity(formEntity);
+            return client.execute(post);
+        } finally {
+            oauth.closeClient(client);
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java
new file mode 100644
index 0000000..667c85f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.apache.commons.io.IOUtils;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.authentication.AuthenticationFlow;
+import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
+import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.storage.user.UserCredentialAuthenticationProvider;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.ExecutionBuilder;
+import org.keycloak.testsuite.util.FlowBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for {@link org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticator}
+ *
+ * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
+ */
+public class ScriptAuthenticatorTest extends AbstractFlowTest {
+
+    UserRepresentation failUser;
+
+    UserRepresentation okayUser;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    private AuthenticationFlowRepresentation flow;
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+
+        failUser = UserBuilder.create()
+                .id("fail")
+                .username("fail")
+                .email("fail@test.com")
+                .enabled(true)
+                .password("password")
+                .build();
+
+        okayUser = UserBuilder.create()
+                .id("user")
+                .username("user")
+                .email("user@test.com")
+                .enabled(true)
+                .password("password")
+                .build();
+
+        RealmBuilder.edit(testRealm)
+                .user(failUser)
+                .user(okayUser);
+    }
+
+    @Before
+    public void configureFlows() throws Exception {
+
+        String scriptFlow = "scriptBrowser";
+
+        AuthenticationFlowRepresentation scriptBrowserFlow = FlowBuilder.create()
+                .alias(scriptFlow)
+                .description("dummy pass through registration")
+                .providerId("basic-flow")
+                .topLevel(true)
+                .builtIn(false)
+                .build();
+
+        String scriptAuth = "scriptAuth";
+
+        Response createFlowResponse = testRealm().flows().createFlow(scriptBrowserFlow);
+        Assert.assertEquals(201, createFlowResponse.getStatus());
+
+        RealmRepresentation realm = testRealm().toRepresentation();
+        realm.setBrowserFlow(scriptFlow);
+        realm.setDirectGrantFlow(scriptFlow);
+        testRealm().update(realm);
+
+        this.flow = findFlowByAlias(scriptFlow);
+
+        AuthenticationExecutionRepresentation usernamePasswordFormExecution = ExecutionBuilder.create()
+                .id("username password form")
+                .parentFlow(this.flow.getId())
+                .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
+                .authenticator(UsernamePasswordFormFactory.PROVIDER_ID)
+                .build();
+
+        AuthenticationExecutionRepresentation authScriptExecution = ExecutionBuilder.create()
+                .id(scriptAuth)
+                .parentFlow(this.flow.getId())
+                .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
+                .authenticator(ScriptBasedAuthenticatorFactory.PROVIDER_ID)
+                .build();
+
+        Response addExecutionResponse = testRealm().flows().addExecution(usernamePasswordFormExecution);
+        Assert.assertEquals(201, addExecutionResponse.getStatus());
+
+        addExecutionResponse = testRealm().flows().addExecution(authScriptExecution);
+        Assert.assertEquals(201, addExecutionResponse.getStatus());
+
+        Response newExecutionConfigResponse = testRealm().flows().newExecutionConfig(scriptAuth, createScriptAuthConfig(scriptAuth, "authenticator-example.js", "/scripts/authenticator-example.js", "simple script based authenticator"));
+        Assert.assertEquals(201, newExecutionConfigResponse.getStatus());
+    }
+
+    /**
+     * KEYCLOAK-3491
+     */
+    @Test
+    public void loginShouldWorkWithScriptAuthenticator() {
+
+        loginPage.open();
+
+        loginPage.login(okayUser.getUsername(), "password");
+
+        events.expectLogin().user(okayUser.getId()).detail(Details.USERNAME, okayUser.getUsername()).assertEvent();
+    }
+
+    /**
+     * KEYCLOAK-3491
+     */
+    @Test
+    public void loginShouldFailWithScriptAuthenticator() {
+
+        loginPage.open();
+
+        loginPage.login(failUser.getUsername(), "password");
+
+        events.expect(EventType.LOGIN_ERROR).user((String)null).error(Errors.USER_NOT_FOUND).assertEvent();
+    }
+
+    private AuthenticatorConfigRepresentation createScriptAuthConfig(String alias, String scriptName, String scriptCodePath, String scriptDescription) throws IOException {
+
+        AuthenticatorConfigRepresentation configRep = new AuthenticatorConfigRepresentation();
+
+        configRep.setAlias(alias);
+        configRep.getConfig().put("scriptCode", IOUtils.toString(getClass().getResourceAsStream(scriptCodePath)));
+        configRep.getConfig().put("scriptName", scriptName);
+        configRep.getConfig().put("scriptDescription", scriptDescription);
+
+        return configRep;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 7c47446..4a0da21 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -24,9 +24,12 @@ import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
 import org.junit.Assert;
 import org.junit.Test;
 import org.keycloak.adapters.HttpClientBuilder;
+import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.pages.LoginPage;
 
 import javax.ws.rs.core.Response;
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.util.IdentityProviderBuilder;
 
 /**
  * @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
@@ -37,6 +40,24 @@ public class LoginPageTest extends AbstractI18NTest {
     @Page
     protected LoginPage loginPage;
 
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                .providerId("github")
+                .alias("github")
+                .build());
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                .providerId("saml")
+                .alias("mysaml")
+                .build());
+        testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+                .providerId("oidc")
+                .alias("myoidc")
+                .displayName("MyOIDC")
+                .build());
+
+    }
+
     @Test
     public void languageDropdown() {
         loginPage.open();
@@ -87,4 +108,13 @@ public class LoginPageTest extends AbstractI18NTest {
         response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
         Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
     }
+
+    @Test
+    public void testIdentityProviderCapitalization(){
+        loginPage.open();
+        Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
+        Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText());
+        Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
+
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 300d08d..eba2baf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -38,6 +38,7 @@ import org.keycloak.adapters.AdapterUtils;
 import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
 import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
 import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authentication.AuthenticationFlowError;
 import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.BouncyIntegration;
@@ -76,6 +77,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.file.Files;
+import java.security.KeyPair;
 import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.PublicKey;
@@ -318,18 +320,21 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
         keyStoreIs.close();
 
         client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
+        X509Certificate x509Cert = (X509Certificate) keyStore.getCertificate(keyAlias);
 
         assertCertificate(client, certOld,
-                KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias)));
+                KeycloakModelUtils.getPemFromCertificate(x509Cert));
 
 
         // Try to login with the new keys
 
         oauth.clientId(client.getClientId());
         PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
+        KeyPair keyPair = new KeyPair(x509Cert.getPublicKey(), privateKey);
+
         OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(),
                                                       user.getCredentials().get(0).getValue(),
-                                                        getClientSignedJWT(privateKey, client.getClientId()));
+                                                        getClientSignedJWT(keyPair, client.getClientId()));
 
         assertEquals(200, response.getStatusCode());
 
@@ -479,7 +484,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
 
     @Test
     public void testAssertionMissingIssuer() throws Exception {
-        String invalidJwt = getClientSignedJWT(getClient1PrivateKey(), null);
+        String invalidJwt = getClientSignedJWT(getClient1KeyPair(), null);
 
         List<NameValuePair> parameters = new LinkedList<NameValuePair>();
         parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -494,7 +499,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
 
     @Test
     public void testAssertionUnknownClient() throws Exception {
-        String invalidJwt = getClientSignedJWT(getClient1PrivateKey(), "unknown-client");
+        String invalidJwt = getClientSignedJWT(getClient1KeyPair(), "unknown-client");
 
         List<NameValuePair> parameters = new LinkedList<NameValuePair>();
         parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -559,7 +564,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
     @Test
     public void testAssertionInvalidSignature() throws Exception {
         // JWT for client1, but signed by privateKey of client2
-        String invalidJwt = getClientSignedJWT(getClient2PrivateKey(), "client1");
+        String invalidJwt = getClientSignedJWT(getClient2KeyPair(), "client1");
 
         List<NameValuePair> parameters = new LinkedList<NameValuePair>();
         parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
@@ -569,7 +574,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
         HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
         OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
 
-        assertError(response, "client1", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
+        assertError(response, "client1", "unauthorized_client", AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED.toString().toLowerCase());
     }
 
 
@@ -669,7 +674,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
 
     private OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception {
         CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider();
-        jwtProvider.setPrivateKey(getClient1PrivateKey());
+        jwtProvider.setupKeyPair(getClient1KeyPair());
         jwtProvider.setTokenTimeout(10);
 
         for (String claim : claims) {
@@ -788,26 +793,26 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
     }
 
     private String getClient1SignedJWT() {
-        return getClientSignedJWT(getClient1PrivateKey(), "client1");
+        return getClientSignedJWT(getClient1KeyPair(), "client1");
     }
 
     private String getClient2SignedJWT() {
-        return getClientSignedJWT(getClient2PrivateKey(), "client2");
+        return getClientSignedJWT(getClient2KeyPair(), "client2");
     }
 
-    private PrivateKey getClient1PrivateKey() {
-        return KeystoreUtil.loadPrivateKeyFromKeystore("classpath:client-auth-test/keystore-client1.jks",
+    private KeyPair getClient1KeyPair() {
+        return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client1.jks",
                 "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
     }
 
-    private PrivateKey getClient2PrivateKey() {
-        return KeystoreUtil.loadPrivateKeyFromKeystore("classpath:client-auth-test/keystore-client2.jks",
+    private KeyPair getClient2KeyPair() {
+        return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks",
                 "storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
     }
 
-    private String getClientSignedJWT(PrivateKey privateKey, String clientId) {
+    private String getClientSignedJWT(KeyPair keyPair, String clientId) {
         JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
-        jwtProvider.setPrivateKey(privateKey);
+        jwtProvider.setupKeyPair(keyPair);
         jwtProvider.setTokenTimeout(10);
         return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index 560f597..d551667 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -406,6 +406,9 @@ public class OIDCAdvancedRequestParamsTest extends TestRealmKeycloakTest {
         CertificateInfoHelper.updateClientRepresentationCertificateInfo(clientRep, cert, JWTClientAuthenticator.ATTR_PREFIX);
         clientResource.update(clientRep);
 
+        // set time offset, so that new keys are downloaded
+        setTimeOffset(20);
+
         // Check signed request_uri will pass
         OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
         Assert.assertNotNull(response.getCode());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
index bc3dae6..7cb1446 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
@@ -44,6 +44,11 @@ public class IdentityProviderBuilder {
         return this;
     }
 
+    public IdentityProviderBuilder displayName(String displayName) {
+        rep.setDisplayName(displayName);
+        return this;
+    }
+
     public IdentityProviderRepresentation build() {
         return rep;
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
index ce33acf..6a891c3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
@@ -62,4 +62,5 @@ log4j.logger.org.jboss.resteasy=warn
 log4j.logger.org.apache.directory.api=warn
 log4j.logger.org.apache.directory.server.core=warn
 
-log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
\ No newline at end of file
+# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
+# log4j.logger.org.keycloak.keys.infinispan=trace
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js
new file mode 100644
index 0000000..0fc10a4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js
@@ -0,0 +1,10 @@
+AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+
+function authenticate(context) {
+    LOG.info(script.name + " --> trace auth for: " + user.username);
+    if (user.username === "fail") {
+        context.failure(AuthenticationFlowError.INVALID_USER);
+        return;
+    }
+    context.success();
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java
index 3f17261..8e98baa 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java
@@ -200,7 +200,6 @@ public class SecurityDefensesTest extends AbstractRealmTest {
 
         wait *= 1000;
 
-        log.info("Wait: " + wait);
         pause(wait);
 
         if (finalLogin) {
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml
new file mode 100644
index 0000000..3acadaf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-tests-other</artifactId>
+        <version>2.2.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>integration-arquillian-tests-nodejs-adapter</artifactId>
+
+    <name>Node.js adapter tests</name>
+
+    <properties>
+        <testsuite.adapter.nodejs.example.url>http://localhost:3000/</testsuite.adapter.nodejs.example.url>
+        <auth.server.port.offset>0</auth.server.port.offset>
+        <auth.server.http.port>8080</auth.server.http.port>
+        <auth.server.https.port>8443</auth.server.https.port>
+        <auth.server.management.port>9990</auth.server.management.port>
+        <auth.server.management.port.jmx>9999</auth.server.management.port.jmx>
+    </properties>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <configuration>
+                        <systemProperties>
+                            <testsuite.adapter.nodejs.example.url>${testsuite.adapter.nodejs.example.url}</testsuite.adapter.nodejs.example.url>
+                        </systemProperties>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/AbstractNodejsExamplePage.java b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/AbstractNodejsExamplePage.java
new file mode 100644
index 0000000..9dde9d4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/AbstractNodejsExamplePage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.nodejs.page;
+
+import org.keycloak.testsuite.page.AbstractPage;
+
+import javax.ws.rs.core.UriBuilder;
+
+/**
+ * @author Vaclav Muzikar <vmuzikar@redhat.com>
+ */
+public abstract class AbstractNodejsExamplePage extends AbstractPage {
+    private static final String EXAMPLE_URL_PROPERY_NAME = "testsuite.adapter.nodejs.example.url";
+
+    @Override
+    public UriBuilder createUriBuilder() {
+        return UriBuilder.fromUri(System.getProperty(EXAMPLE_URL_PROPERY_NAME));
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/NodejsExamplePage.java b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/NodejsExamplePage.java
new file mode 100644
index 0000000..0c223f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/main/java/org/keycloak/testsuite/adapter/nodejs/page/NodejsExamplePage.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.nodejs.page;
+
+import org.keycloak.testsuite.util.URLUtils;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+import javax.ws.rs.core.UriBuilder;
+
+/**
+ * @author Vaclav Muzikar <vmuzikar@redhat.com>
+ */
+public class NodejsExamplePage extends AbstractNodejsExamplePage {
+    @FindBy(xpath = "//div[@class='nav']//a[text()='Login']")
+    private WebElement loginButton;
+
+    @FindBy(xpath = "//div[@class='nav']//a[text()='Logout']")
+    private WebElement logoutButton;
+
+    @FindBy(id = "output")
+    private WebElement outputBox;
+
+    public void clickLogin() {
+        loginButton.click();
+    }
+
+    public void clickLogout() {
+        logoutButton.click();
+    }
+
+    public String getOutput() {
+        return outputBox.getText();
+    }
+
+    public boolean isOnLoginSecuredPage() {
+        UriBuilder uriBuilder = createUriBuilder().path("login");
+        return URLUtils.currentUrlEqual(driver, uriBuilder.build().toASCIIString());
+    }
+
+    @Override
+    public boolean isCurrent() {
+        return URLUtils.currentUrlStartWith(driver, toString());
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java
new file mode 100644
index 0000000..1a1fa14
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/nodejs_adapter/src/test/java/org/keycloak/testsuite/adapter/nodejs/NodejsAdapterTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.nodejs;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractAuthTest;
+import org.keycloak.testsuite.adapter.nodejs.page.NodejsExamplePage;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+/**
+ * This test class expects following:
+ * <ul>
+ *     <li>The {@value #EXAMPLE_REALM_PROPERTY_NAME} System Property to be set and pointing to a realm configuration file
+ *     of the example app from keycloak-nodejs-connect project</li>
+ *     <li>The stated example app to be running</li>
+ * </ul>
+ *
+ * @author Vaclav Muzikar <vmuzikar@redhat.com>
+ */
+public class NodejsAdapterTest extends AbstractAuthTest {
+    private static final String EXAMPLE_REALM_PROPERTY_NAME = "testsuite.adapter.nodejs.example.realm";
+    private static RealmRepresentation exampleRealm;
+
+    private static final String USER = "user";
+    private static final String PASSWORD = "password";
+
+    @Page
+    private NodejsExamplePage nodejsExamplePage;
+
+    @BeforeClass
+    public static void beforeNodejsAdapterTestClass() {
+        String exampleRealmPath = System.getProperty(EXAMPLE_REALM_PROPERTY_NAME);
+        if (exampleRealmPath == null) {
+            throw new IllegalStateException(EXAMPLE_REALM_PROPERTY_NAME + " property must be set");
+        }
+
+        exampleRealm = loadRealm(new File(exampleRealmPath));
+    }
+
+    @Before
+    public void beforeNodejsAdapterTest() {
+        nodejsExamplePage.navigateTo();
+        driver.manage().deleteAllCookies();
+    }
+
+    @Override
+    public void setDefaultPageUriParameters() {
+        super.setDefaultPageUriParameters();
+        testRealmPage.setAuthRealm(exampleRealm.getRealm());
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(exampleRealm);
+    }
+
+    @Test
+    public void simpleLoginTest() {
+        nodejsExamplePage.navigateTo();
+        assertCurrentUrlEquals(nodejsExamplePage);
+
+        nodejsExamplePage.clickLogin();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage);
+        testRealmLoginPage.form().login(USER, PASSWORD);
+
+        assertTrue("Should be redirected back to the secured page after the login", nodejsExamplePage.isOnLoginSecuredPage());
+        String output = nodejsExamplePage.getOutput();
+        assertFalse("Output should not be empty", output.isEmpty());
+
+        nodejsExamplePage.clickLogin();
+        assertTrue("Should be already logged in", nodejsExamplePage.isOnLoginSecuredPage());
+        assertEquals("Authentication responses should be the same", output, nodejsExamplePage.getOutput());
+
+        nodejsExamplePage.clickLogout();
+        assertCurrentUrlEquals(nodejsExamplePage);
+
+        nodejsExamplePage.clickLogin();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage);
+    }
+
+    @Test
+    public void timeoutsTest() {
+        // in seconds
+        final int ssoTimeout = 15;
+        final int tokenTimeout = 10;
+
+        // change the realm's timeouts
+        RealmRepresentation realmUpdate = new RealmRepresentation();
+        realmUpdate.setSsoSessionIdleTimeout(ssoTimeout);
+        realmUpdate.setAccessTokenLifespan(tokenTimeout);
+        adminClient.realm(exampleRealm.getRealm()).update(realmUpdate);
+
+
+
+        // test access token lifespan
+        nodejsExamplePage.clickLogin();
+        testRealmLoginPage.form().login(USER, PASSWORD);
+        assertTrue("Should be logged in", nodejsExamplePage.isOnLoginSecuredPage());
+        String output = nodejsExamplePage.getOutput();
+
+        pause(tokenTimeout * 1000);
+
+        nodejsExamplePage.clickLogin();
+        assertTrue("Should be still logged in", nodejsExamplePage.isOnLoginSecuredPage());
+        assertNotEquals("Authentication responses should be different", output, nodejsExamplePage.getOutput()); // token should be refreshed
+
+
+
+        // test SSO timeout
+        pause(ssoTimeout * 1000);
+        nodejsExamplePage.clickLogin();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage);   // there should be an attempt for token refresh
+                                                                    // but SSO session should be already expired
+    }
+
+    // KEYCLOAK-3284
+    @Test
+    @Ignore // to be enabled when KEYCLOAK-3284 is fixed
+    public void sessionTest() {
+        nodejsExamplePage.clickLogin();
+        testRealmLoginPage.form().login(USER, PASSWORD);
+        assertTrue("Should be logged in", nodejsExamplePage.isOnLoginSecuredPage());
+
+        testRealmAccountPage.navigateTo();
+        assertCurrentUrlEquals(testRealmAccountPage); // should be already logged in
+
+        testRealmAccountPage.logOut();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage);
+
+        nodejsExamplePage.navigateTo();
+        nodejsExamplePage.clickLogin();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage); // should be logged out
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/pom.xml b/testsuite/integration-arquillian/tests/other/pom.xml
index 0ce2a25..be2cab0 100644
--- a/testsuite/integration-arquillian/tests/other/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/pom.xml
@@ -142,6 +142,12 @@
                 <module>jpa-performance</module>
             </modules>
         </profile>
+        <profile>
+            <id>nodejs-adapter-tests</id>
+            <modules>
+                <module>nodejs_adapter</module>
+            </modules>
+        </profile>
     </profiles>
 
 </project>
diff --git a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
index 81ffa98..b11d5e4 100644
--- a/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
+++ b/testsuite/integration-arquillian/tests/other/sssd/src/test/java/org/keycloak/testsuite/sssd/SSSDTest.java
@@ -6,6 +6,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.representations.idm.GroupRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation;
 import org.keycloak.representations.idm.UserFederationProviderRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
@@ -72,6 +73,12 @@ public class SSSDTest extends AbstractKeycloakTest {
     }
 
     @Test
+    public void testProviderFactories() {
+        List<UserFederationProviderFactoryRepresentation> providerFactories = adminClient.realm(REALM_NAME).userFederation().getProviderFactories();
+        Assert.assertNames(providerFactories, "ldap", "kerberos", "dummy", "dummy-configurable", "sssd");
+    }
+
+    @Test
     public void testWrongUser() {
         log.debug("Testing wrong password for user " + USERNAME);
 
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index ee9f362..1c2ba57 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -532,6 +532,12 @@
                     <version>1.0.13</version>
                 </dependency>
 
+                <dependency>
+                    <groupId>org.keycloak.testsuite</groupId>
+                    <artifactId>integration-arquillian-test-apps-servlets</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+
                 <!--                <dependency>
                     <groupId>org.arquillian.extension</groupId>
                     <artifactId>arquillian-recorder-reporter-impl</artifactId>
@@ -552,12 +558,6 @@
                 </dependency>
 
                 <dependency>
-                    <groupId>javax.servlet</groupId>
-                    <artifactId>javax.servlet-api</artifactId>
-                    <version>3.1.0</version>
-                </dependency>
-
-                <dependency>
                     <groupId>org.apache.ant</groupId>
                     <artifactId>ant</artifactId>
                     <version>1.9.2</version>
@@ -602,10 +602,6 @@
 
                 <dependency>
                     <groupId>org.keycloak</groupId>
-                    <artifactId>keycloak-core</artifactId>
-                </dependency>
-                <dependency>
-                    <groupId>org.keycloak</groupId>
                     <artifactId>keycloak-admin-client</artifactId>
                 </dependency>
                 <dependency>
@@ -618,18 +614,6 @@
                 </dependency>
                 <dependency>
                     <groupId>org.keycloak</groupId>
-                    <artifactId>keycloak-adapter-core</artifactId>
-                </dependency>
-                <dependency>
-                    <groupId>org.keycloak</groupId>
-                    <artifactId>keycloak-adapter-spi</artifactId>
-                </dependency>
-                <dependency>
-                    <groupId>org.keycloak</groupId>
-                    <artifactId>keycloak-saml-adapter-api-public</artifactId>
-                </dependency>
-                <dependency>
-                    <groupId>org.keycloak</groupId>
                     <artifactId>keycloak-authz-client</artifactId>
                 </dependency>
 
@@ -648,33 +632,11 @@
                 </dependency>
 
                 <dependency>
-                    <groupId>org.jboss.spec.javax.ws.rs</groupId>
-                    <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
-                </dependency>
-                <dependency>
                     <groupId>org.jboss.resteasy</groupId>
                     <artifactId>async-http-servlet-3.0</artifactId>
                 </dependency>
                 <dependency>
                     <groupId>org.jboss.resteasy</groupId>
-                    <artifactId>resteasy-jaxrs</artifactId>
-                    <exclusions>
-                        <exclusion>
-                            <groupId>log4j</groupId>
-                            <artifactId>log4j</artifactId>
-                        </exclusion>
-                        <exclusion>
-                            <groupId>org.slf4j</groupId>
-                            <artifactId>slf4j-api</artifactId>
-                        </exclusion>
-                        <exclusion>
-                            <groupId>org.slf4j</groupId>
-                            <artifactId>slf4j-simple</artifactId>
-                        </exclusion>
-                    </exclusions>
-                </dependency>
-                <dependency>
-                    <groupId>org.jboss.resteasy</groupId>
                     <artifactId>resteasy-client</artifactId>
                 </dependency>
                 <dependency>
diff --git a/themes/src/main/resources/theme/base/account/federatedIdentity.ftl b/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
index 3a18805..9a90173 100755
--- a/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
+++ b/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
@@ -11,7 +11,7 @@
         <#list federatedIdentity.identities as identity>
             <div class="form-group">
                 <div class="col-sm-2 col-md-2">
-                    <label for="${identity.providerId!}" class="control-label">${identity.providerName!}</label>
+                    <label for="${identity.providerId!}" class="control-label">${identity.displayName!}</label>
                 </div>
                 <div class="col-sm-5 col-md-5">
                     <input disabled="true" class="form-control" value="${identity.userName!}">
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index d3cdd9e..e9f3449 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -293,6 +293,12 @@ gen-new-keys-and-cert=Generate new keys and certificate
 import-certificate=Import Certificate
 gen-client-private-key=Generate Client Private Key
 generate-private-key=Generate Private Key
+kid=Kid
+kid.tooltip=KID (Key ID) of the client public key from imported JWKS.
+use-jwks-url=Use JWKS URL
+use-jwks-url.tooltip=If the switch is on, then client public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when client generates new keypair. If the switch is off, then public key (or certificate) from the Keycloak DB is used, so when client keypair changes, you always need to import new key (or certificate) to the Keycloak DB as well.
+jwks-url=JWKS URL
+jwks-url.tooltip=URL where client keys in JWK format are stored. See JWK specification for more details. If you use keycloak client adapter with "jwt" credential, then you can use URL of your app with '/k_jwks' suffix. For example 'http://www.myhost.com/myapp/k_jwks' .
 archive-format=Archive Format
 archive-format.tooltip=Java keystore or PKCS12 archive format.
 key-alias=Key Alias
@@ -418,7 +424,9 @@ post-broker-login-flow=Post Login Flow
 redirect-uri=Redirect URI
 redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
 alias=Alias
+display-name=Display Name
 identity-provider.alias.tooltip=The alias uniquely identifies an identity provider and it is also used to build the redirect uri.
+identity-provider.display-name.tooltip=Friendly name for Identity Providers.
 identity-provider.enabled.tooltip=Enable/disable this identity provider.
 authenticate-by-default=Authenticate by Default
 identity-provider.authenticate-by-default.tooltip=Indicates if this provider should be tried by default for authentication even before displaying login screen.
@@ -466,6 +474,8 @@ select-account.option=select_account
 prompt.tooltip=Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
 validate-signatures=Validate Signatures
 identity-provider.validate-signatures.tooltip=Enable/disable signature validation of external IDP signatures.
+identity-provider.use-jwks-url.tooltip=If the switch is on, then identity provider public keys  will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when identity provider generates new keypair. If the switch is off, then public key (or certificate) from the Keycloak DB is used, so when identity provider keypair changes, you always need to import new key to the Keycloak DB as well.
+identity-provider.jwks-url.tooltip=URL where identity provider keys in JWK format are stored. See JWK specification for more details. If you use external keycloak identity provider, then you can use URL like 'http://broker-keycloak:8180/auth/realms/test/protocol/openid-connect/certs' assuming your brokered keycloak is running on 'http://broker-keycloak:8180' and it's realm is 'test' .
 validating-public-key=Validating Public Key
 identity-provider.validating-public-key.tooltip=The public key in PEM format that must be used to verify external IDP signatures.
 import-external-idp-config=Import External IDP Config
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index f46f2d8..4b33b14 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -124,13 +124,54 @@ module.controller('ClientSecretCtrl', function($scope, $location, ClientSecret, 
     };
 });
 
-module.controller('ClientSignedJWTCtrl', function($scope, $location, ClientCertificate) {
+module.controller('ClientSignedJWTCtrl', function($scope, $location, Client, ClientCertificate, Notifications, $route) {
     var signingKeyInfo = ClientCertificate.get({ realm : $scope.realm.realm, client : $scope.client.id, attribute: 'jwt.credential' },
         function() {
             $scope.signingKeyInfo = signingKeyInfo;
         }
     );
 
+    console.log('ClientSignedJWTCtrl invoked');
+
+    $scope.clientCopy = angular.copy($scope.client);
+    $scope.changed = false;
+
+    $scope.$watch('client', function() {
+        if (!angular.equals($scope.client, $scope.clientCopy)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    if ($scope.client.attributes["use.jwks.url"]) {
+        if ($scope.client.attributes["use.jwks.url"] == "true") {
+            $scope.useJwksUrl = true;
+        } else {
+            $scope.useJwksUrl = false;
+        }
+    }
+
+    $scope.switchChange = function() {
+        $scope.changed = true;
+    }
+
+    $scope.save = function() {
+
+        if ($scope.useJwksUrl == true) {
+            $scope.client.attributes["use.jwks.url"] = "true";
+        } else {
+            $scope.client.attributes["use.jwks.url"] = "false";
+        }
+
+        Client.update({
+            realm : $scope.realm.realm,
+            client : $scope.client.id
+        }, $scope.client, function() {
+            $scope.changed = false;
+            $scope.clientCopy = angular.copy($scope.client);
+            Notifications.success("Client authentication configuration has been saved to the client.");
+        });
+    };
+
     $scope.importCertificate = function() {
         $location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/import/jwt.credential");
     };
@@ -139,8 +180,8 @@ module.controller('ClientSignedJWTCtrl', function($scope, $location, ClientCerti
         $location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials/client-jwt/Signing/export/jwt.credential");
     };
 
-    $scope.cancel = function() {
-        $location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials");
+    $scope.reset = function() {
+        $route.reload();
     };
 });
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
old mode 100755
new mode 100644
index 5db9cf0..bce6b9d
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -768,14 +768,23 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
     if (instance && instance.alias) {
         $scope.identityProvider = angular.copy(instance);
         $scope.newIdentityProvider = false;
+        for (var i in serverInfo.identityProviders) {
+            var provider = serverInfo.identityProviders[i];
+
+            if (provider.id == instance.providerId) {
+                $scope.provider = provider;
+            }
+        }
     } else {
         $scope.identityProvider = {};
         $scope.identityProvider.config = {};
         $scope.identityProvider.alias = providerFactory.id;
         $scope.identityProvider.providerId = providerFactory.id;
+
         $scope.identityProvider.enabled = true;
         $scope.identityProvider.authenticateByDefault = false;
         $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login';
+        $scope.identityProvider.config.useJwksUrl = 'true';
         $scope.newIdentityProvider = true;
     }
 
@@ -836,7 +845,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
         }
     }
 
-
     $scope.uploadFile = function() {
         if (!$scope.identityProvider.alias) {
             Notifications.error("You must specify an alias");
@@ -906,13 +914,12 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
 
                 for (var i in $scope.allProviders) {
                     var provider = $scope.allProviders[i];
-
-                    if (provider.groupName == 'Social' && (provider.id == configProvidedId)) {
-                        $scope.allProviders.splice(i, 1);
-                        break;
+                    if (provider.id == configProvidedId) {
+                        configuredProviders[j].provider = provider;
                     }
                 }
             }
+            $scope.configuredProviders = angular.copy(configuredProviders);
         }
     }, true);
 
@@ -2132,9 +2139,20 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
     $scope.realm = realm;
     $scope.flow = flow;
     $scope.create = true;
-    $scope.config = { config: {}};
     $scope.configType = configType;
 
+    var defaultConfig = {};
+    if (configType && Array.isArray(configType.properties)) {
+        for(var i = 0; i < configType.properties.length; i++) {
+            var property = configType.properties[i];
+            if (property && property.name) {
+                defaultConfig[property.name] = property.defaultValue;
+            }
+        }
+    }
+
+    $scope.config = { config: defaultConfig};
+
     $scope.$watch(function() {
         return $location.path();
     }, function() {
@@ -2159,8 +2177,6 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
         //$location.url("/realms");
         window.history.back();
     };
-
-
 });
 
 module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
index 0cb1999..29f6524 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-credentials-jwt.html
@@ -1,34 +1,74 @@
-<div>
-    <form class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
-        <div class="form-group">
-
-            <div data-ng-show="signingKeyInfo.certificate">
-                <label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
-                <kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
-
-                <div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
-                    <textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
-                </div>
-            </div>
-
-            <div data-ng-show="signingKeyInfo.publicKey">
-                <label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
-                <kc-tooltip>{{:: 'publicKey.tooltip' | translate}}</kc-tooltip>
-
-                <div class="col-sm-10" data-ng-show="signingKeyInfo.publicKey">
-                    <textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.publicKey}}</textarea>
-                </div>
-            </div>
-
-            <div class="col-sm-10" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
-                {{:: 'no-client-certificate-configured' | translate}}
-            </div>
-        </div>
-        <div class="form-group">
-            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
-                <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">{{:: 'gen-new-keys-and-cert' | translate}}</button>
-                <button class="btn btn-default" type="submit" data-ng-click="importCertificate()">{{:: 'import-certificate' | translate}}</button>
-            </div>
-        </div>
-    </form>
+ <div class="form-horizontal no-margin-top" name="keyForm" novalidate kc-read-only="!access.manageClients" data-ng-controller="ClientSignedJWTCtrl">
+
+     <div class="form-group">
+         <label class="col-md-2 control-label" for="useJwksUrl">{{:: 'use-jwks-url' | translate}}</label>
+         <div class="col-sm-6">
+             <input ng-model="useJwksUrl" name="useJwksUrl" id="useJwksUrl" ng-click="switchChange()" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+         </div>
+         <kc-tooltip>{{:: 'use-jwks-url.tooltip' | translate}}</kc-tooltip>
+     </div>
+
+     <div class="form-group" data-ng-show="useJwksUrl">
+         <label class="col-md-2 control-label" for="jwksUrl">{{:: 'jwks-url' | translate}}</label>
+         <div class="col-sm-6">
+             <input class="form-control" type="text" name="jwksUrl" id="jwksUrl" data-ng-model="client.attributes['jwks.url']">
+         </div>
+         <kc-tooltip>{{:: 'jwks-url.tooltip' | translate}}</kc-tooltip>
+     </div>
+
+     <div data-ng-show="!useJwksUrl">
+
+         <div class="form-group" data-ng-show="signingKeyInfo.certificate">
+             <label class="col-md-2 control-label" for="signingCert">{{:: 'certificate' | translate}}</label>
+             <kc-tooltip>{{:: 'certificate.tooltip' | translate}}</kc-tooltip>
+
+             <div class="col-sm-10" data-ng-show="signingKeyInfo.certificate">
+                 <textarea type="text" id="signingCert" name="signingCert" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.certificate}}</textarea>
+             </div>
+         </div>
+
+         <div class="form-group" data-ng-show="signingKeyInfo.publicKey">
+             <label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
+             <kc-tooltip>{{:: 'publicKey.tooltip' | translate}}</kc-tooltip>
+
+             <div class="col-sm-10" data-ng-show="signingKeyInfo.publicKey">
+                 <textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="5" kc-select-action="click" readonly>{{signingKeyInfo.publicKey}}</textarea>
+             </div>
+         </div>
+
+         <div class="form-group" data-ng-show="signingKeyInfo.kid">
+             <label class="col-md-2  control-label" for="kid">{{:: 'kid' | translate}}</label>
+             <kc-tooltip>{{:: 'kid.tooltip' | translate}}</kc-tooltip>
+
+             <div class="col-sm-6">
+                 <div class="row">
+                     <div class="col-sm-6">
+                         <input readonly kc-select-action="click" class="form-control" type="text" id="kid" name="kid" data-ng-model="signingKeyInfo.kid">
+                     </div>
+                 </div>
+             </div>
+         </div>
+
+         <div class="form-group" data-ng-hide="signingKeyInfo.certificate || signingKeyInfo.publicKey">
+             <label class="col-md-2 control-label"></label>
+             <div class="col-sm-6">
+                 <div class="row">
+                     <div class="col-sm-6">
+                         {{:: 'no-client-certificate-configured' | translate}}
+                     </div>
+                 </div>
+             </div>
+         </div>
+
+     </div>
+
+     <div class="form-group">
+         <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+             <button class="btn btn-default" type="submit" data-ng-click="generateSigningKey()">{{:: 'gen-new-keys-and-cert' | translate}}</button>
+             <button data-ng-disabled="useJwksUrl" class="btn btn-default" type="submit" data-ng-click="importCertificate()">{{:: 'import-certificate' | translate}}</button>
+             <button kc-save  data-ng-disabled="!changed" data-ng-click="save()">{{:: 'save' | translate}}</button>
+             <button kc-reset data-ng-disabled="!changed" data-ng-click="reset()">{{:: 'cancel' | translate}}</button>
+         </div>
+     </div>
+
 </div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
index cd2964d..a7c54f8 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html
@@ -1,7 +1,7 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.displayName}}</a></li>
         <li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">{{:: 'identity-provider-mappers' | translate}}</a></li>
         <li class="active" data-ng-show="create">{{:: 'create-identity-provider-mapper' | translate}}</li>
         <li class="active" data-ng-hide="create">{{mapper.name|capitalize}}</li>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
index a2f7eff..89bea67 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mappers.html
@@ -1,7 +1,7 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li>{{identityProvider.alias}}</li>
+        <li>{{identityProvider.displayName}}</li>
     </ol>
 
     <kc-tabs-identity-provider></kc-tabs-identity-provider>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
index 2f5ed40..0de7894 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider.html
@@ -54,7 +54,11 @@
                     <tbody>
                     <tr ng-repeat="identityProvider in configuredProviders">
                         <td>
-                            <a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}}</a>
+                            <a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">
+                                <span data-ng-show="identityProvider.displayName">{{identityProvider.displayName}}</span>
+                                <span data-ng-show="!identityProvider.displayName && identityProvider.provider.groupName == 'Social'">{{identityProvider.provider.name}}</span>
+                                <span data-ng-show="!identityProvider.displayName && identityProvider.provider.groupName != 'Social'">{{identityProvider.alias}}</span>
+                            </a>
                         </td>
                         <td>{{identityProvider.providerId}}</td>
                         <td translate="{{identityProvider.enabled}}"></td>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
index 3e483f7..78645d2 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-export.html
@@ -1,7 +1,7 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2" data-ng-init="initProvider()">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li>{{identityProvider.alias}}</li>
+        <li>{{identityProvider.displayName}}</li>
     </ol>
 
     <kc-tabs-identity-provider></kc-tabs-identity-provider>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index bbbf9b9..80b4074 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -1,7 +1,9 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li>{{identityProvider.alias}}</li>
+        <li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
+        <li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
+        <li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
     </ol>
 
     <kc-tabs-identity-provider></kc-tabs-identity-provider>
@@ -27,6 +29,13 @@
                 </div>
                 <kc-tooltip>{{:: 'identity-provider.alias.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="displayName"> {{:: 'display-name' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="displayName" type="text" ng-model="identityProvider.displayName">
+                </div>
+                <kc-tooltip>{{:: 'identity-provider.display-name.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group">
                 <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
                 <div class="col-md-6">
@@ -175,13 +184,35 @@
                 </div>
                 <kc-tooltip>{{:: 'identity-provider.validate-signatures.tooltip' | translate}}</kc-tooltip>
             </div>
-            <div class="form-group clearfix" data-ng-show="identityProvider.config.validateSignature == 'true'">
-                <label class="col-md-2 control-label" for="publicKeySignatureVerifier">{{:: 'validating-public-key' | translate}}</label>
-                <div class="col-md-6">
-                    <textarea class="form-control" id="publicKeySignatureVerifier" ng-model="identityProvider.config.publicKeySignatureVerifier"/>
+
+            <div data-ng-show="identityProvider.config.validateSignature == 'true'">
+
+                <div class="form-group">
+                    <label class="col-md-2 control-label" for="useJwksUrl">{{:: 'use-jwks-url' | translate}}</label>
+                    <div class="col-md-6">
+                        <input ng-model="identityProvider.config.useJwksUrl" id="useJwksUrl" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                    </div>
+                    <kc-tooltip>{{:: 'identity-provider.use-jwks-url.tooltip' | translate}}</kc-tooltip>
                 </div>
-                <kc-tooltip>{{:: 'identity-provider.validating-public-key.tooltip' | translate}}</kc-tooltip>
+
+                <div class="form-group clearfix" data-ng-show="identityProvider.config.useJwksUrl == 'true'">
+                    <label class="col-md-2 control-label" for="jwksUrl">{{:: 'jwks-url' | translate}} </label>
+                    <div class="col-md-6">
+                        <input class="form-control" id="jwksUrl" type="text" ng-model="identityProvider.config.jwksUrl">
+                    </div>
+                    <kc-tooltip>{{:: 'identity-provider.jwks-url.tooltip' | translate}}</kc-tooltip>
+                </div>
+
+                <div class="form-group clearfix" data-ng-hide="identityProvider.config.useJwksUrl == 'true'">
+                    <label class="col-md-2 control-label" for="publicKeySignatureVerifier">{{:: 'validating-public-key' | translate}}</label>
+                    <div class="col-md-6">
+                        <textarea class="form-control" id="publicKeySignatureVerifier" ng-model="identityProvider.config.publicKeySignatureVerifier"/>
+                    </div>
+                    <kc-tooltip>{{:: 'identity-provider.validating-public-key.tooltip' | translate}}</kc-tooltip>
+                </div>
+
             </div>
+
         </fieldset>
         <fieldset data-ng-show="newIdentityProvider">
             <legend uncollapsed><span class="text">{{:: 'import-external-idp-config' | translate}}</span> <kc-tooltip>{{:: 'import-external-idp-config.tooltip' | translate}}</kc-tooltip></legend>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index eaf4439..0ed66c6 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -1,7 +1,9 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2" data-ng-init="initSamlProvider()">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li>{{identityProvider.alias}}</li>
+        <li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
+        <li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
+        <li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
     </ol>
 
     <kc-tabs-identity-provider></kc-tabs-identity-provider>
@@ -24,6 +26,13 @@
                 </div>
                 <kc-tooltip>{{:: 'identity-provider.alias.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="displayName"> {{:: 'display-name' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="displayName" type="text" ng-model="identityProvider.displayName">
+                </div>
+                <kc-tooltip>{{:: 'identity-provider.display-name.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group">
                 <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
                 <div class="col-md-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
index 6c7ceb7..2c51dd5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
@@ -1,7 +1,8 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
-        <li>{{identityProvider.alias}}</li>
+        <li data-ng-hide="newIdentityProvider">{{provider.name}}</li>
+        <li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
     </ol>
 
     <kc-tabs-identity-provider></kc-tabs-identity-provider>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
index 97d8876..57bbc06 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
@@ -33,7 +33,7 @@
         </div>
 
         <div class="col-md-6" data-ng-show="option.type == 'Script'">
-            <div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
+            <div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
                 {{config[option.name]}}
             </div>
         </div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
index 0195f10..279e598 100644
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
@@ -1,6 +1,9 @@
 <div data-ng-controller="IdentityProviderTabCtrl">
     <h1 data-ng-hide="path[0] == 'create'">
-        {{identityProvider.alias|capitalize}}
+        <span data-ng-show="identityProvider.displayName">{{identityProvider.displayName}}</span>
+        <span data-ng-show="!identityProvider.displayName && provider.groupName == 'Social'">{{provider.name}}</span>
+        <span data-ng-show="!identityProvider.displayName && provider.groupName != 'Social'">{{identityProvider.alias}}</span>
+
         <i class="pficon pficon-delete clickable" data-ng-hide="newIdentityProvider || changed" data-ng-click="removeIdentityProvider()"></i>
     </h1>
     <h1 data-ng-show="path[0] == 'create'">{{:: 'add-identity-provider' | translate}}</h1>
diff --git a/themes/src/main/resources/theme/base/login/login.ftl b/themes/src/main/resources/theme/base/login/login.ftl
index 3f70f0c..ab4b363 100755
--- a/themes/src/main/resources/theme/base/login/login.ftl
+++ b/themes/src/main/resources/theme/base/login/login.ftl
@@ -70,7 +70,7 @@
             <div id="kc-social-providers">
                 <ul>
                     <#list social.providers as p>
-                        <li><a href="${p.loginUrl}" id="zocial-${p.alias}" class="zocial ${p.providerId}"> <span class="text">${p.alias}</span></a></li>
+                        <li><a href="${p.loginUrl}" id="zocial-${p.alias}" class="zocial ${p.providerId}"> <span class="text">${p.displayName}</span></a></li>
                     </#list>
                 </ul>
             </div>
diff --git a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
index c3a9a64..9253c8c 100755
--- a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
+++ b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
@@ -377,6 +377,6 @@ h1 i {
 }
 
 .ace_editor {
-    height: 400px;
+    height: 600px;
     width: 100%;
 }
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
index 8d702d9..0ed51ac 100644
--- a/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
+++ b/themes/src/main/resources/theme/keycloak/login/resources/css/login.css
@@ -229,9 +229,6 @@ ol#kc-totp-settings li:first-of-type {
 .zocial.google {
     background-color: #dd4b39 !important;
 }
-.zocial.google .text:after {
-    content: "+";
-}
 
 .zocial.facebook:hover,
 .zocial.github:hover,
diff --git a/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
index e29d1bd..bb4b18c 100644
--- a/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
+++ b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
@@ -73,4 +73,11 @@ keycloak.server.subsys.default.config=\
         <default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>\
         <provider name="jboss" enabled="true"/>\
     </spi>\
+    <spi name="keyStorage">\
+        <provider name="infinispan" enabled="true">\
+              <properties>\
+                  <property name="minTimeBetweenRequests" value="10"/>\
+              </properties>\
+        </provider>\
+    </spi>\
 </subsystem>\
\ No newline at end of file
diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
index 598a7d5..af140a6 100755
--- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
+++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
@@ -101,7 +101,8 @@ public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcesso
             st.addDependency(cacheContainerService.append("offlineSessions"));
             st.addDependency(cacheContainerService.append("loginFailures"));
             st.addDependency(cacheContainerService.append("work"));
-            st.addDependency(cacheContainerService.append("authorization"));;
+            st.addDependency(cacheContainerService.append("authorization"));
+            st.addDependency(cacheContainerService.append("keys"));
         }
     }
 
diff --git a/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli b/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli
index 6adef01..cbfdd53 100644
--- a/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli
+++ b/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli
@@ -18,4 +18,6 @@
 /subsystem=keycloak-server/spi=connectionsInfinispan/:add(default-provider=default)
 /subsystem=keycloak-server/spi=connectionsInfinispan/provider=default/:add(properties={cacheContainer => "java:comp/env/infinispan/Keycloak"},enabled=true)
 /subsystem=keycloak-server/spi=jta-lookup/:add(default-provider=${keycloak.jta.lookup.provider:jboss})
-/subsystem=keycloak-server/spi=jta-lookup/provider=jboss/:add(enabled=true)
\ No newline at end of file
+/subsystem=keycloak-server/spi=jta-lookup/provider=jboss/:add(enabled=true)
+/subsystem=keycloak-server/spi=keyStorage/:add
+/subsystem=keycloak-server/spi=keyStorage/provider=infinispan/:add(properties={minTimeBetweenRequests => "10"},enabled=true)
\ No newline at end of file
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index bbc1a83..840fa2c 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -36,6 +36,10 @@
                 <local-cache name="authorization">
                     <eviction max-entries="100" strategy="LRU"/>
                 </local-cache>
+                <local-cache name="keys">
+                    <eviction max-entries="1000" strategy="LRU"/>
+                    <expiration max-idle="3600000" />
+                </local-cache>
             </cache-container>
             <cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
                 <local-cache name="default">
@@ -97,6 +101,10 @@
                 <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
                 <distributed-cache name="authorization" mode="SYNC" owners="1"/>
                 <replicated-cache name="work" mode="SYNC" />
+                <local-cache name="keys">
+                    <eviction max-entries="1000" strategy="LRU"/>
+                    <expiration max-idle="3600000" />
+                </local-cache>
             </cache-container>
             <cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
                 <transport lock-timeout="60000"/>