Split PublicKeyStorageProvider (#12897)

Split PublicKeyStorageProvider

- Extract clearCache() method to separate interface and move it to the legacy module
- Make PublicKeyProvider factories environment dependent
- Simple map storage for public keys that just delegates

Resolves #12763

Co-authored-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Alexander Schwartz 2022-07-05 14:57:51 +02:00 committed by GitHub
parent 63614b1240
commit 098d4dda0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 668 additions and 56 deletions

View file

@ -0,0 +1,48 @@
/*
* Copyright 2022 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.keys.infinispan;
import org.infinispan.Cache;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.cache.CachePublicKeyProvider;
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
public class InfinispanCachePublicKeyProvider implements CachePublicKeyProvider {
private final KeycloakSession session;
private final Cache<String, PublicKeysEntry> keys;
public InfinispanCachePublicKeyProvider(KeycloakSession session, Cache<String, PublicKeysEntry> keys) {
this.session = session;
this.keys = keys;
}
@Override
public void clearCache() {
keys.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanCachePublicKeyProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright 2022 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.keys.infinispan;
import org.infinispan.Cache;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.Profile;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.CachePublicKeyProvider;
import org.keycloak.models.cache.CachePublicKeyProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
public class InfinispanCachePublicKeyProviderFactory implements CachePublicKeyProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "infinispan";
public static final String PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT";
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
private volatile Cache<String, PublicKeysEntry> keysCache;
@Override
public CachePublicKeyProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanCachePublicKeyProvider(session, keysCache);
}
private void lazyInit(KeycloakSession session) {
if (keysCache == null) {
synchronized (this) {
if (keysCache == null) {
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, (ClusterEvent event) -> {
PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
keysCache.remove(invalidationEvent.getCacheKey());
});
cluster.registerListener(KEYS_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
keysCache.clear();
});
}
}
}
}
@Override
public boolean isSupported() {
return !Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -34,7 +34,6 @@ import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
/**
@ -63,15 +62,6 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
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, ClusterProvider.DCNotify.ALL_DCS);
}
void addInvalidation(String cacheKey) {
if (!transactionEnlisted) {
session.getTransactionManager().enlistAfterCompletion(getAfterTransaction());
@ -121,7 +111,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
for (String cacheKey : invalidations) {
keys.remove(cacheKey);
cluster.notify(InfinispanPublicKeyStorageProviderFactory.PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, PublicKeyStorageInvalidationEvent.create(cacheKey), true, ClusterProvider.DCNotify.ALL_DCS);
cluster.notify(InfinispanCachePublicKeyProviderFactory.PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, PublicKeyStorageInvalidationEvent.create(cacheKey), true, ClusterProvider.DCNotify.ALL_DCS);
}
}

View file

@ -25,8 +25,7 @@ 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.common.Profile;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyStorageProvider;
@ -36,22 +35,19 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStorageProviderFactory {
public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStorageProviderFactory, EnvironmentDependentProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanPublicKeyStorageProviderFactory.class);
public static final String PROVIDER_ID = "infinispan";
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
public static final String PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT";
private volatile Cache<String, PublicKeysEntry> keysCache;
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
@ -69,20 +65,6 @@ 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(PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, (ClusterEvent event) -> {
PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
keysCache.remove(invalidationEvent.getCacheKey());
});
cluster.registerListener(KEYS_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
keysCache.clear();
});
}
}
}
@ -147,6 +129,11 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
}
}
@Override
public boolean isSupported() {
return !Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
private static class SessionAndKeyHolder {
private final KeycloakSession session;
private final ArrayList<String> cacheKeys;

View file

@ -0,0 +1,18 @@
#
# 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.
#
org.keycloak.keys.infinispan.InfinispanCachePublicKeyProviderFactory

View file

@ -0,0 +1,29 @@
/*
* Copyright 2022 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.models.cache;
import org.keycloak.provider.Provider;
public interface CachePublicKeyProvider extends Provider {
/**
* Clears all the cached public keys, so they need to be loaded again
*/
void clearCache();
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2022 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.models.cache;
import org.keycloak.provider.ProviderFactory;
public interface CachePublicKeyProviderFactory extends ProviderFactory<CachePublicKeyProvider> {
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2022 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.models.cache;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class CachePublicKeyProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "publicKeyCache";
}
@Override
public Class<? extends Provider> getProviderClass() {
return CachePublicKeyProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return CachePublicKeyProviderFactory.class;
}
}

View file

@ -17,6 +17,7 @@
org.keycloak.models.cache.CacheUserProviderSpi
org.keycloak.models.cache.CacheRealmProviderSpi
org.keycloak.models.cache.CachePublicKeyProviderSpi
org.keycloak.storage.client.ClientStorageProviderSpi
org.keycloak.storage.group.GroupStorageProviderSpi
org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi

View file

@ -0,0 +1,59 @@
/*
* Copyright 2022 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.services.resources.admin;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
public class ClearKeysCacheRealmAdminProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
@Override
public AdminRealmResourceProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "clear-keys-cache";
}
@Override
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
return new ClearKeysCacheResource(session, realm, auth, adminEvent);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2022 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.services.resources.admin;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.CachePublicKeyProvider;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
public class ClearKeysCacheResource {
protected AdminPermissionEvaluator auth;
protected RealmModel realm;
private AdminEventBuilder adminEvent;
@Context
protected KeycloakSession session;
public ClearKeysCacheResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent;
this.session = session;
}
/**
* Clear cache of external public keys (Public keys of clients or Identity providers)
*
*/
@POST
public void clearKeysCache() {
auth.realm().requireManageRealm();
CachePublicKeyProvider cache = session.getProvider(CachePublicKeyProvider.class);
if (cache != null) {
cache.clearCache();
}
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
}
}

View file

@ -18,3 +18,4 @@
org.keycloak.services.resources.admin.UserStorageProviderRealmAdminProvider
org.keycloak.services.resources.admin.ClearUserCacheRealmAdminProvider
org.keycloak.services.resources.admin.ClearRealmCacheRealmAdminProvider
org.keycloak.services.resources.admin.ClearKeysCacheRealmAdminProvider

View file

@ -0,0 +1,136 @@
/*
* Copyright 2022 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.models.map.keys;
import org.jboss.logging.Logger;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.models.KeycloakSession;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MapPublicKeyStorageProvider implements PublicKeyStorageProvider {
private static final Logger log = Logger.getLogger(MapPublicKeyStorageProvider.class);
private final KeycloakSession session;
private final Map<String, FutureTask<Map<String, KeyWrapper>>> tasksInProgress;
public MapPublicKeyStorageProvider(KeycloakSession session, Map<String, FutureTask<Map<String, KeyWrapper>>> tasksInProgress) {
this.session = session;
this.tasksInProgress = tasksInProgress;
}
@Override
public KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
return getPublicKey(modelKey, kid, null, loader);
}
@Override
public KeyWrapper getFirstPublicKey(String modelKey, String algorithm, PublicKeyLoader loader) {
return getPublicKey(modelKey, null, algorithm, loader);
}
private KeyWrapper getPublicKey(String modelKey, String kid, String algorithm, PublicKeyLoader loader) {
WrapperCallable wrapperCallable = new WrapperCallable(modelKey, loader);
FutureTask<Map<String, KeyWrapper>> task = new FutureTask<>(wrapperCallable);
FutureTask<Map<String, KeyWrapper>> existing = tasksInProgress.putIfAbsent(modelKey, task);
Map<String, KeyWrapper> currentKeys;
if (existing == null) {
task.run();
} else {
task = existing;
}
try {
currentKeys = task.get();
// Computation finished. Let's see if key is available
KeyWrapper publicKey = algorithm != null ? getPublicKeyByAlg(currentKeys, algorithm) : getPublicKey(currentKeys, kid);
if (publicKey != null) {
return publicKey;
}
} catch (ExecutionException ee) {
throw new RuntimeException("Error when loading public keys: " + ee.getMessage(), ee);
} catch (InterruptedException ie) {
throw new RuntimeException("Error. Interrupted when loading public keys", ie);
} finally {
// Our thread inserted the task. Let's clean
if (existing == null) {
tasksInProgress.remove(modelKey);
}
}
Set<String> availableKids = currentKeys == null ? Collections.emptySet() : currentKeys.keySet();
log.warnf("PublicKey wasn't found in the storage. Requested kid: '%s' . Available kids: '%s'", kid, availableKids);
return null;
}
private KeyWrapper getPublicKey(Map<String, KeyWrapper> publicKeys, String kid) {
// Backwards compatibility
if (kid == null && !publicKeys.isEmpty()) {
return publicKeys.values().iterator().next();
} else {
return publicKeys.get(kid);
}
}
private KeyWrapper getPublicKeyByAlg(Map<String, KeyWrapper> publicKeys, String algorithm) {
if (algorithm == null) return null;
for (KeyWrapper keyWrapper : publicKeys.values())
if (algorithm.equals(keyWrapper.getAlgorithmOrDefault())) return keyWrapper;
return null;
}
private class WrapperCallable implements Callable<Map<String, KeyWrapper>> {
private final String modelKey;
private final PublicKeyLoader delegate;
public WrapperCallable(String modelKey, PublicKeyLoader delegate) {
this.modelKey = modelKey;
this.delegate = delegate;
}
@Override
public Map<String, KeyWrapper> call() throws Exception {
Map<String, KeyWrapper> publicKeys = delegate.loadKeys();
if (log.isDebugEnabled()) {
log.debugf("Public keys retrieved successfully for model %s. New kids: %s", modelKey, publicKeys.keySet().toString());
}
return publicKeys;
}
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2022 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.models.map.keys;
import org.keycloak.common.Profile;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.keys.PublicKeyStorageProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
public class MapPublicKeyStorageProviderFactory extends AbstractMapProviderFactory<MapPublicKeyStorageProvider, AbstractEntity, Object>
implements PublicKeyStorageProviderFactory<MapPublicKeyStorageProvider>, EnvironmentDependentProviderFactory {
private final Map<String, FutureTask<Map<String, KeyWrapper>>> tasksInProgress = new ConcurrentHashMap<>();
public MapPublicKeyStorageProviderFactory() {
super(Object.class, MapPublicKeyStorageProvider.class);
}
@Override
public MapPublicKeyStorageProvider createNew(KeycloakSession session) {
return new MapPublicKeyStorageProvider(session, tasksInProgress);
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
@Override
public String getHelpText() {
return "Public key storage provider";
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2022 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.
#
org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory

View file

@ -47,9 +47,4 @@ public interface PublicKeyStorageProvider extends Provider {
*/
KeyWrapper getFirstPublicKey(String modelKey, String algorithm, PublicKeyLoader loader);
/**
* Clears all the cached public keys, so they need to be loaded again
*/
void clearCache();
}

View file

@ -22,5 +22,5 @@ import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface PublicKeyStorageProviderFactory extends ProviderFactory<PublicKeyStorageProvider> {
public interface PublicKeyStorageProviderFactory<T extends PublicKeyStorageProvider> extends ProviderFactory<T> {
}

View file

@ -1079,23 +1079,6 @@ public class RealmAdminResource {
return stripForExport(session, rep);
}
/**
* Clear cache of external public keys (Public keys of clients or Identity providers)
*
*/
@Path("clear-keys-cache")
@POST
public void clearKeysCache() {
auth.realm().requireManageRealm();
PublicKeyStorageProvider cache = session.getProvider(PublicKeyStorageProvider.class);
if (cache != null) {
cache.clearCache();
}
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
}
@Path("keys")
public KeyResource keys() {
KeyResource resource = new KeyResource(realm, session, this.auth);

View file

@ -849,9 +849,11 @@
<keycloak.eventsStore.provider>map</keycloak.eventsStore.provider>
<keycloak.actionToken.provider>map</keycloak.actionToken.provider>
<keycloak.singleUseObject.provider>map</keycloak.singleUseObject.provider>
<keycloak.publicKeyStorage.provider>map</keycloak.publicKeyStorage.provider>
<keycloak.authorizationCache.enabled>false</keycloak.authorizationCache.enabled>
<keycloak.realmCache.enabled>false</keycloak.realmCache.enabled>
<keycloak.userCache.enabled>false</keycloak.userCache.enabled>
<keycloak.publicKeyCache.enabled>false</keycloak.publicKeyCache.enabled>
<keycloak.userSessionPersister.provider></keycloak.userSessionPersister.provider>
</systemPropertyVariables>
</configuration>

View file

@ -26,6 +26,7 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.common.util.*;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.crypto.Algorithm;
@ -39,6 +40,7 @@ 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.ProfileAssume;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestingCacheResource;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
@ -95,6 +97,8 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
@Test
public void testSignatureVerificationJwksUrl() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
// Configure OIDC identity provider with JWKS URL
updateIdentityProviderWithJwksUrl();
@ -303,6 +307,8 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
@Test
public void testClearKeysCache() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
// Configure OIDC identity provider with JWKS URL
updateIdentityProviderWithJwksUrl();
@ -328,6 +334,8 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
// Test that when I update identityProvier, then the record in publicKey cache is cleared and it's not possible to authenticate with it anymore
@Test
public void testPublicKeyCacheInvalidatedWhenProviderUpdated() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
// Configure OIDC identity provider with JWKS URL
updateIdentityProviderWithJwksUrl();

View file

@ -40,6 +40,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
import org.keycloak.client.registration.Auth;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.constants.ServiceUrlConstants;
@ -58,6 +59,7 @@ import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
@ -179,6 +181,8 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
@Test
public void testTwoClientsWithSameKid() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
// Create client with manually set "kid"
OIDCClientRepresentation response = createClientWithManuallySetKid("a1");
@ -218,6 +222,8 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
@Test
public void testPublicKeyCacheInvalidatedWhenUpdatingClient() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
OIDCClientRepresentation response = createClientWithManuallySetKid("a1");
Map<String, String> generatedKeys = testingClient.testApp().oidcClientEndpoints().getKeysAsPem();
@ -271,6 +277,8 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
@Test
public void createClientWithJWKSURI_rotateClientKeys() throws Exception {
ProfileAssume.assumeFeatureDisabled(Profile.Feature.MAP_STORAGE);
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));

View file

@ -160,6 +160,15 @@
}
},
"publicKeyStorage": {
"provider": "${keycloak.publicKeyStorage.provider:infinispan}",
"map": {
"storage": {
"provider": "${keycloak.publicKeyStorage.map.storage.provider:concurrenthashmap}"
}
}
},
"mapStorage": {
"concurrenthashmap": {
"dir": "${project.build.directory:target}",
@ -209,6 +218,12 @@
}
},
"publicKeyCache": {
"default" : {
"enabled": "${keycloak.publicKeyCache.enabled:true}"
}
},
"timer": {
"provider": "basic"
},

View file

@ -32,6 +32,7 @@ import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
import org.keycloak.models.map.events.MapEventStoreProviderFactory;
import org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory;
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProviderFactory;
@ -78,6 +79,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)

View file

@ -19,8 +19,12 @@ package org.keycloak.testsuite.model.parameters;
import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
import org.keycloak.keys.PublicKeyStorageSpi;
import org.keycloak.keys.infinispan.InfinispanCachePublicKeyProviderFactory;
import org.keycloak.keys.infinispan.InfinispanPublicKeyStorageProviderFactory;
import org.keycloak.models.ActionTokenStoreSpi;
import org.keycloak.models.SingleUseObjectSpi;
import org.keycloak.models.cache.CachePublicKeyProviderSpi;
import org.keycloak.models.session.UserSessionPersisterSpi;
import org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory;
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
@ -63,6 +67,8 @@ public class Infinispan extends KeycloakModelParameters {
.add(UserSessionPersisterSpi.class)
.add(ActionTokenStoreSpi.class)
.add(SingleUseObjectSpi.class)
.add(PublicKeyStorageSpi.class)
.add(CachePublicKeyProviderSpi.class)
.add(LegacySessionSupportSpi.class) // necessary as it will call session.userCredentialManager().onCache()
@ -80,6 +86,8 @@ public class Infinispan extends KeycloakModelParameters {
.add(InfinispanSingleUseObjectProviderFactory.class)
.add(StickySessionEncoderProviderFactory.class)
.add(TimerProviderFactory.class)
.add(InfinispanPublicKeyStorageProviderFactory.class)
.add(InfinispanCachePublicKeyProviderFactory.class)
.add(LegacySessionSupportProviderFactory.class)
.build();

View file

@ -33,6 +33,7 @@ import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
import org.keycloak.models.map.deploymentState.MapDeploymentStateProviderFactory;
import org.keycloak.models.map.group.MapGroupProviderFactory;
import org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.realm.MapRealmProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory;
@ -99,6 +100,7 @@ public class JpaMapStorage extends KeycloakModelParameters {
.spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.config("storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)

View file

@ -105,7 +105,8 @@ public class LdapMapStorage extends KeycloakModelParameters {
.spi(EventStoreSpi.NAME).config("map.storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(ActionTokenStoreSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(SingleUseObjectSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
.spi(SingleUseObjectSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("publicKeyStorage").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
}

View file

@ -18,6 +18,7 @@ package org.keycloak.testsuite.model.parameters;
import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.keys.PublicKeyStorageSpi;
import org.keycloak.models.ActionTokenStoreProviderFactory;
import org.keycloak.models.ActionTokenStoreSpi;
import org.keycloak.models.DeploymentStateSpi;
@ -29,6 +30,7 @@ import org.keycloak.models.dblock.NoLockingDBLockProviderFactory;
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.events.MapEventStoreProviderFactory;
import org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
@ -59,6 +61,7 @@ public class Map extends KeycloakModelParameters {
.add(AuthenticationSessionSpi.class)
.add(ActionTokenStoreSpi.class)
.add(SingleUseObjectSpi.class)
.add(PublicKeyStorageSpi.class)
.add(MapStorageSpi.class)
.build();
@ -79,6 +82,7 @@ public class Map extends KeycloakModelParameters {
.add(MapEventStoreProviderFactory.class)
.add(ActionTokenStoreProviderFactory.class)
.add(SingleUseObjectProviderFactory.class)
.add(MapPublicKeyStorageProviderFactory.class)
.build();
public Map() {
@ -102,6 +106,7 @@ public class Map extends KeycloakModelParameters {
.spi(UserLoginFailureSpi.NAME).defaultProvider(MapUserLoginFailureProviderFactory.PROVIDER_ID)
.spi("dblock").defaultProvider(NoLockingDBLockProviderFactory.PROVIDER_ID)
.spi(EventStoreSpi.NAME).defaultProvider(MapEventStoreProviderFactory.PROVIDER_ID)
.spi("publicKeyStorage").defaultProvider(MapPublicKeyStorageProviderFactory.PROVIDER_ID)
;
cf.spi(MapStorageSpi.NAME).provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).config("keyType.single-use-objects", "string");
}

View file

@ -321,9 +321,11 @@
<systemProperty><key>keycloak.actionToken.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.singleUseObject.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.eventsStore.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.publicKeyStorage.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.authorizationCache.enabled</key><value>false</value></systemProperty>
<systemProperty><key>keycloak.realmCache.enabled</key><value>false</value></systemProperty>
<systemProperty><key>keycloak.userCache.enabled</key><value>false</value></systemProperty>
<systemProperty><key>keycloak.publicKeyCache.enabled</key><value>false</value></systemProperty>
</systemProperties>
</configuration>
</plugin>

View file

@ -132,6 +132,15 @@
}
},
"publicKeyStorage": {
"provider": "${keycloak.publicKeyStorage.provider:infinispan}",
"map": {
"storage": {
"provider": "${keycloak.publicKeyStorage.map.storage.provider:concurrenthashmap}"
}
}
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:}",
"concurrenthashmap": {
@ -234,6 +243,12 @@
}
},
"publicKeyCache": {
"default" : {
"enabled": "${keycloak.publicKeyCache.enabled:true}"
}
},
"authorizationCache": {
"default": {
"enabled": "${keycloak.authorizationCache.enabled:true}"