Merge pull request #3550 from mposolda/master

KEYCLOAK-3825 Ability to expire publicKeys cache. Migrated OIDCBroker…
This commit is contained in:
Marek Posolda 2016-11-25 20:23:57 +01:00 committed by GitHub
commit 7db06b8afb
23 changed files with 693 additions and 415 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,43 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.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();
}
}

View file

@ -1,205 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApiUtil;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCKeycloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest {
private static final int PORT = 8082;
private static Keycloak keycloak1;
private static Keycloak keycloak2;
@ClassRule
public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() {
@Override
protected void configureServer(KeycloakServer server) {
server.getConfig().setPort(PORT);
}
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json"));
}
@Override
protected String[] getTestRealms() {
return new String[] { "realm-with-oidc-identity-provider" };
}
};
@BeforeClass
public static void beforeClazz() {
keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
}
@Override
public void onBefore() {
super.onBefore();
// Enable validate signatures
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setValidateSignature(true);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
}
@Override
protected String getProviderId() {
return "kc-oidc-idp";
}
@Test
public void testSignatureVerificationJwksUrl() throws Exception {
// Configure OIDC identity provider with JWKS URL
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(true);
UriBuilder b = OIDCLoginProtocolService.certsUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT).port(PORT));
String jwksUrl = b.build("realm-with-oidc-identity-provider").toString();
cfg.setJwksUrl(jwksUrl);
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
rotateKeys("realm-with-oidc-identity-provider");
RealmRepresentation realm = keycloak2.realm("realm-with-oidc-identity-provider").toRepresentation();
realm.setPublicKey(org.keycloak.models.Constants.GENERATE);
keycloak2.realm("realm-with-oidc-identity-provider").update(realm);
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Set time offset. New keys can be downloaded. Check that user is able to login.
Time.setOffset(20);
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
Time.setOffset(0);
}
@Test
public void testSignatureVerificationHardcodedPublicKey() throws Exception {
// Configure OIDC identity provider with publicKeySignatureVerifier
IdentityProviderModel idpModel = getIdentityProviderModel();
OIDCIdentityProviderConfig cfg = new OIDCIdentityProviderConfig(idpModel);
cfg.setUseJwksUrl(false);
KeysMetadataRepresentation.KeyMetadataRepresentation key = ApiUtil.findActiveKey(keycloak2.realm("realm-with-oidc-identity-provider"));
cfg.setPublicKeySignatureVerifier(key.getPublicKey());
getRealm().updateIdentityProvider(cfg);
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
// Check that user is able to login
assertSuccessfulAuthentication(getIdentityProviderModel(), "test-user", "test-user@localhost", false);
// Rotate public keys on the parent broker
rotateKeys("realm-with-oidc-identity-provider");
// User not able to login now as new keys can't be yet downloaded (10s timeout)
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
Time.setOffset(20);
loginIDP("test-user");
assertTrue(errorPage.isCurrent());
assertEquals("Unexpected error when authenticating with identity provider", errorPage.getError());
keycloak2.realm("realm-with-oidc-identity-provider").logoutAll();
Time.setOffset(0);
}
private void rotateKeys(String realmName) {
String activeKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
// Rotate public keys on the parent broker
String realmId = keycloak2.realm(realmName).toRepresentation().getId();
ComponentRepresentation keys = new ComponentRepresentation();
keys.setName("generated");
keys.setProviderType(KeyProvider.class.getName());
keys.setProviderId("rsa-generated");
keys.setParentId(realmId);
keys.setConfig(new MultivaluedHashMap<>());
keys.getConfig().putSingle("priority", Long.toString(System.currentTimeMillis()));
Response response = keycloak2.realm("realm-with-oidc-identity-provider").components().add(keys);
assertEquals(201, response.getStatus());
response.close();
String updatedActiveKid = keycloak2.realm("realm-with-oidc-identity-provider").keys().getKeyMetadata().getActive().get("RSA");
assertNotEquals(activeKid, updatedActiveKid);
}
}

View file

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

View file

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

View file

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

View file

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