keycloak-memoizeit

Merge pull request #3550 from mposolda/master KEYCLOAK-3825

11/25/2016 5:23:57 PM

Changes

testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java 205(+0 -205)

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>