Merge pull request #3550 from mposolda/master
KEYCLOAK-3825 Ability to expire publicKeys cache. Migrated OIDCBroker…
This commit is contained in:
commit
7db06b8afb
23 changed files with 693 additions and 415 deletions
|
@ -187,6 +187,10 @@ public interface RealmResource {
|
|||
@POST
|
||||
void clearUserCache();
|
||||
|
||||
@Path("clear-keys-cache")
|
||||
@POST
|
||||
void clearKeysCache();
|
||||
|
||||
@Path("push-revocation")
|
||||
@POST
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -152,18 +152,8 @@ public class InfinispanNotificationsManager {
|
|||
|
||||
private void hotrodEventReceived(String key) {
|
||||
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
|
||||
Object value = remoteCache.get(key);
|
||||
|
||||
Serializable rawValue;
|
||||
if (value instanceof MarshalledEntry) {
|
||||
Object rw = ((MarshalledEntry)value).getValue();
|
||||
rawValue = (Serializable) rw;
|
||||
} else {
|
||||
rawValue = (Serializable) value;
|
||||
}
|
||||
|
||||
|
||||
eventReceived(key, rawValue);
|
||||
Object value = workCache.get(key);
|
||||
eventReceived(key, (Serializable) value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,9 +27,13 @@ import java.util.concurrent.FutureTask;
|
|||
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.keys.PublicKeyLoader;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
|
||||
import org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -39,18 +43,27 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
|
|||
|
||||
private static final Logger log = Logger.getLogger(InfinispanPublicKeyStorageProvider.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
private final Cache<String, PublicKeysEntry> keys;
|
||||
|
||||
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress;
|
||||
|
||||
private final int minTimeBetweenRequests ;
|
||||
|
||||
public InfinispanPublicKeyStorageProvider(Cache<String, PublicKeysEntry> keys, Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
|
||||
public InfinispanPublicKeyStorageProvider(KeycloakSession session, Cache<String, PublicKeysEntry> keys, Map<String, FutureTask<PublicKeysEntry>> tasksInProgress, int minTimeBetweenRequests) {
|
||||
this.session = session;
|
||||
this.keys = keys;
|
||||
this.tasksInProgress = tasksInProgress;
|
||||
this.minTimeBetweenRequests = minTimeBetweenRequests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
keys.clear();
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
|
||||
|
|
|
@ -24,12 +24,15 @@ import java.util.concurrent.FutureTask;
|
|||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.cluster.ClusterEvent;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.keys.PublicKeyStorageSpi;
|
||||
import org.keycloak.keys.PublicKeyStorageProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -40,6 +43,8 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
|
||||
public static final String PROVIDER_ID = "infinispan";
|
||||
|
||||
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
|
||||
|
||||
private Cache<String, PublicKeysEntry> keysCache;
|
||||
|
||||
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
|
||||
|
@ -49,7 +54,7 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
@Override
|
||||
public PublicKeyStorageProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new InfinispanPublicKeyStorageProvider(keysCache, tasksInProgress, minTimeBetweenRequests);
|
||||
return new InfinispanPublicKeyStorageProvider(session, keysCache, tasksInProgress, minTimeBetweenRequests);
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
|
@ -57,6 +62,13 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
|
|||
synchronized (this) {
|
||||
if (keysCache == null) {
|
||||
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
cluster.registerListener(KEYS_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
|
||||
|
||||
keysCache.clear();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ public class InfinispanKeyStorageProviderTest {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
InfinispanPublicKeyStorageProvider provider = new InfinispanPublicKeyStorageProvider(keys, tasksInProgress, minTimeBetweenRequests);
|
||||
InfinispanPublicKeyStorageProvider provider = new InfinispanPublicKeyStorageProvider(null, keys, tasksInProgress, minTimeBetweenRequests);
|
||||
provider.getPublicKey(modelKey, "kid1", new SampleLoader(modelKey));
|
||||
}
|
||||
|
||||
|
|
|
@ -37,4 +37,9 @@ public interface PublicKeyStorageProvider extends Provider {
|
|||
*/
|
||||
PublicKey getPublicKey(String modelKey, String kid, PublicKeyLoader loader);
|
||||
|
||||
/**
|
||||
* Clears all the cached public keys, so they need to be loaded again
|
||||
*/
|
||||
void clearCache();
|
||||
|
||||
}
|
||||
|
|
|
@ -69,18 +69,20 @@ public class IdentityProviderModel implements Serializable {
|
|||
}
|
||||
|
||||
public IdentityProviderModel(IdentityProviderModel model) {
|
||||
this.internalId = model.getInternalId();
|
||||
this.providerId = model.getProviderId();
|
||||
this.alias = model.getAlias();
|
||||
this.displayName = model.getDisplayName();
|
||||
this.config = new HashMap<String, String>(model.getConfig());
|
||||
this.enabled = model.isEnabled();
|
||||
this.trustEmail = model.isTrustEmail();
|
||||
this.storeToken = model.isStoreToken();
|
||||
this.authenticateByDefault = model.isAuthenticateByDefault();
|
||||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
|
||||
if (model != null) {
|
||||
this.internalId = model.getInternalId();
|
||||
this.providerId = model.getProviderId();
|
||||
this.alias = model.getAlias();
|
||||
this.displayName = model.getDisplayName();
|
||||
this.config = new HashMap<String, String>(model.getConfig());
|
||||
this.enabled = model.isEnabled();
|
||||
this.trustEmail = model.isTrustEmail();
|
||||
this.storeToken = model.isStoreToken();
|
||||
this.authenticateByDefault = model.isAuthenticateByDefault();
|
||||
this.addReadTokenRoleOnCreate = model.addReadTokenRoleOnCreate;
|
||||
this.firstBrokerLoginFlowId = model.getFirstBrokerLoginFlowId();
|
||||
this.postBrokerLoginFlowId = model.getPostBrokerLoginFlowId();
|
||||
}
|
||||
}
|
||||
|
||||
public String getInternalId() {
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.keycloak.events.admin.OperationType;
|
|||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverter;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
|
||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.GroupModel;
|
||||
|
@ -873,6 +874,23 @@ public class RealmAdminResource {
|
|||
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache of external public keys (Public keys of clients or Identity providers)
|
||||
*
|
||||
*/
|
||||
@Path("clear-keys-cache")
|
||||
@POST
|
||||
public void clearKeysCache() {
|
||||
auth.requireManage();
|
||||
|
||||
PublicKeyStorageProvider cache = session.getProvider(PublicKeyStorageProvider.class);
|
||||
if (cache != null) {
|
||||
cache.clearCache();
|
||||
}
|
||||
|
||||
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
|
||||
}
|
||||
|
||||
@Path("keys")
|
||||
public KeyResource keys() {
|
||||
KeyResource resource = new KeyResource(realm, session, this.auth);
|
||||
|
|
|
@ -59,6 +59,7 @@ import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
|
|||
import org.keycloak.testsuite.forms.PassThroughAuthenticator;
|
||||
import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
|
||||
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
|
||||
import org.keycloak.testsuite.rest.resource.TestCacheResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
|
@ -516,15 +517,12 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
return details;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/cache/{cache}/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public boolean isCached(@PathParam("cache") String cacheName, @PathParam("id") String id) {
|
||||
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
|
||||
Cache<Object, Object> cache = provider.getCache(cacheName);
|
||||
return cache.containsKey(id);
|
||||
@Path("/cache/{cache}")
|
||||
public TestCacheResource getCacheResource(@PathParam("cache") String cacheName) {
|
||||
return new TestCacheResource(session, cacheName);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.rest.resource;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class TestCacheResource {
|
||||
|
||||
private final Cache<Object, Object> cache;
|
||||
|
||||
public TestCacheResource(KeycloakSession session, String cacheName) {
|
||||
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
|
||||
cache = provider.getCache(cacheName);
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/contains/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public boolean contains(@PathParam("id") String id) {
|
||||
return cache.containsKey(id);
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/enumerate-keys")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Set<String> enumerateKeys() {
|
||||
return cache.keySet().stream().map((Object o) -> {
|
||||
|
||||
return o.toString();
|
||||
|
||||
}).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/size")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public int size() {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.client.resources;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface TestingCacheResource {
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/contains/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
boolean contains(@PathParam("id") String id);
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/enumerate-keys")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Set<String> enumerateKeys();
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/size")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
int size();
|
||||
|
||||
}
|
|
@ -24,6 +24,7 @@ import org.keycloak.representations.idm.EventRepresentation;
|
|||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.components.TestProvider;
|
||||
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
|
||||
import org.keycloak.testsuite.rest.resource.TestCacheResource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -190,10 +191,8 @@ public interface TestingResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response removeExpired(@QueryParam("realm") final String realm);
|
||||
|
||||
@GET
|
||||
@Path("/cache/{cache}/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
boolean isCached(@PathParam("cache") String cacheName, @PathParam("id") String id);
|
||||
@Path("/cache/{cache}")
|
||||
TestingCacheResource cache(@PathParam("cache") String cacheName);
|
||||
|
||||
@POST
|
||||
@Path("/update-pass-through-auth-state")
|
||||
|
|
|
@ -463,12 +463,12 @@ public class RealmTest extends AbstractAdminTest {
|
|||
@Test
|
||||
public void clearRealmCache() {
|
||||
RealmRepresentation realmRep = realm.toRepresentation();
|
||||
assertTrue(testingClient.testing().isCached("realms", realmRep.getId()));
|
||||
assertTrue(testingClient.testing().cache("realms").contains(realmRep.getId()));
|
||||
|
||||
realm.clearRealmCache();
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.ACTION, "clear-realm-cache", ResourceType.REALM);
|
||||
|
||||
assertFalse(testingClient.testing().isCached("realms", realmRep.getId()));
|
||||
assertFalse(testingClient.testing().cache("realms").contains(realmRep.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -482,14 +482,16 @@ public class RealmTest extends AbstractAdminTest {
|
|||
|
||||
realm.users().get(userId).toRepresentation();
|
||||
|
||||
assertTrue(testingClient.testing().isCached("users", userId));
|
||||
assertTrue(testingClient.testing().cache("users").contains(userId));
|
||||
|
||||
realm.clearUserCache();
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.ACTION, "clear-user-cache", ResourceType.REALM);
|
||||
|
||||
assertFalse(testingClient.testing().isCached("users", userId));
|
||||
assertFalse(testingClient.testing().cache("users").contains(userId));
|
||||
}
|
||||
|
||||
// NOTE: clearKeysCache tested in KcOIDCBrokerWithSignatureTest
|
||||
|
||||
@Test
|
||||
public void pushNotBefore() {
|
||||
setupTestAppAndUser();
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.Retry;
|
||||
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
||||
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
|
||||
import org.openqa.selenium.TimeoutException;
|
||||
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.encodeUrl;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||
|
||||
/**
|
||||
* No test methods there. Just some useful common functionality
|
||||
*/
|
||||
public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
|
||||
|
||||
@Page
|
||||
protected AccountUpdateProfilePage accountUpdateProfilePage;
|
||||
|
||||
// TODO: Rename this to loginPage
|
||||
@Page
|
||||
protected LoginPage accountLoginPage;
|
||||
|
||||
@Page
|
||||
protected UpdateAccountInformationPage updateAccountInformationPage;
|
||||
|
||||
@Page
|
||||
protected AccountPasswordPage accountPasswordPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
||||
|
||||
protected BrokerConfiguration bc = getBrokerConfiguration();
|
||||
|
||||
protected String userId;
|
||||
|
||||
/**
|
||||
* Returns a broker configuration. Return value should not change between calls.
|
||||
* @return
|
||||
*/
|
||||
protected abstract BrokerConfiguration getBrokerConfiguration();
|
||||
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation providerRealm = bc.createProviderRealm();
|
||||
RealmRepresentation consumerRealm = bc.createConsumerRealm();
|
||||
|
||||
testRealms.add(providerRealm);
|
||||
testRealms.add(consumerRealm);
|
||||
}
|
||||
|
||||
|
||||
protected void logInAsUserInIDP() {
|
||||
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
|
||||
|
||||
log.debug("Clicking social " + bc.getIDPAlias());
|
||||
accountLoginPage.clickSocial(bc.getIDPAlias());
|
||||
|
||||
waitForPage(driver, "log in to");
|
||||
|
||||
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||
|
||||
log.debug("Logging in");
|
||||
accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
||||
}
|
||||
|
||||
|
||||
/** Logs in the IDP and updates account information */
|
||||
protected void logInAsUserInIDPForFirstTime() {
|
||||
logInAsUserInIDP();
|
||||
|
||||
waitForPage(driver, "update account information");
|
||||
|
||||
Assert.assertTrue(updateAccountInformationPage.isCurrent());
|
||||
Assert.assertTrue("We must be on correct realm right now",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||
|
||||
log.debug("Updating info on updateAccount page");
|
||||
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||
}
|
||||
|
||||
|
||||
protected String getAccountUrl(String realmName) {
|
||||
return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/account";
|
||||
}
|
||||
|
||||
|
||||
protected String getAccountPasswordUrl(String realmName) {
|
||||
return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/account/password";
|
||||
}
|
||||
|
||||
|
||||
protected void logoutFromRealm(String realm) {
|
||||
driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext)
|
||||
+ "/auth/realms/" + realm
|
||||
+ "/protocol/" + "openid-connect"
|
||||
+ "/logout?redirect_uri=" + encodeUrl(getAccountUrl(realm)));
|
||||
|
||||
try {
|
||||
Retry.execute(() -> {
|
||||
try {
|
||||
waitForPage(driver, "log in to " + realm);
|
||||
} catch (TimeoutException ex) {
|
||||
driver.navigate().refresh();
|
||||
log.debug("[Retriable] Timed out waiting for login page");
|
||||
throw ex;
|
||||
}
|
||||
}, 10, 100);
|
||||
} catch (TimeoutException e) {
|
||||
log.debug(driver.getTitle());
|
||||
log.debug(driver.getPageSource());
|
||||
Assert.fail("Timeout while waiting for login page");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void assertLoggedInAccountManagement() {
|
||||
Assert.assertTrue(accountUpdateProfilePage.isCurrent());
|
||||
Assert.assertEquals(accountUpdateProfilePage.getUsername(), bc.getUserLogin());
|
||||
Assert.assertEquals(accountUpdateProfilePage.getEmail(), bc.getUserEmail());
|
||||
}
|
||||
|
||||
|
||||
protected void assertErrorPage(String expectedError) {
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals(expectedError, errorPage.getError());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
|
@ -8,13 +7,7 @@ import org.keycloak.admin.client.resource.UsersResource;
|
|||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.Retry;
|
||||
import org.keycloak.testsuite.pages.AccountPasswordPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
|
||||
import org.openqa.selenium.TimeoutException;
|
||||
|
@ -27,45 +20,12 @@ import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
|
|||
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.USER_EMAIL;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.*;
|
||||
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
|
||||
import static org.keycloak.testsuite.util.MailAssert.assertEmailAndGetUrl;
|
||||
import org.keycloak.testsuite.util.MailServer;
|
||||
import org.keycloak.testsuite.util.MailServerConfiguration;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
||||
|
||||
@Page
|
||||
protected LoginPage accountLoginPage;
|
||||
|
||||
@Page
|
||||
protected UpdateAccountInformationPage updateAccountInformationPage;
|
||||
|
||||
@Page
|
||||
protected AccountPasswordPage accountPasswordPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
||||
|
||||
protected BrokerConfiguration bc = getBrokerConfiguration();
|
||||
|
||||
/**
|
||||
* Returns a broker configuration. Return value should not change between calls.
|
||||
* @return
|
||||
*/
|
||||
protected abstract BrokerConfiguration getBrokerConfiguration();
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation providerRealm = bc.createProviderRealm();
|
||||
RealmRepresentation consumerRealm = bc.createConsumerRealm();
|
||||
|
||||
testRealms.add(providerRealm);
|
||||
testRealms.add(consumerRealm);
|
||||
}
|
||||
public abstract class AbstractBrokerTest extends AbstractBaseBrokerTest {
|
||||
|
||||
@Before
|
||||
public void createUser() {
|
||||
|
@ -114,12 +74,9 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
protected String getAuthRoot() {
|
||||
return suiteContext.getAuthServerInfo().getContextRoot().toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logInAsUserInIDP() {
|
||||
public void testLogInAsUserInIDP() {
|
||||
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
|
||||
|
||||
log.debug("Clicking social " + bc.getIDPAlias());
|
||||
|
@ -165,7 +122,7 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void loginWithExistingUser() {
|
||||
logInAsUserInIDP();
|
||||
testLogInAsUserInIDP();
|
||||
|
||||
Integer userCount = adminClient.realm(bc.consumerRealmName()).users().count();
|
||||
|
||||
|
@ -299,28 +256,6 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
|||
assertEquals("Account is disabled, contact admin.", errorPage.getError());
|
||||
}
|
||||
|
||||
protected void logoutFromRealm(String realm) {
|
||||
driver.navigate().to(getAuthRoot()
|
||||
+ "/auth/realms/" + realm
|
||||
+ "/protocol/" + "openid-connect"
|
||||
+ "/logout?redirect_uri=" + encodeUrl(getAccountUrl(realm)));
|
||||
|
||||
try {
|
||||
Retry.execute(() -> {
|
||||
try {
|
||||
waitForPage(driver, "log in to " + realm);
|
||||
} catch (TimeoutException ex) {
|
||||
driver.navigate().refresh();
|
||||
log.debug("[Retriable] Timed out waiting for login page");
|
||||
throw ex;
|
||||
}
|
||||
}, 10, 100);
|
||||
} catch (TimeoutException e) {
|
||||
log.debug(driver.getTitle());
|
||||
log.debug(driver.getPageSource());
|
||||
Assert.fail("Timeout while waiting for login page");
|
||||
}
|
||||
}
|
||||
|
||||
protected void testSingleLogout() {
|
||||
log.debug("Testing single log out");
|
||||
|
@ -338,12 +273,4 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
|||
Assert.assertTrue("Should be on " + bc.consumerRealmName() + " realm on login page",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/protocol/openid-connect/"));
|
||||
}
|
||||
|
||||
private String getAccountUrl(String realmName) {
|
||||
return getAuthRoot() + "/auth/realms/" + realmName + "/account";
|
||||
}
|
||||
|
||||
private String getAccountPasswordUrl(String realmName) {
|
||||
return getAuthRoot() + "/auth/realms/" + realmName + "/account/password";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
|||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public abstract class AbstractUserAttributeMapperTest extends AbstractKeycloakTest {
|
||||
public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBrokerTest {
|
||||
|
||||
protected static final String MAPPED_ATTRIBUTE_NAME = "mapped-user-attribute";
|
||||
protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly";
|
||||
|
@ -55,42 +55,8 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractKeycloakTe
|
|||
.put(ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME)
|
||||
.build();
|
||||
|
||||
@Page
|
||||
protected LoginPage accountLoginPage;
|
||||
|
||||
@Page
|
||||
protected UpdateAccountInformationPage updateAccountInformationPage;
|
||||
|
||||
@Page
|
||||
protected AccountPasswordPage accountPasswordPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
||||
|
||||
protected BrokerConfiguration bc = getBrokerConfiguration();
|
||||
|
||||
protected String userId;
|
||||
|
||||
/**
|
||||
* Returns a broker configuration. Return value should not change between calls.
|
||||
* @return
|
||||
*/
|
||||
protected abstract BrokerConfiguration getBrokerConfiguration();
|
||||
|
||||
protected abstract Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers();
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation providerRealm = bc.createProviderRealm();
|
||||
RealmRepresentation consumerRealm = bc.createConsumerRealm();
|
||||
|
||||
testRealms.add(providerRealm);
|
||||
testRealms.add(consumerRealm);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void addIdentityProviderToConsumerRealm() {
|
||||
log.debug("adding identity provider to realm " + bc.consumerRealmName());
|
||||
|
@ -142,62 +108,6 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractKeycloakTe
|
|||
this.userId = createUserAndResetPasswordWithAdminClient(adminClient.realm(bc.providerRealmName()), user, bc.getUserPassword());
|
||||
}
|
||||
|
||||
private void logInAsUserInIDP() {
|
||||
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
|
||||
|
||||
log.debug("Clicking social " + bc.getIDPAlias());
|
||||
accountLoginPage.clickSocial(bc.getIDPAlias());
|
||||
|
||||
waitForPage(driver, "log in to");
|
||||
|
||||
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||
|
||||
log.debug("Logging in");
|
||||
accountLoginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
||||
}
|
||||
|
||||
/** Logs in the IDP and updates account information */
|
||||
private void logInAsUserInIDPForFirstTime() {
|
||||
logInAsUserInIDP();
|
||||
|
||||
waitForPage(driver, "update account information");
|
||||
|
||||
Assert.assertTrue(updateAccountInformationPage.isCurrent());
|
||||
Assert.assertTrue("We must be on correct realm right now",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||
|
||||
log.debug("Updating info on updateAccount page");
|
||||
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
|
||||
}
|
||||
|
||||
private String getAccountUrl(String realmName) {
|
||||
return BrokerTestTools.getAuthRoot(suiteContext) + "/auth/realms/" + realmName + "/account";
|
||||
}
|
||||
|
||||
private void logoutFromRealm(String realm) {
|
||||
driver.navigate().to(BrokerTestTools.getAuthRoot(suiteContext)
|
||||
+ "/auth/realms/" + realm
|
||||
+ "/protocol/" + "openid-connect"
|
||||
+ "/logout?redirect_uri=" + encodeUrl(getAccountUrl(realm)));
|
||||
|
||||
try {
|
||||
Retry.execute(() -> {
|
||||
try {
|
||||
waitForPage(driver, "log in to " + realm);
|
||||
} catch (TimeoutException ex) {
|
||||
driver.navigate().refresh();
|
||||
log.debug("[Retriable] Timed out waiting for login page");
|
||||
throw ex;
|
||||
}
|
||||
}, 10, 100);
|
||||
} catch (TimeoutException e) {
|
||||
log.debug(driver.getTitle());
|
||||
log.debug(driver.getPageSource());
|
||||
Assert.fail("Timeout while waiting for login page");
|
||||
}
|
||||
}
|
||||
|
||||
private UserRepresentation findUser(String realm, String userName, String email) {
|
||||
UsersResource consumerUsers = adminClient.realm(realm).users();
|
||||
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
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.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.client.resources.TestingCacheResource;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return KcOidcBrokerConfiguration.INSTANCE;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void createUser() {
|
||||
log.debug("creating user for realm " + bc.providerRealmName());
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(bc.getUserLogin());
|
||||
user.setEmail(bc.getUserEmail());
|
||||
user.setEmailVerified(true);
|
||||
user.setEnabled(true);
|
||||
|
||||
RealmResource realmResource = adminClient.realm(bc.providerRealmName());
|
||||
String userId = createUserWithAdminClient(realmResource, user);
|
||||
|
||||
resetUserPassword(realmResource.users().get(userId), bc.getUserPassword(), false);
|
||||
}
|
||||
|
||||
// TODO: Possibly move to parent superclass
|
||||
@Before
|
||||
public void addIdentityProviderToProviderRealm() {
|
||||
log.debug("adding identity provider to realm " + bc.consumerRealmName());
|
||||
|
||||
RealmResource realm = adminClient.realm(bc.consumerRealmName());
|
||||
realm.identityProviders().create(bc.setUpIdentityProvider(suiteContext));
|
||||
}
|
||||
|
||||
|
||||
@Before
|
||||
public void addClients() {
|
||||
List<ClientRepresentation> clients = bc.createProviderClients(suiteContext);
|
||||
if (clients != null) {
|
||||
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||
for (ClientRepresentation client : clients) {
|
||||
log.debug("adding client " + client.getName() + " to realm " + bc.providerRealmName());
|
||||
|
||||
providerRealm.clients().create(client);
|
||||
}
|
||||
}
|
||||
|
||||
clients = bc.createConsumerClients(suiteContext);
|
||||
if (clients != null) {
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
for (ClientRepresentation client : clients) {
|
||||
log.debug("adding client " + client.getName() + " to realm " + bc.consumerRealmName());
|
||||
|
||||
consumerRealm.clients().create(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationJwksUrl() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
|
||||
String jwksUrl = b.build(bc.providerRealmName()).toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys();
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
logInAsUserInIDP();
|
||||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Set time offset. New keys can be downloaded. Check that user is able to login.
|
||||
setTimeOffset(20);
|
||||
|
||||
logInAsUserInIDP();
|
||||
assertLoggedInAccountManagement();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKey() 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());
|
||||
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys();
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
logInAsUserInIDP();
|
||||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
|
||||
logoutFromRealm(bc.consumerRealmName());
|
||||
|
||||
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
|
||||
setTimeOffset(20);
|
||||
|
||||
logInAsUserInIDP();
|
||||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testClearKeysCache() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
IdentityProviderRepresentation idpRep = getIdentityProvider();
|
||||
OIDCIdentityProviderConfigRep cfg = new OIDCIdentityProviderConfigRep(idpRep);
|
||||
cfg.setValidateSignature(true);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT));
|
||||
String jwksUrl = b.build(bc.providerRealmName()).toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
updateIdentityProvider(idpRep);
|
||||
|
||||
// Check that user is able to login
|
||||
logInAsUserInIDPForFirstTime();
|
||||
assertLoggedInAccountManagement();
|
||||
|
||||
|
||||
// Check that key is cached
|
||||
String expectedCacheKey = consumerRealm().toRepresentation().getId() + "::idp::" + idpRep.getInternalId();
|
||||
TestingCacheResource cache = testingClient.testing(bc.consumerRealmName()).cache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
|
||||
Assert.assertTrue(cache.contains(expectedCacheKey));
|
||||
|
||||
// Clear cache and check nothing cached
|
||||
consumerRealm().clearKeysCache();
|
||||
Assert.assertFalse(cache.contains(expectedCacheKey));
|
||||
Assert.assertEquals(cache.size(), 0);
|
||||
}
|
||||
|
||||
|
||||
private void rotateKeys() {
|
||||
String activeKid = providerRealm().keys().getKeyMetadata().getActive().get("RSA");
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
String realmId = providerRealm().toRepresentation().getId();
|
||||
ComponentRepresentation keys = new ComponentRepresentation();
|
||||
keys.setName("generated");
|
||||
keys.setProviderType(KeyProvider.class.getName());
|
||||
keys.setProviderId("rsa-generated");
|
||||
keys.setParentId(realmId);
|
||||
keys.setConfig(new MultivaluedHashMap<>());
|
||||
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
|
||||
Response response = providerRealm().components().add(keys);
|
||||
assertEquals(201, response.getStatus());
|
||||
response.close();
|
||||
|
||||
String updatedActiveKid = providerRealm().keys().getKeyMetadata().getActive().get("RSA");
|
||||
assertNotEquals(activeKid, updatedActiveKid);
|
||||
}
|
||||
|
||||
|
||||
private RealmResource providerRealm() {
|
||||
return adminClient.realm(bc.providerRealmName());
|
||||
}
|
||||
|
||||
private IdentityProviderRepresentation getIdentityProvider() {
|
||||
return consumerRealm().identityProviders().get(BrokerTestConstants.IDP_OIDC_ALIAS).toRepresentation();
|
||||
}
|
||||
|
||||
private void updateIdentityProvider(IdentityProviderRepresentation rep) {
|
||||
consumerRealm().identityProviders().get(BrokerTestConstants.IDP_OIDC_ALIAS).update(rep);
|
||||
}
|
||||
|
||||
private RealmResource consumerRealm() {
|
||||
return adminClient.realm(bc.consumerRealmName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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.broker;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
|
||||
/**
|
||||
* Helper to avoid updating rep configuration with hardcoded constants
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
class OIDCIdentityProviderConfigRep extends OIDCIdentityProviderConfig {
|
||||
|
||||
private final IdentityProviderRepresentation rep;
|
||||
|
||||
public OIDCIdentityProviderConfigRep(IdentityProviderRepresentation rep) {
|
||||
super(null);
|
||||
this.rep = rep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getConfig() {
|
||||
return rep.getConfig();
|
||||
}
|
||||
}
|
|
@ -1,205 +0,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.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.broker;
|
||||
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.testsuite.ApiUtil;
|
||||
import org.keycloak.testsuite.Constants;
|
||||
import org.keycloak.testsuite.KeycloakServer;
|
||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest {
|
||||
|
||||
private static final int PORT = 8082;
|
||||
|
||||
private static Keycloak keycloak1;
|
||||
private static Keycloak keycloak2;
|
||||
|
||||
@ClassRule
|
||||
public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
|
||||
|
||||
@Override
|
||||
protected void configureServer(KeycloakServer server) {
|
||||
server.getConfig().setPort(PORT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
|
||||
server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getTestRealms() {
|
||||
return new String[] { "realm-with-oidc-identity-provider" };
|
||||
}
|
||||
};
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClazz() {
|
||||
keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
|
||||
keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBefore() {
|
||||
super.onBefore();
|
||||
|
||||
// Enable validate signatures
|
||||
IdentityProviderModel idpModel = getIdentityProviderModel();
|
||||
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
|
||||
cfg.setValidateSignature(true);
|
||||
getRealm().updateIdentityProvider(cfg);
|
||||
|
||||
brokerServerRule.stopSession(this.session, true);
|
||||
this.session = brokerServerRule.startSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderId() {
|
||||
return "kc-oidc-idp";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationJwksUrl() throws Exception {
|
||||
// Configure OIDC identity provider with JWKS URL
|
||||
IdentityProviderModel idpModel = getIdentityProviderModel();
|
||||
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
|
||||
cfg.setUseJwksUrl(true);
|
||||
|
||||
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT));
|
||||
String jwksUrl = b.build("realm-with-oidc-identity-provider").toString();
|
||||
cfg.setJwksUrl(jwksUrl);
|
||||
getRealm().updateIdentityProvider(cfg);
|
||||
|
||||
brokerServerRule.stopSession(this.session, true);
|
||||
this.session = brokerServerRule.startSession();
|
||||
|
||||
// Check that user is able to login
|
||||
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys("realm-with-oidc-identity-provider");
|
||||
|
||||
RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
|
||||
realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
|
||||
keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
loginIDP("test-user");
|
||||
assertTrue(errorPage.isCurrent());
|
||||
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
|
||||
|
||||
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
|
||||
|
||||
// Set time offset. New keys can be downloaded. Check that user is able to login.
|
||||
Time.setOffset(20);
|
||||
|
||||
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
|
||||
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignatureVerificationHardcodedPublicKey() throws Exception {
|
||||
// Configure OIDC identity provider with publicKeySignatureVerifier
|
||||
IdentityProviderModel idpModel = getIdentityProviderModel();
|
||||
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
|
||||
cfg.setUseJwksUrl(false);
|
||||
|
||||
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(keycloak2.realm("realm-with-oidc-identity-provider"));
|
||||
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
|
||||
getRealm().updateIdentityProvider(cfg);
|
||||
|
||||
brokerServerRule.stopSession(this.session, true);
|
||||
this.session = brokerServerRule.startSession();
|
||||
|
||||
// Check that user is able to login
|
||||
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
rotateKeys("realm-with-oidc-identity-provider");
|
||||
|
||||
// User not able to login now as new keys can't be yet downloaded (10s timeout)
|
||||
loginIDP("test-user");
|
||||
assertTrue(errorPage.isCurrent());
|
||||
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
|
||||
|
||||
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
|
||||
|
||||
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
|
||||
Time.setOffset(20);
|
||||
|
||||
loginIDP("test-user");
|
||||
assertTrue(errorPage.isCurrent());
|
||||
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
|
||||
|
||||
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
|
||||
|
||||
Time.setOffset(0);
|
||||
|
||||
}
|
||||
|
||||
private void rotateKeys(String realmName) {
|
||||
String activeKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
|
||||
|
||||
// Rotate public keys on the parent broker
|
||||
String realmId = keycloak2.realm(realmName).toRepresentation().getId();
|
||||
ComponentRepresentation keys = new ComponentRepresentation();
|
||||
keys.setName("generated");
|
||||
keys.setProviderType(KeyProvider.class.getName());
|
||||
keys.setProviderId("rsa-generated");
|
||||
keys.setParentId(realmId);
|
||||
keys.setConfig(new MultivaluedHashMap<>());
|
||||
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
|
||||
Response response = keycloak2.realm("realm-with-oidc-identity-provider").components().add(keys);
|
||||
assertEquals(201, response.getStatus());
|
||||
response.close();
|
||||
|
||||
String updatedActiveKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
|
||||
assertNotEquals(activeKid, updatedActiveKid);
|
||||
}
|
||||
|
||||
}
|
|
@ -72,6 +72,8 @@ realm-cache-clear=Realm Cache
|
|||
realm-cache-clear.tooltip=Clears all entries from the realm cache (this will clear entries for all realms)
|
||||
user-cache-clear=User Cache
|
||||
user-cache-clear.tooltip=Clears all entries from the user cache (this will clear entries for all realms)
|
||||
keys-cache-clear=Keys Cache
|
||||
keys-cache-clear.tooltip=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. (this wil clear entries for all realms)
|
||||
revoke-refresh-token=Revoke Refresh Token
|
||||
revoke-refresh-token.tooltip=If enabled refresh tokens can only be used once. Otherwise refresh tokens are not revoked when used and can be used multiple times.
|
||||
sso-session-idle=SSO Session Idle
|
||||
|
|
|
@ -426,7 +426,7 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
|
|||
$scope.$watch('realm.internationalizationEnabled', updateSupported);
|
||||
});
|
||||
|
||||
module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache, RealmClearRealmCache, Notifications) {
|
||||
module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache, RealmClearRealmCache, RealmClearKeysCache, Notifications) {
|
||||
$scope.realm = angular.copy(realm);
|
||||
|
||||
$scope.clearUserCache = function() {
|
||||
|
@ -441,6 +441,13 @@ module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache,
|
|||
});
|
||||
}
|
||||
|
||||
$scope.clearKeysCache = function() {
|
||||
RealmClearKeysCache.save({ realm: realm.realm}, function () {
|
||||
Notifications.success("Public keys cache cleared");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
module.controller('RealmPasswordPolicyCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, serverInfo) {
|
||||
|
|
|
@ -678,6 +678,12 @@ module.factory('RealmClearRealmCache', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('RealmClearKeysCache', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clear-keys-cache', {
|
||||
realm : '@realm'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('RealmSessionStats', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/session-stats', {
|
||||
realm : '@realm'
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'user-cache-clear.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label">{{:: 'keys-cache-clear' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<button type="submit" data-ng-click="clearKeysCache()" class="btn btn-default">{{:: 'clear' | translate}}</button>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'keys-cache-clear.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in a new issue