diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index 70b318ec4b..f0cae4211c 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,49 +634,6 @@ kc.tokenTimeoutHandle = null; } - 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; - } - kc.sessionId = sessionId; - kc.authenticated = true; - kc.subject = kc.tokenParsed.sub; - kc.realmAccess = kc.tokenParsed.realm_access; - kc.resourceAccess = kc.tokenParsed.resource_access; - - 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 { - var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000; - if (expiresIn <= 0) { - kc.onTokenExpired(); - } else { - kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn); - } - } - } - - } else { - delete kc.token; - delete kc.tokenParsed; - delete kc.subject; - delete kc.realmAccess; - delete kc.resourceAccess; - - kc.authenticated = false; - } - if (refreshToken) { kc.refreshToken = refreshToken; kc.refreshTokenParsed = decodeToken(refreshToken); @@ -696,6 +649,48 @@ 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; + } + kc.sessionId = sessionId; + kc.authenticated = true; + kc.subject = kc.tokenParsed.sub; + kc.realmAccess = kc.tokenParsed.realm_access; + kc.resourceAccess = kc.tokenParsed.resource_access; + + 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'); + + 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; + delete kc.subject; + delete kc.realmAccess; + delete kc.resourceAccess; + + kc.authenticated = false; + } } 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 6266e2ee75..342bb2ab6a 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() { 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 9d2902e671..0e6a1a1f80 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 c2ebc26dbf..4a97d7343a 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 f9a30703ac..916db65cc3 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 11c1c62ac7..6f7608577b 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 0b36d671d3..1e6109f6b1 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 c21f787098..7d68c18ae4 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> 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> 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> 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> 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> 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> 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 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 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 itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator(); + Iterator 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 d636ae379e..4b04d9b752 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 DistributedCallableMarek Posolda */ @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 c1be0ca889..5849a118bb 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 Marek Posolda */ @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 9bdc40ed4c..6bc60b1aea 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 072ca402bb..a0468278df 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 @@ -214,6 +214,19 @@ "jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg==" } }, + { + "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, 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 0000000000..b4ddcce386 --- /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 @@ + + + + + \ 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 0000000000..8c59313878 --- /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 @@ + + + + + + + + + + + + + \ 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 0000000000..443a08cfdf --- /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 0000000000..399be7a48a 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 0000000000..bfcc1db59b --- /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 @@ + + + + + + secure-portal-with-custom-session-config + + + Servlet + org.keycloak.testsuite.adapter.servlet.CallAuthenticatedServlet + + + + Servlet + /* + + + + + Permit all + /* + + + * + + + + + + true + CUSTOM_JSESSION_ID_NAME + + + + + KEYCLOAK + demo + + + + admin + + + user + + 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 0000000000..7b6de1bb5c --- /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 Marek Posolda + */ +@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 sessions1 = getSessions(session1); + List 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 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 59be8aadee..f94cea05d6 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 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 9b2c17aaca..b1ff087950 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 27e9f5ec9e..40a15e98e6 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/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 e6a9a63b1f..04b6d212ab 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 e33b52d05e..b4069ea1d4 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 @@
- +