Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2017-01-12 09:34:36 -05:00
commit 1e51ade620
29 changed files with 594 additions and 110 deletions

View file

@ -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) {

View file

@ -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>() {

View file

@ -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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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());
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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"));
}

View file

@ -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());

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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"
}
}
}

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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());
}
}
}

View file

@ -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,

View file

@ -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}"

View file

@ -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

View file

@ -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>