keycloak-memoizeit
Changes
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java 14(+2 -12)
model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java 15(+14 -1)
model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java 14(+13 -1)
model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java 205(+0 -205)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java 73(+73 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 12(+5 -7)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 7(+3 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java 10(+6 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java 166(+166 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java 79(+3 -76)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java 92(+1 -91)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java 248(+248 -0)
Details
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index 85e6689..42b6196 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -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)
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
index 57cc003..fa73420 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -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);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
index 217a421..52d47df 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
@@ -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) {
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
index c0606b1..8f2e321 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
@@ -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();
+
+ });
}
}
}
diff --git a/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java b/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
index e5dd1c1..030e5a0 100644
--- a/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
@@ -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));
}
diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
index 41a1e41..083ec42 100755
--- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
@@ -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() {
diff --git a/server-spi-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java
index 190ae8d..1d72180 100644
--- a/server-spi-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java
@@ -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();
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index f71c6af..55427f7 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -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);
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java
new file mode 100644
index 0000000..be531aa
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java
@@ -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();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index f2b530a..75838c5 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -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() {
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java
new file mode 100644
index 0000000..946d0f5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java
@@ -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();
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index e19653a..68a5836 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -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")
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
index d793a8c..26cf87b 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
@@ -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();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java
new file mode 100644
index 0000000..217a4e7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java
@@ -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());
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
index b32b94c..8950d1b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
@@ -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";
- }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
index fb74fce..2e5c4c6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
@@ -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();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java
new file mode 100644
index 0000000..5e6b30d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java
@@ -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());
+ }
+
+
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OIDCIdentityProviderConfigRep.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OIDCIdentityProviderConfigRep.java
new file mode 100644
index 0000000..da8622f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OIDCIdentityProviderConfigRep.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.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();
+ }
+}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 6c696c9..da4e50b 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index ac19dac..5e6bdba 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -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) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index abacb07..6aa04c4 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -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'
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
index 29d978e..53a7987 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
@@ -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>