keycloak-aplcache

Changes

Details

diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js
index 70b318e..f0cae42 100755
--- a/adapters/oidc/js/src/main/resources/keycloak.js
+++ b/adapters/oidc/js/src/main/resources/keycloak.js
@@ -153,7 +153,7 @@
                     processCallback(callback, initPromise);
                     return;
                 } else if (initOptions) {
-                    if (initOptions.token || initOptions.refreshToken) {
+                    if (initOptions.refreshToken) {
                         setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
 
                         if (loginIframe.enable) {
@@ -379,7 +379,7 @@
         kc.updateToken = function(minValidity) {
             var promise = createPromise();
 
-            if (!kc.tokenParsed || !kc.refreshToken) {
+            if (!kc.refreshToken) {
                 promise.setError();
                 return promise.promise;
             }
@@ -388,14 +388,10 @@
 
             var exec = function() {
                 var refreshToken = false;
-                if (kc.timeSkew == -1) {
-                    console.info('Skew ' + kc.timeSkew);
-                    refreshToken = true;
-                    console.info('[KEYCLOAK] Refreshing token: time skew not set');
-                } else if (minValidity == -1) {
+                if (minValidity == -1) {
                     refreshToken = true;
                     console.info('[KEYCLOAK] Refreshing token: forced refresh');
-                } else if (kc.isTokenExpired(minValidity)) {
+                } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {
                     refreshToken = true;
                     console.info('[KEYCLOAK] Refreshing token: token expired');
                 }
@@ -638,9 +634,26 @@
                 kc.tokenTimeoutHandle = null;
             }
 
+            if (refreshToken) {
+                kc.refreshToken = refreshToken;
+                kc.refreshTokenParsed = decodeToken(refreshToken);
+            } else {
+                delete kc.refreshToken;
+                delete kc.refreshTokenParsed;
+            }
+
+            if (idToken) {
+                kc.idToken = idToken;
+                kc.idTokenParsed = decodeToken(idToken);
+            } else {
+                delete kc.idToken;
+                delete kc.idTokenParsed;
+            }
+
             if (token) {
                 kc.token = token;
                 kc.tokenParsed = decodeToken(token);
+
                 var sessionId = kc.realm + '/' + kc.tokenParsed.sub;
                 if (kc.tokenParsed.session_state) {
                     sessionId = sessionId + '/' + kc.tokenParsed.session_state;
@@ -654,23 +667,21 @@
                 if (timeLocal) {
                     kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
                     console.info('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');
-                } else {
-                    kc.timeSkew = -1;
-                }
 
-                if (kc.onTokenExpired) {
-                    if (kc.timeSkew == -1) {
-                        kc.onTokenExpired();
-                    } else {
+                    if (kc.onTokenExpired) {
                         var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
+                        console.info('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');
                         if (expiresIn <= 0) {
                             kc.onTokenExpired();
                         } else {
                             kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
                         }
                     }
+                } else {
+                    kc.updateToken(-1);
                 }
-
+            } else if (refreshToken) {
+                kc.updateToken(-1);
             } else {
                 delete kc.token;
                 delete kc.tokenParsed;
@@ -680,22 +691,6 @@
 
                 kc.authenticated = false;
             }
-
-            if (refreshToken) {
-                kc.refreshToken = refreshToken;
-                kc.refreshTokenParsed = decodeToken(refreshToken);
-            } else {
-                delete kc.refreshToken;
-                delete kc.refreshTokenParsed;
-            }
-
-            if (idToken) {
-                kc.idToken = idToken;
-                kc.idTokenParsed = decodeToken(idToken);
-            } else {
-                delete kc.idToken;
-                delete kc.idTokenParsed;
-            }
         }
 
         function decodeToken(str) {
diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
index 6266e2e..342bb2a 100755
--- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
+++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
@@ -173,10 +173,15 @@ public class KeycloakServletExtension implements ServletExtension {
             }
         });
 
-        log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
-        ServletSessionConfig cookieConfig = new ServletSessionConfig();
-        cookieConfig.setPath(deploymentInfo.getContextPath());
-        deploymentInfo.setServletSessionConfig(cookieConfig);
+        ServletSessionConfig cookieConfig = deploymentInfo.getServletSessionConfig();
+        if (cookieConfig == null) {
+            cookieConfig = new ServletSessionConfig();
+        }
+        if (cookieConfig.getPath() == null) {
+            log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
+            cookieConfig.setPath(deploymentInfo.getContextPath());
+            deploymentInfo.setServletSessionConfig(cookieConfig);
+        }
         ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
         deploymentInfo.addListener(new ListenerInfo(UndertowNodesRegistrationManagementWrapper.class, new InstanceFactory<UndertowNodesRegistrationManagementWrapper>() {
 
diff --git a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
index 9d2902e..0e6a1a1 100755
--- a/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
+++ b/adapters/saml/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
@@ -182,10 +182,15 @@ public class SamlServletExtension implements ServletExtension {
             }
         });
 
-        log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
-        ServletSessionConfig cookieConfig = new ServletSessionConfig();
-        cookieConfig.setPath(deploymentInfo.getContextPath());
-        deploymentInfo.setServletSessionConfig(cookieConfig);
+        ServletSessionConfig cookieConfig = deploymentInfo.getServletSessionConfig();
+        if (cookieConfig == null) {
+            cookieConfig = new ServletSessionConfig();
+        }
+        if (cookieConfig.getPath() == null) {
+            log.debug("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
+            cookieConfig.setPath(deploymentInfo.getContextPath());
+            deploymentInfo.setServletSessionConfig(cookieConfig);
+        }
         addEndpointConstraint(deploymentInfo);
 
         ChangeSessionId.turnOffChangeSessionIdOnLogin(deploymentInfo);
diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java
index c2ebc26..4a97d73 100755
--- a/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java
+++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HMACProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.jose.jws.JWSInput;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
 /**
@@ -81,8 +82,7 @@ public class HMACProvider implements SignatureProvider {
     public static boolean verify(JWSInput input, SecretKey key) {
         try {
             byte[] signature = sign(input.getEncodedSignatureInput().getBytes("UTF-8"), input.getHeader().getAlgorithm(), key);
-            String x = Base64Url.encode(signature);
-            return x.equals(input.getEncodedSignature());
+            return MessageDigest.isEqual(signature, Base64Url.decode(input.getEncodedSignature()));
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -92,8 +92,7 @@ public class HMACProvider implements SignatureProvider {
     public static boolean verify(JWSInput input, byte[] sharedSecret) {
         try {
             byte[] signature = sign(input.getEncodedSignatureInput().getBytes("UTF-8"), input.getHeader().getAlgorithm(), sharedSecret);
-            String x = Base64Url.encode(signature);
-            return x.equals(input.getEncodedSignature());
+            return MessageDigest.isEqual(signature, Base64Url.decode(input.getEncodedSignature()));
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
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 f9a3070..916db65 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
@@ -164,9 +164,16 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
                 throw new RuntimeException("Invalid value for sessionsMode");
             }
 
-            sessionConfigBuilder.clustering().hash()
-                    .numOwners(config.getInt("sessionsOwners", 2))
-                    .numSegments(config.getInt("sessionsSegments", 60)).build();
+            int l1Lifespan = config.getInt("l1Lifespan", 600000);
+            boolean l1Enabled = l1Lifespan > 0;
+            sessionConfigBuilder.clustering()
+                    .hash()
+                        .numOwners(config.getInt("sessionsOwners", 2))
+                        .numSegments(config.getInt("sessionsSegments", 60))
+                    .l1()
+                        .enabled(l1Enabled)
+                        .lifespan(l1Lifespan)
+                    .build();
         }
 
         Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 11c1c62..6f76085 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -569,10 +569,7 @@ public class ClientAdapter implements ClientModel {
 
     @Override
     public RoleModel getRole(String name) {
-        for (RoleModel role : getRoles()) {
-            if (role.getName().equals(name)) return role;
-        }
-        return null;
+        return cacheSession.getClientRole(getRealm(), this, name);
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 0b36d67..1e6109f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -801,10 +801,7 @@ public class RealmAdapter implements CachedRealmModel {
 
     @Override
     public RoleModel getRole(String name) {
-        for (RoleModel role : getRoles()) {
-            if (role.getName().equals(name)) return role;
-        }
-        return null;
+        return cacheSession.getRealmRole(this, name);
     }
 
     @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index c21f787..7d68c18 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.models.sessions.infinispan;
 
 import org.infinispan.Cache;
 import org.infinispan.CacheStream;
+import org.infinispan.context.Flag;
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.Time;
 import org.keycloak.models.ClientInitialAccessModel;
@@ -291,6 +292,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     @Override
     public void removeExpired(RealmModel realm) {
+        log.debugf("Removing expired sessions");
         removeExpiredUserSessions(realm);
         removeExpiredClientSessions(realm);
         removeExpiredOfflineUserSessions(realm);
@@ -302,9 +304,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
         int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
 
-        Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
+        // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+        Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+                .entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
 
+        int counter = 0;
         while (itr.hasNext()) {
+            counter++;
             UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
             tx.remove(sessionCache, entity.getId());
 
@@ -314,23 +320,38 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                 }
             }
         }
+
+        log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
     }
 
     private void removeExpiredClientSessions(RealmModel realm) {
         int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
 
-        Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator();
+        // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+        Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+                .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator();
+
+        int counter = 0;
         while (itr.hasNext()) {
+            counter++;
             tx.remove(sessionCache, itr.next().getKey());
         }
+
+        log.debugf("Removed %d expired client sessions for realm '%s'", counter, realm.getName());
     }
 
     private void removeExpiredOfflineUserSessions(RealmModel realm) {
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
         int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
 
-        Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline)).iterator();
+        // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+        UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
+        Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+                .entrySet().stream().filter(predicate).iterator();
+
+        int counter = 0;
         while (itr.hasNext()) {
+            counter++;
             UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
             tx.remove(offlineSessionCache, entity.getId());
 
@@ -340,22 +361,32 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                 tx.remove(offlineSessionCache, clientSessionId);
             }
         }
+
+        log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
     }
 
     private void removeExpiredOfflineClientSessions(RealmModel realm) {
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
         int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
 
-        Iterator<String> itr = offlineSessionCache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator();
+        // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+        Iterator<String> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+                .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator();
+
+        int counter = 0;
         while (itr.hasNext()) {
+            counter++;
             String sessionId = itr.next();
             tx.remove(offlineSessionCache, sessionId);
             persister.removeClientSession(sessionId, true);
         }
+
+        log.debugf("Removed %d expired offline client sessions for realm '%s'", counter, realm.getName());
     }
 
     private void removeExpiredClientInitialAccess(RealmModel realm) {
-        Iterator<String> itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
+        Iterator<String> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+                .entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
         while (itr.hasNext()) {
             tx.remove(sessionCache, itr.next());
         }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
index d636ae3..4b04d9b 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
@@ -60,7 +60,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
 
         KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
         if (sessionFactory == null) {
-            log.warnf("KeycloakSessionFactory not yet set in cache. Worker skipped");
+            log.debugf("KeycloakSessionFactory not yet set in cache. Worker skipped");
             return InfinispanUserSessionInitializer.WorkerResult.create(segment, false);
         }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index e343823..ef11479 100755
--- a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -28,6 +28,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
+import java.security.MessageDigest;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -252,7 +253,7 @@ public class ClientSessionCode {
 
             clientSession.removeNote(ACTIVE_CODE);
 
-            return code.equals(activeCode);
+            return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes());
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
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 c594f7f..fd49f3e 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -17,7 +17,6 @@
 package org.keycloak.broker.oidc;
 
 import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
 
 /**
  * @author Pedro Igor
@@ -61,6 +60,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
         getConfig().put("publicKeySignatureVerifier", signingCertificate);
     }
 
+    public String getPublicKeySignatureVerifierKeyId() {
+        return getConfig().get("publicKeySignatureVerifierKeyId");
+    }
+
+    public void setPublicKeySignatureVerifierKeyId(String publicKeySignatureVerifierKeyId) {
+        getConfig().put("publicKeySignatureVerifierKeyId", publicKeySignatureVerifierKeyId);
+    }
+
     public boolean isValidateSignature() {
         return Boolean.valueOf(getConfig().get("validateSignature"));
     }
diff --git a/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderPublicKeyLoader.java
index cf24a1b..fda2c2f 100644
--- a/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderPublicKeyLoader.java
+++ b/services/src/main/java/org/keycloak/keys/loader/OIDCIdentityProviderPublicKeyLoader.java
@@ -60,7 +60,10 @@ public class OIDCIdentityProviderPublicKeyLoader implements PublicKeyLoader {
                     return Collections.emptyMap();
                 }
 
-                String kid = KeyUtils.createKeyId(publicKey);
+                String presetKeyId = config.getPublicKeySignatureVerifierKeyId();
+                String kid = (presetKeyId == null || presetKeyId.trim().isEmpty())
+                  ? KeyUtils.createKeyId(publicKey)
+                  : presetKeyId;
                 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());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java
new file mode 100644
index 0000000..7b6de1b
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.model;
+
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.testsuite.rule.KeycloakRule;
+
+/**
+ * Run test with shared MySQL DB and in cluster:
+ *
+ * -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver -Dkeycloak.connectionsJpa.user=keycloak
+ * -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Ignore
+public class ClusterSessionCleanerTest {
+
+    protected static final Logger logger = Logger.getLogger(ClusterSessionCleanerTest.class);
+
+    private static final String REALM_NAME = "test";
+
+    @ClassRule
+    public static KeycloakRule server1 = new KeycloakRule();
+
+    @ClassRule
+    public static KeycloakRule server2 = new KeycloakRule() {
+
+        @Override
+        protected void configureServer(KeycloakServer server) {
+            server.getConfig().setPort(8082);
+        }
+
+        @Override
+        protected void importRealm() {
+        }
+
+        @Override
+        protected void removeTestRealms() {
+        }
+
+    };
+
+    @Test
+    public void testClusterPeriodicSessionCleanups() throws Exception {
+        // Add some userSessions on server1
+        KeycloakSession session1 = server1.startSession();
+        RealmModel realm1 = session1.realms().getRealmByName(REALM_NAME);
+        UserModel user1 = session1.users().getUserByUsername("test-user@localhost", realm1);
+        for (int i=0 ; i<15 ; i++) {
+            session1.sessions().createUserSession(realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
+        }
+        session1 = commit(server1, session1);
+
+        // Add some userSessions on server2
+        KeycloakSession session2 = server2.startSession();
+        RealmModel realm2 = session2.realms().getRealmByName(REALM_NAME);
+        UserModel user2 = session2.users().getUserByUsername("test-user@localhost", realm2);
+        // Check we are really in cluster (same user ids)
+        Assert.assertEquals(user2.getId(), user1.getId());
+
+        for (int i=0 ; i<15 ; i++) {
+            session2.sessions().createUserSession(realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
+        }
+        session2 = commit(server2, session2);
+
+        // Assert sessions on both nodes
+        List<UserSessionModel> sessions1 = getSessions(session1);
+        List<UserSessionModel> sessions2 = getSessions(session2);
+        Assert.assertEquals(30, sessions1.size());
+        Assert.assertEquals(30, sessions2.size());
+        logger.info("Before offset: sessions1 : " + sessions1.size());
+        logger.info("Before offset: sessions2 : " + sessions2.size());
+
+
+        // set Time offset and run periodic cleaner on server1
+        Time.setOffset(999999);
+        realm1 = session1.realms().getRealmByName(REALM_NAME);
+        session1.sessions().removeExpired(realm1);
+        session1 = commit(server1, session1);
+
+        // Ensure some sessions still there
+        sessions1 = getSessions(session1);
+        sessions2 = getSessions(session2);
+        logger.info("After server1 periodic clean: sessions1 : " + sessions1.size());
+        logger.info("After server1 periodic clean: sessions2 : " + sessions2.size());
+
+
+        // Run periodic cleaner on server2
+        realm2 = session2.realms().getRealmByName(REALM_NAME);
+        session2.sessions().removeExpired(realm2);
+        session2 = commit(server1, session2);
+
+        // Ensure there are no sessions on server1 or server2
+        sessions1 = getSessions(session1);
+        sessions2 = getSessions(session2);
+        Assert.assertTrue(sessions1.isEmpty());
+        Assert.assertTrue(sessions2.isEmpty());
+        logger.info("After both periodic cleans: sessions1 : " + sessions1.size());
+        logger.info("After both periodic cleans: sessions2 : " + sessions2.size());
+    }
+
+    private List<UserSessionModel> getSessions(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(REALM_NAME);
+        UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
+        return session.sessions().getUserSessions(realm, user);
+    }
+
+    private KeycloakSession commit(KeycloakRule rule, KeycloakSession session) throws Exception {
+        session.getTransactionManager().commit();
+        session.close();
+        return rule.startSession();
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
index 59be8aa..f94cea0 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
@@ -223,4 +223,18 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand {
         }
     }
 
+
+    public static class SizeLocalCommand extends AbstractOfflineCacheCommand {
+
+        @Override
+        public String getName() {
+            return "sizeLocal";
+        }
+
+        @Override
+        protected void doRunCacheCommand(KeycloakSession session, Cache<String, SessionEntity> cache) {
+            log.info("Size local: " + cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL).size());
+        }
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
index 9b2c17a..b1ff087 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/TestsuiteCLI.java
@@ -47,6 +47,7 @@ public class TestsuiteCLI {
             AbstractOfflineCacheCommand.GetCommand.class,
             AbstractOfflineCacheCommand.GetMultipleCommand.class,
             AbstractOfflineCacheCommand.GetLocalCommand.class,
+            AbstractOfflineCacheCommand.SizeLocalCommand.class,
             AbstractOfflineCacheCommand.RemoveCommand.class,
             AbstractOfflineCacheCommand.SizeCommand.class,
             AbstractOfflineCacheCommand.ListCommand.class,
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 27e9f5e..40a15e9 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -97,7 +97,8 @@
         "default": {
             "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
             "async": "${keycloak.connectionsInfinispan.async:false}",
-            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
+            "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
+            "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
             "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
             "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
             "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SecurePortalWithCustomSessionConfig.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SecurePortalWithCustomSessionConfig.java
new file mode 100644
index 0000000..47b51dd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SecurePortalWithCustomSessionConfig.java
@@ -0,0 +1,43 @@
+/*
+ * 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.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+
+import java.net.URL;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+public class SecurePortalWithCustomSessionConfig extends AbstractPageWithInjectedUrl {
+
+    public static final String DEPLOYMENT_NAME = "secure-portal-with-custom-session-config";
+
+    @ArquillianResource
+    @OperateOnDeployment(DEPLOYMENT_NAME)
+    private URL url;
+
+    @Override
+    public URL getInjectedUrl() {
+        return url;
+    }
+
+}
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 fde26a1..cf50cce 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
@@ -24,6 +24,7 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
+
 import org.keycloak.OAuth2Constants;
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.common.Version;
@@ -38,14 +39,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
 import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter;
-import org.keycloak.testsuite.adapter.page.BasicAuth;
-import org.keycloak.testsuite.adapter.page.CustomerDb;
-import org.keycloak.testsuite.adapter.page.CustomerDbErrorPage;
-import org.keycloak.testsuite.adapter.page.CustomerPortal;
-import org.keycloak.testsuite.adapter.page.InputPortal;
-import org.keycloak.testsuite.adapter.page.ProductPortal;
-import org.keycloak.testsuite.adapter.page.SecurePortal;
-import org.keycloak.testsuite.adapter.page.TokenMinTTLPage;
+import org.keycloak.testsuite.adapter.page.*;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.auth.page.account.Applications;
 import org.keycloak.testsuite.auth.page.login.OAuthGrant;
@@ -53,6 +47,7 @@ import org.keycloak.testsuite.console.page.events.Config;
 import org.keycloak.testsuite.console.page.events.LoginEvents;
 import org.keycloak.testsuite.util.URLUtils;
 import org.keycloak.util.BasicAuthHelper;
+
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebElement;
 
@@ -73,12 +68,13 @@ import java.util.concurrent.TimeUnit;
 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.assertTrue;
-import org.keycloak.testsuite.adapter.page.CustomerPortalNoConf;
+import static org.junit.Assert.*;
+
+import org.keycloak.testsuite.util.Matchers;
+
+import javax.ws.rs.core.Response.Status;
+
+import static org.hamcrest.Matchers.*;
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
@@ -98,6 +94,8 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
     @Page
     private SecurePortal securePortal;
     @Page
+    private SecurePortalWithCustomSessionConfig securePortalWithCustomSessionConfig;
+    @Page
     private CustomerDb customerDb;
     @Page
     private CustomerDbErrorPage customerDbErrorPage;
@@ -133,6 +131,11 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         return servletDeployment(SecurePortal.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
     }
 
+    @Deployment(name = SecurePortalWithCustomSessionConfig.DEPLOYMENT_NAME)
+    protected static WebArchive securePortalWithCustomSessionConfig() {
+        return servletDeployment(SecurePortalWithCustomSessionConfig.DEPLOYMENT_NAME, CallAuthenticatedServlet.class);
+    }
+
     @Deployment(name = CustomerDb.DEPLOYMENT_NAME)
     protected static WebArchive customerDb() {
         return servletDeployment(CustomerDb.DEPLOYMENT_NAME, AdapterActionsFilter.class, CustomerDatabaseServlet.class);
@@ -479,6 +482,27 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
     }
 
+    @Test
+    public void testAuthenticatedWithCustomSessionConfig() {
+        // test login to customer-portal which does a bearer request to customer-db
+        securePortalWithCustomSessionConfig.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        testRealmLoginPage.form().login("bburke@redhat.com", "password");
+        assertCurrentUrlEquals(securePortalWithCustomSessionConfig);
+
+        assertThat("Cookie CUSTOM_JSESSION_ID_NAME should exist", driver.manage().getCookieNamed("CUSTOM_JSESSION_ID_NAME"), notNullValue());
+
+        String pageSource = driver.getPageSource();
+        assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+        // test logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder())
+                .queryParam(OAuth2Constants.REDIRECT_URI, securePortalWithCustomSessionConfig.toString()).build("demo").toString();
+        driver.navigate().to(logoutUri);
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+        securePortalWithCustomSessionConfig.navigateTo();
+        assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
+    }
+
     // Tests "token-minimum-time-to-live" adapter configuration option
     @Test
     public void testTokenMinTTL() {
@@ -549,23 +573,19 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         Response response = client.target(basicAuthPage
                 .setTemplateValues("mposolda", "password", value).buildUri()).request().get();
 
-        assertEquals(200, response.getStatus());
+        assertThat(response, Matchers.statusCodeIs(Status.OK));
         assertEquals(value, response.readEntity(String.class));
         response.close();
 
         response = client.target(basicAuthPage
                 .setTemplateValues("invalid-user", "password", value).buildUri()).request().get();
-        assertEquals(401, response.getStatus());
-        String readResponse = response.readEntity(String.class);
-        assertTrue(readResponse.contains("Unauthorized") || readResponse.contains("Status 401"));
-        response.close();
+        assertThat(response, Matchers.statusCodeIs(Status.UNAUTHORIZED));
+        assertThat(response, Matchers.body(anyOf(containsString("Unauthorized"), containsString("Status 401"))));
 
         response = client.target(basicAuthPage
                 .setTemplateValues("admin", "invalid-password", value).buildUri()).request().get();
-        assertEquals(401, response.getStatus());
-        readResponse = response.readEntity(String.class);
-        assertTrue(readResponse.contains("Unauthorized") || readResponse.contains("Status 401"));
-        response.close();
+        assertThat(response, Matchers.statusCodeIs(Status.UNAUTHORIZED));
+        assertThat(response, Matchers.body(anyOf(containsString("Unauthorized"), containsString("Status 401"))));
 
         client.close();
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoFilterServletAdapterTest.java
index e910a2a..25ba365 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoFilterServletAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoFilterServletAdapterTest.java
@@ -20,9 +20,16 @@ package org.keycloak.testsuite.adapter.undertow.servlet;
 import org.keycloak.testsuite.adapter.servlet.AbstractDemoFilterServletAdapterTest;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
 
+import org.junit.Ignore;
+
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @AppServerContainer("auth-server-undertow")
 public class UndertowDemoFilterServletAdapterTest extends AbstractDemoFilterServletAdapterTest {
+    @Ignore
+    @Override
+    public void testAuthenticatedWithCustomSessionConfig() {
+        // Undertow deployment ignores session cookie settings in web.xml, see org.keycloak.testsuite.arquillian.undertow.SimpleWebXmlParser class
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoServletsAdapterTest.java
index c1be0ca..5849a11 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowDemoServletsAdapterTest.java
@@ -20,9 +20,17 @@ package org.keycloak.testsuite.adapter.undertow.servlet;
 import org.keycloak.testsuite.adapter.servlet.AbstractDemoServletsAdapterTest;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
 
+import org.junit.Ignore;
+
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @AppServerContainer("auth-server-undertow")
 public class UndertowDemoServletsAdapterTest extends AbstractDemoServletsAdapterTest {
+
+    @Ignore
+    @Override
+    public void testAuthenticatedWithCustomSessionConfig() {
+        // Undertow deployment ignores session cookie settings in web.xml, see org.keycloak.testsuite.arquillian.undertow.SimpleWebXmlParser class
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java
index 9bdc40e..6bc60b1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java
@@ -24,12 +24,12 @@ import javax.ws.rs.core.UriBuilder;
 
 import org.junit.Before;
 import org.junit.Test;
+
 import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.*;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.keys.KeyProvider;
 import org.keycloak.keys.PublicKeyStorageUtils;
-import org.keycloak.keys.loader.PublicKeyStorageManager;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ComponentRepresentation;
@@ -181,6 +181,38 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
         assertErrorPage("Unexpected error when authenticating with identity provider");
     }
 
+    @Test
+    public void testSignatureVerificationHardcodedPublicKeyWithKeyIdSetExplicitly() throws Exception {
+        // Configure OIDC identity provider with JWKS URL
+        IdentityProviderRepresentation idpRep = getIdentityProvider();
+        OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
+        cfg.setValidateSignature(true);
+        cfg.setUseJwksUrl(false);
+
+        KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(providerRealm());
+        String pemData = key.getPublicKey();
+        cfg.setPublicKeySignatureVerifier(pemData);
+        String expectedKeyId = KeyUtils.createKeyId(PemUtils.decodePublicKey(pemData));
+        updateIdentityProvider(idpRep);
+
+        // Check that user is able to login
+        logInAsUserInIDPForFirstTime();
+        assertLoggedInAccountManagement();
+
+        logoutFromRealm(bc.consumerRealmName());
+
+        // Set key id to an invalid one
+        cfg.setPublicKeySignatureVerifierKeyId("invalid-key-id");
+        updateIdentityProvider(idpRep);
+
+        logInAsUserInIDP();
+        assertErrorPage("Unexpected error when authenticating with identity provider");
+
+        // Set key id to a valid one
+        cfg.setPublicKeySignatureVerifierKeyId(expectedKeyId);
+        updateIdentityProvider(idpRep);
+    }
+
 
     @Test
     public void testClearKeysCache() throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
index 072ca40..a046827 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/demorealm.json
@@ -215,6 +215,19 @@
             }
         },
         {
+            "clientId": "secure-portal-with-custom-session-config",
+            "enabled": true,
+            "adminUrl": "/secure-portal-with-custom-session-config",
+            "baseUrl": "/secure-portal-with-custom-session-config",
+            "clientAuthenticatorType": "client-jwt",
+            "redirectUris": [
+                "/secure-portal-with-custom-session-config/*"
+            ],
+            "attributes" : {
+                "jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg=="
+            }
+        },
+        {
             "clientId": "session-portal",
             "enabled": true,
             "adminUrl": "/session-portal",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/META-INF/context.xml
new file mode 100644
index 0000000..b4ddcce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/META-INF/context.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<Context path="/customer-portal">
+    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  ~ 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.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+    <Get name="securityHandler">
+        <Set name="authenticator">
+            <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+                <!--
+                <Set name="adapterConfig">
+                    <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+                        <Set name="realm">tomcat</Set>
+                        <Set name="resource">customer-portal</Set>
+                        <Set name="authServerUrl">http://localhost:8180/auth</Set>
+                        <Set name="sslRequired">external</Set>
+                        <Set name="credentials">
+                            <Map>
+                                <Entry>
+                                    <Item>secret</Item>
+                                    <Item>password</Item>
+                                </Entry>
+                            </Map>
+                        </Set>
+                        <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+                    </New>
+                </Set>
+                -->
+            </New>
+        </Set>
+    </Get>
+</Configure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keycloak.json
new file mode 100644
index 0000000..443a08c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keycloak.json
@@ -0,0 +1,16 @@
+{
+  "realm": "demo",
+  "auth-server-url": "http://localhost:8180/auth",
+  "ssl-required": "external",
+  "resource": "secure-portal-with-custom-session-config",
+  "credentials": {
+    "jwt": {
+      "client-key-password": "password",
+      "client-keystore-file": "classpath:keystore.jks",
+      "client-keystore-password": "password",
+      "client-key-alias": "secure-portal",
+      "token-timeout": 10,
+      "client-keystore-type": "jks"
+    }
+  }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keystore.jks
new file mode 100644
index 0000000..399be7a
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/web.xml
new file mode 100644
index 0000000..bfcc1db
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/secure-portal-with-custom-session-config/WEB-INF/web.xml
@@ -0,0 +1,64 @@
+<?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.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>secure-portal-with-custom-session-config</module-name>
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Permit all</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>*</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <session-config>
+        <cookie-config>
+            <http-only>true</http-only>
+            <name>CUSTOM_JSESSION_ID_NAME</name>
+        </cookie-config>
+    </session-config>
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
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 e6a9a63..04b6d21 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
@@ -503,6 +503,8 @@ identity-provider.use-jwks-url.tooltip=If the switch is on, then identity provid
 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.
+validating-public-key-id=Validating Public Key Id
+identity-provider.validating-public-key-id.tooltip=Explicit ID of the validating public key given above if the key ID. Leave unset if the external IDP is Keycloak or uses the same mechanism to determine key ID.
 import-external-idp-config=Import External IDP Config
 import-external-idp-config.tooltip=Allows you to load external IDP metadata from a config file or to download it from a URL.
 import-from-url=Import from URL
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 e33b52d..b4069ea 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
@@ -211,13 +211,21 @@
                 </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>
+                    <label class="col-md-2 control-label" for="publicKeySignatureVerifierKey">{{:: '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 class="form-group clearfix" data-ng-hide="identityProvider.config.useJwksUrl == 'true'">
+                    <label class="col-md-2 control-label" for="publicKeySignatureVerifierKeyId">{{:: 'validating-public-key-id' | translate}}</label>
+                    <div class="col-md-6">
+                        <input class="form-control" id="publicKeySignatureVerifierKeyId" ng-model="identityProvider.config.publicKeySignatureVerifierKeyId"/>
+                    </div>
+                    <kc-tooltip>{{:: 'identity-provider.validating-public-key-id.tooltip' | translate}}</kc-tooltip>
+                </div>
+
             </div>
 
         </fieldset>