From 7c6032cc84a60b23b21211e1c57a0eecf737a181 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 25 Nov 2016 13:18:03 +0100 Subject: [PATCH] KEYCLOAK-3825 Ability to expire publicKeys cache. Migrated OIDCBrokerWithSignatureTest to new testsuite --- .../admin/client/resource/RealmResource.java | 4 + .../InfinispanNotificationsManager.java | 14 +- .../InfinispanPublicKeyStorageProvider.java | 15 +- ...nispanPublicKeyStorageProviderFactory.java | 14 +- .../InfinispanKeyStorageProviderTest.java | 2 +- .../keys/PublicKeyStorageProvider.java | 5 + .../models/IdentityProviderModel.java | 26 +- .../resources/admin/RealmAdminResource.java | 18 ++ .../rest/TestingResourceProvider.java | 12 +- .../rest/resource/TestCacheResource.java | 73 ++++++ .../resources/TestingCacheResource.java | 51 ++++ .../client/resources/TestingResource.java | 7 +- .../testsuite/admin/realm/RealmTest.java | 10 +- .../broker/AbstractBaseBrokerTest.java | 166 ++++++++++++ .../testsuite/broker/AbstractBrokerTest.java | 79 +----- .../AbstractUserAttributeMapperTest.java | 92 +------ .../broker/KcOIDCBrokerWithSignatureTest.java | 248 ++++++++++++++++++ .../broker/OIDCIdentityProviderConfigRep.java | 43 +++ ...KeycloakServerBrokerWithSignatureTest.java | 205 --------------- .../messages/admin-messages_en.properties | 2 + .../admin/resources/js/controllers/realm.js | 9 +- .../theme/base/admin/resources/js/services.js | 6 + .../partials/realm-cache-settings.html | 7 + 23 files changed, 693 insertions(+), 415 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/resource/TestCacheResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingCacheResource.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OIDCIdentityProviderConfigRep.java delete mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java 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 85e66899ff..42b619655e 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 57cc003a53..fa73420ebb 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 217a421178..52d47dffe3 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 keys; private final Map> tasksInProgress; private final int minTimeBetweenRequests ; - public InfinispanPublicKeyStorageProvider(Cache keys, Map> tasksInProgress, int minTimeBetweenRequests) { + public InfinispanPublicKeyStorageProvider(KeycloakSession session, Cache keys, Map> 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 c0606b1c6b..8f2e321b0e 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 Marek Posolda @@ -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 keysCache; private final Map> 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 e5dd1c1f56..030e5a0f33 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-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java b/server-spi-private/src/main/java/org/keycloak/keys/PublicKeyStorageProvider.java index 190ae8de9d..1d72180679 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/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java index 41a1e41fee..083ec42aff 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(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(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/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index f71c6af721..55427f7c53 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/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index f2b530a139..75838c597c 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 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/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 0000000000..be531aa28d --- /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 Marek Posolda + */ +public class TestCacheResource { + + private final Cache 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 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/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 0000000000..946d0f54e8 --- /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 Marek Posolda + */ +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 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 e19653a08e..68a5836721 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 d793a8c6c5..26cf87b153 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 0000000000..217a4e7550 --- /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 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 b32b94c0d0..8950d1b087 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 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 fb74fcecbd..2e5c4c6789 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 createIdentityProviderMappers(); - @Override - public void addTestRealms(List 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 0000000000..5e6b30d4a3 --- /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 Marek Posolda + */ +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 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 0000000000..da8622fa38 --- /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 Marek Posolda + */ +class OIDCIdentityProviderConfigRep extends OIDCIdentityProviderConfig { + + private final IdentityProviderRepresentation rep; + + public OIDCIdentityProviderConfigRep(IdentityProviderRepresentation rep) { + super(null); + this.rep = rep; + } + + @Override + public Map getConfig() { + return rep.getConfig(); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java deleted file mode 100644 index 28c6625b93..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithSignatureTest.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite.broker; - - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; - -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.Time; -import org.keycloak.keys.KeyProvider; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocolService; -import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.representations.idm.KeysMetadataRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.testsuite.ApiUtil; -import org.keycloak.testsuite.Constants; -import org.keycloak.testsuite.KeycloakServer; -import org.keycloak.testsuite.rule.AbstractKeycloakRule; - -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -/** - * @author Marek Posolda - */ -public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest { - - private static final int PORT = 8082; - - private static Keycloak keycloak1; - private static Keycloak keycloak2; - - @ClassRule - public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { - - @Override - protected void configureServer(KeycloakServer server) { - server.getConfig().setPort(PORT); - } - - @Override - protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { - server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json")); - } - - @Override - protected String[] getTestRealms() { - return new String[] { "realm-with-oidc-identity-provider" }; - } - }; - - @BeforeClass - public static void beforeClazz() { - keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); - keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); - } - - @Override - public void onBefore() { - super.onBefore(); - - // Enable validate signatures - IdentityProviderModel idpModel = getIdentityProviderModel(); - OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel); - cfg.setValidateSignature(true); - getRealm().updateIdentityProvider(cfg); - - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - } - - @Override - protected String getProviderId() { - return "kc-oidc-idp"; - } - - @Test - public void testSignatureVerificationJwksUrl() throws Exception { - // Configure OIDC identity provider with JWKS URL - IdentityProviderModel idpModel = getIdentityProviderModel(); - OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel); - cfg.setUseJwksUrl(true); - - UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT)); - String jwksUrl = b.build("realm-with-oidc-identity-provider").toString(); - cfg.setJwksUrl(jwksUrl); - getRealm().updateIdentityProvider(cfg); - - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - // Check that user is able to login - assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false); - - // Rotate public keys on the parent broker - rotateKeys("realm-with-oidc-identity-provider"); - - RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation(); - realm.setPublicKey(org.keycloak.models.Constants.GENERATE); - keycloak2.realm("realm-with-oidc-identity-provider").update(realm); - - // User not able to login now as new keys can't be yet downloaded (10s timeout) - loginIDP("test-user"); - assertTrue(errorPage.isCurrent()); - assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError()); - - keycloak2.realm("realm-with-oidc-identity-provider").logoutAll(); - - // Set time offset. New keys can be downloaded. Check that user is able to login. - Time.setOffset(20); - - assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false); - - Time.setOffset(0); - } - - @Test - public void testSignatureVerificationHardcodedPublicKey() throws Exception { - // Configure OIDC identity provider with publicKeySignatureVerifier - IdentityProviderModel idpModel = getIdentityProviderModel(); - OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel); - cfg.setUseJwksUrl(false); - - KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(keycloak2.realm("realm-with-oidc-identity-provider")); - cfg.setPublicKeySignatureVerifier(key.getPublicKey()); - getRealm().updateIdentityProvider(cfg); - - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - // Check that user is able to login - assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false); - - // Rotate public keys on the parent broker - rotateKeys("realm-with-oidc-identity-provider"); - - // User not able to login now as new keys can't be yet downloaded (10s timeout) - loginIDP("test-user"); - assertTrue(errorPage.isCurrent()); - assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError()); - - keycloak2.realm("realm-with-oidc-identity-provider").logoutAll(); - - // Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config - Time.setOffset(20); - - loginIDP("test-user"); - assertTrue(errorPage.isCurrent()); - assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError()); - - keycloak2.realm("realm-with-oidc-identity-provider").logoutAll(); - - Time.setOffset(0); - - } - - private void rotateKeys(String realmName) { - String activeKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA"); - - // Rotate public keys on the parent broker - String realmId = keycloak2.realm(realmName).toRepresentation().getId(); - ComponentRepresentation keys = new ComponentRepresentation(); - keys.setName("generated"); - keys.setProviderType(KeyProvider.class.getName()); - keys.setProviderId("rsa-generated"); - keys.setParentId(realmId); - keys.setConfig(new MultivaluedHashMap<>()); - keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis())); - Response response = keycloak2.realm("realm-with-oidc-identity-provider").components().add(keys); - assertEquals(201, response.getStatus()); - response.close(); - - String updatedActiveKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA"); - assertNotEquals(activeKid, updatedActiveKid); - } - -} 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 6c696c9261..da4e50baae 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 ac19dacd4e..5e6bdbaace 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 abacb079c2..6aa04c41b6 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 29d978e3db..53a7987f1e 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 @@ {{:: 'user-cache-clear.tooltip' | translate}} +
+ +
+ +
+ {{:: 'keys-cache-clear.tooltip' | translate}} +