mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Add crl cache to certificate validation
Closes #26473 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
f89be1813d
commit
6cf92d9dc7
@ -8,3 +8,9 @@ A `direction` query parameter was also added, allowing controlling the order of
|
||||
|
||||
Finally, the returned event representations now also include the `id`, which provides a unique identifier for
|
||||
an event.
|
||||
|
||||
= New cache for CRLs loaded for the X.509 authenticator
|
||||
|
||||
Now the Certificate Revocation Lists (CRL), that are used to validate certificates in the X.509 authenticator, are cached inside a new infinispan cache called `crl`. Caching improves the validation performance and decreases the memory consumption because just one CRL is maintained per source.
|
||||
|
||||
Check the `crl-storage` section in the link:https://www.keycloak.org/server/all-provider-config[All provider configuration] {section} to know the options for the new cache provider.
|
||||
|
||||
@ -40,6 +40,7 @@ You configure these caches in `conf/cache-ispn.xml`:
|
||||
|users|Local|Cache persisted user data
|
||||
|authorization|Local|Cache persisted authorization data
|
||||
|keys|Local|Cache external public keys
|
||||
|crl|Local|Cache for X.509 authenticator CRLs
|
||||
|work|Replicated|Propagate invalidation messages across nodes
|
||||
|authenticationSessions|Distributed|Caches authentication sessions, created/destroyed/expired during the authentication process
|
||||
|sessions|Distributed|Cache persisted user session data
|
||||
|
||||
@ -293,6 +293,10 @@ public interface RealmResource {
|
||||
@POST
|
||||
void clearKeysCache();
|
||||
|
||||
@Path("clear-crl-cache")
|
||||
@POST
|
||||
void clearCrlCache();
|
||||
|
||||
@Path("push-revocation")
|
||||
@POST
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
||||
@ -3314,10 +3314,12 @@ clearCachesTitle=Clear Caches
|
||||
realmCache=Realm Cache
|
||||
userCache=User Cache
|
||||
keysCache=Keys Cache
|
||||
crlCache=CRL Cache
|
||||
clearButtonTitle=Clear
|
||||
clearRealmCacheHelp=This will clear entries for all realms.
|
||||
clearUserCacheHelp=This will clear entries for all realms.
|
||||
clearKeysCacheHelp=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. This will clear all entries for all realms.
|
||||
clearCrlCacheHelp=Clears all entries from the CRL cache. The CRL cache improves the performance of the X.509 authenticator when Certificate Revocation List (CRL) are enabled. This action will clear all the CRL entries for all the realms.
|
||||
clearCacheSuccess=Cache cleared successfully
|
||||
clearCacheError=Could not clear cache\: {{error}}
|
||||
expandRow=Expand row
|
||||
|
||||
@ -37,6 +37,7 @@ export const PageHeaderClearCachesModal = ({
|
||||
const clearRealmCache = clearCache(adminClient.cache.clearRealmCache);
|
||||
const clearUserCache = clearCache(adminClient.cache.clearUserCache);
|
||||
const clearKeysCache = clearCache(adminClient.cache.clearKeysCache);
|
||||
const clearCrlCache = clearCache(adminClient.cache.clearCrlCache);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -95,6 +96,22 @@ export const PageHeaderClearCachesModal = ({
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Flex justifyContent={{ default: "justifyContentSpaceBetween" }}>
|
||||
<FlexItem>
|
||||
{t("crlCache")}{" "}
|
||||
<HelpItem
|
||||
helpText={t("clearCrlCacheHelp")}
|
||||
fieldLabelId="clearCrlCacheHelp"
|
||||
/>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Button onClick={() => clearCrlCache(realmName)}>
|
||||
{t("clearButtonTitle")}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -10,6 +10,10 @@ export class Cache extends Resource<{ realm?: string }> {
|
||||
method: "POST",
|
||||
path: "/clear-keys-cache",
|
||||
});
|
||||
public clearCrlCache = this.makeRequest<{}, void>({
|
||||
method: "POST",
|
||||
path: "/clear-crl-cache",
|
||||
});
|
||||
public clearRealmCache = this.makeRequest<{}, void>({
|
||||
method: "POST",
|
||||
path: "/clear-realm-cache",
|
||||
|
||||
@ -68,6 +68,8 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.A
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_CACHE_NAMES;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CRL_CACHE_DEFAULT_MAX;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CRL_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JGROUPS_BIND_ADDR;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JMX_DOMAIN;
|
||||
@ -264,6 +266,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||
defineRevisionCache(cacheManager, AUTHORIZATION_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX);
|
||||
|
||||
cacheManager.getCache(KEYS_CACHE_NAME, true);
|
||||
cacheManager.getCache(CRL_CACHE_NAME, true);
|
||||
|
||||
this.topologyInfo = new TopologyInfo(cacheManager, config, false, getId());
|
||||
|
||||
@ -318,6 +321,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||
cacheManager.defineConfiguration(KEYS_CACHE_NAME, getKeysCacheConfig());
|
||||
cacheManager.getCache(KEYS_CACHE_NAME, true);
|
||||
|
||||
cacheManager.defineConfiguration(CRL_CACHE_NAME, getCrlCacheConfig());
|
||||
cacheManager.getCache(CRL_CACHE_NAME, true);
|
||||
|
||||
var builder = createCacheConfigurationBuilder();
|
||||
if (clustered) {
|
||||
builder.simpleCache(false);
|
||||
@ -473,6 +479,16 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||
return cb.build();
|
||||
}
|
||||
|
||||
protected Configuration getCrlCacheConfig() {
|
||||
ConfigurationBuilder cb = createCacheConfigurationBuilder();
|
||||
|
||||
cb.memory()
|
||||
.whenFull(EvictionStrategy.REMOVE)
|
||||
.maxCount(CRL_CACHE_DEFAULT_MAX);
|
||||
|
||||
return cb.build();
|
||||
}
|
||||
|
||||
private void registerSystemWideListeners(KeycloakSession session) {
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
|
||||
@ -64,6 +64,9 @@ public interface InfinispanConnectionProvider extends Provider {
|
||||
int KEYS_CACHE_DEFAULT_MAX = 1000;
|
||||
int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
|
||||
|
||||
String CRL_CACHE_NAME = "crl";
|
||||
int CRL_CACHE_DEFAULT_MAX = 1000;
|
||||
|
||||
// System property used on Wildfly to identify distributedCache address and sticky session route
|
||||
String JBOSS_NODE_NAME = "jboss.node.name";
|
||||
String JGROUPS_UDP_MCAST_ADDR = "jgroups.mcast_addr";
|
||||
@ -85,7 +88,8 @@ public interface InfinispanConnectionProvider extends Provider {
|
||||
USER_REVISIONS_CACHE_NAME,
|
||||
AUTHORIZATION_CACHE_NAME,
|
||||
AUTHORIZATION_REVISIONS_CACHE_NAME,
|
||||
KEYS_CACHE_NAME
|
||||
KEYS_CACHE_NAME,
|
||||
CRL_CACHE_NAME,
|
||||
};
|
||||
|
||||
// list of cache name which could be defined as distributed or replicated
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.cache.CacheCrlProvider;
|
||||
import org.keycloak.models.cache.infinispan.ClearCacheEvent;
|
||||
|
||||
public class InfinispanCacheCrlProvider implements CacheCrlProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
private final Cache<String, X509CRLEntry> crlCache;
|
||||
|
||||
public InfinispanCacheCrlProvider(KeycloakSession session, Cache<String, X509CRLEntry> crlCache) {
|
||||
this.session = session;
|
||||
this.crlCache = crlCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
crlCache.clear();
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
cluster.notify(InfinispanCacheCrlProviderFactory.CRL_CLEAR_CACHE_EVENTS, ClearCacheEvent.getInstance(), true, ClusterProvider.DCNotify.ALL_DCS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.cluster.ClusterEvent;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.cache.CacheCrlProvider;
|
||||
import org.keycloak.models.cache.CacheCrlProviderFactory;
|
||||
|
||||
public class InfinispanCacheCrlProviderFactory implements CacheCrlProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "infinispan";
|
||||
|
||||
public static final String CRL_CLEAR_CACHE_EVENTS = "CRL_CLEAR_CACHE_EVENTS";
|
||||
|
||||
private volatile Cache<String, X509CRLEntry> crlCache;
|
||||
|
||||
@Override
|
||||
public CacheCrlProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new InfinispanCacheCrlProvider(session, crlCache);
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if (crlCache == null) {
|
||||
synchronized (this) {
|
||||
if (crlCache == null) {
|
||||
crlCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CRL_CACHE_NAME);
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
|
||||
cluster.registerListener(CRL_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {
|
||||
crlCache.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl.infinispan;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.X509CRL;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.crl.CrlStorageProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class InfinispanCrlStorageProvider implements CrlStorageProvider {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanCrlStorageProvider.class);
|
||||
|
||||
private final SharedData data;
|
||||
|
||||
public InfinispanCrlStorageProvider(SharedData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509CRL get(String key, Callable<X509CRL> loader) throws GeneralSecurityException {
|
||||
final X509CRLEntry crlEntry = data.cache().get(key);
|
||||
final long currentTime = Time.currentTimeMillis();
|
||||
if (crlEntry != null && (crlEntry.crl().getNextUpdate() == null || crlEntry.crl().getNextUpdate().compareTo(new Date(currentTime)) > 0)) {
|
||||
log.debugf("returning CRL '%s' from cache because it's cached OK", key);
|
||||
return crlEntry.crl();
|
||||
}
|
||||
|
||||
// refresh the crl entry in the cache
|
||||
return reloadCrl(key, loader, currentTime, crlEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshCache(String key, Callable<X509CRL> loader) throws GeneralSecurityException {
|
||||
final X509CRLEntry entry = data.cache().get(key);
|
||||
final X509CRL crl = reloadCrl(key, loader, Time.currentTimeMillis(), entry);
|
||||
return crl != null && (entry == null || entry.crl() != crl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private X509CRL reloadCrl(String key, Callable<X509CRL> loader, long currentTime, X509CRLEntry crlEntry) {
|
||||
if (crlEntry != null && currentTime < crlEntry.lastRequestTime()+ data.minTimeBetweenRequests()){
|
||||
log.debugf("Avoiding loading crl with key '%s' again, last refreshed time %d", key, crlEntry.lastRequestTime());
|
||||
return crlEntry.crl();
|
||||
}
|
||||
|
||||
FutureTask<X509CRL> task = new FutureTask<>(() -> loadCrl(key, loader, currentTime));
|
||||
|
||||
final FutureTask<X509CRL> existing = data.tasksInProgress().putIfAbsent(key, task);
|
||||
if (existing == null) {
|
||||
log.debugf("Reloading crl for model key '%s'.", key);
|
||||
task.run();
|
||||
} else {
|
||||
task = existing;
|
||||
}
|
||||
|
||||
try {
|
||||
return task.get();
|
||||
} catch (ExecutionException ee) {
|
||||
throw new RuntimeException("Error when loading crl " + key + " : " + ee.getMessage(), ee);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Error. Interrupted when loading crl " + key, ie);
|
||||
} finally {
|
||||
// Our thread inserted the task. Let's clean
|
||||
if (existing == null) {
|
||||
data.tasksInProgress().remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private X509CRL loadCrl(String key, Callable<X509CRL> loader, long currentTime) throws Exception {
|
||||
final X509CRL crl = loader.call();
|
||||
if (crl == null) {
|
||||
log.warnf("Loading crl with key '%s' returned null.", key);
|
||||
return null;
|
||||
}
|
||||
long lifespan = getLifespan(crl, currentTime);
|
||||
if (lifespan > 0) {
|
||||
data.cache().put(key, new X509CRLEntry(crl, currentTime), lifespan, TimeUnit.MILLISECONDS);
|
||||
log.debugf("The crl with key '%s' was retrieved successfully and cached for %d millis.", key, lifespan);
|
||||
} else {
|
||||
data.cache().put(key, new X509CRLEntry(crl, currentTime));
|
||||
log.debugf("The crl with key '%s' was retrieved successfully and cached forever.", key);
|
||||
}
|
||||
return crl;
|
||||
}
|
||||
|
||||
private long getLifespan(X509CRL crl, long currentTime) {
|
||||
final long cacheTime = data.cacheTime();
|
||||
|
||||
if (crl.getNextUpdate() == null) {
|
||||
return cacheTime;
|
||||
}
|
||||
|
||||
final long nextUpdateTime = crl.getNextUpdate().getTime() - currentTime;
|
||||
if (nextUpdateTime <= 0) {
|
||||
// if the CRL is expired just cache the minimum time
|
||||
return data.minTimeBetweenRequests();
|
||||
} else if (cacheTime > 0) {
|
||||
// get the minimum between cacheTime and nextUpdate
|
||||
return Math.min(cacheTime, nextUpdateTime);
|
||||
} else {
|
||||
// just return the next update because default cache is infinite
|
||||
return nextUpdateTime;
|
||||
}
|
||||
}
|
||||
|
||||
protected interface SharedData {
|
||||
Cache<String, X509CRLEntry> cache();
|
||||
Map<String, FutureTask<X509CRL>> tasksInProgress();
|
||||
long cacheTime();
|
||||
long minTimeBetweenRequests();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl.infinispan;
|
||||
|
||||
import java.security.cert.X509CRL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.crl.CrlStorageProvider;
|
||||
import org.keycloak.crl.CrlStorageProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class InfinispanCrlStorageProviderFactory implements CrlStorageProviderFactory, InfinispanCrlStorageProvider.SharedData {
|
||||
|
||||
public static final String PROVIDER_ID = "infinispan";
|
||||
|
||||
private volatile Cache<String, X509CRLEntry> crlCache;
|
||||
private final Map<String, FutureTask<X509CRL>> tasksInProgress = new ConcurrentHashMap<>();
|
||||
private volatile long cacheTime;
|
||||
private volatile long minTimeBetweenRequests;
|
||||
|
||||
@Override
|
||||
public CrlStorageProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new InfinispanCrlStorageProvider(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name("cacheTime")
|
||||
.type("int")
|
||||
.helpText(
|
||||
"""
|
||||
Interval in seconds that the CRL is cached. The next update time of the CRL is always a minimum if present.
|
||||
Zero or a negative value means CRL is cached until the next update time specified in the CRL (or infinite if the
|
||||
CRL does not contain the next update).
|
||||
"""
|
||||
)
|
||||
.defaultValue(-1)
|
||||
.add()
|
||||
.property()
|
||||
.name("minTimeBetweenRequests")
|
||||
.type("int")
|
||||
.helpText(
|
||||
"""
|
||||
Minimum interval in seconds between two requests to retrieve the CRL. The CRL is not updated
|
||||
from the URL again until this minimum time has passed since the previous refresh. In theory
|
||||
this option is never used if the CRL is refreshed correctly in the next update time.
|
||||
The interval should be a positive number. Default 10 seconds.
|
||||
"""
|
||||
)
|
||||
.defaultValue(10)
|
||||
.add()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
final long tmpCacheTime = config.getLong("cacheTime", -1L);
|
||||
cacheTime = tmpCacheTime > 0? TimeUnit.SECONDS.toMillis(tmpCacheTime) : -1L;
|
||||
|
||||
final long tmpMinTimeBetweenRequests = config.getLong("minTimeBetweenRequests", 10L);
|
||||
minTimeBetweenRequests = tmpMinTimeBetweenRequests > 0? TimeUnit.SECONDS.toMillis(tmpMinTimeBetweenRequests) : 10_000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if (crlCache == null) {
|
||||
synchronized (this) {
|
||||
if (crlCache == null) {
|
||||
this.crlCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CRL_CACHE_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cache<String, X509CRLEntry> cache() {
|
||||
return crlCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, FutureTask<X509CRL>> tasksInProgress() {
|
||||
return tasksInProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cacheTime() {
|
||||
return cacheTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long minTimeBetweenRequests() {
|
||||
return minTimeBetweenRequests;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl.infinispan;
|
||||
|
||||
import java.security.cert.X509CRL;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public record X509CRLEntry(X509CRL crl, long lastRequestTime) {}
|
||||
@ -0,0 +1,18 @@
|
||||
#
|
||||
# Copyright 2025 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.crl.infinispan.InfinispanCrlStorageProviderFactory
|
||||
@ -0,0 +1,18 @@
|
||||
#
|
||||
# Copyright 2025 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.crl.infinispan.InfinispanCacheCrlProviderFactory
|
||||
29
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProvider.java
vendored
Normal file
29
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProvider.java
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 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 CacheCrlProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Clears all the cached CRLs, so they need to be loaded again
|
||||
*/
|
||||
void clearCache();
|
||||
|
||||
}
|
||||
23
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProviderFactory.java
vendored
Normal file
23
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProviderFactory.java
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 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 CacheCrlProviderFactory extends ProviderFactory<CacheCrlProvider> {
|
||||
}
|
||||
43
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProviderSpi.java
vendored
Normal file
43
model/storage-private/src/main/java/org/keycloak/models/cache/CacheCrlProviderSpi.java
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 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.Spi;
|
||||
|
||||
public class CacheCrlProviderSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "crlCache";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CacheCrlProvider> getProviderClass() {
|
||||
return CacheCrlProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CacheCrlProviderFactory> getProviderFactoryClass() {
|
||||
return CacheCrlProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
org.keycloak.models.cache.CacheUserProviderSpi
|
||||
org.keycloak.models.cache.CacheRealmProviderSpi
|
||||
org.keycloak.models.cache.CachePublicKeyProviderSpi
|
||||
org.keycloak.models.cache.CacheCrlProviderSpi
|
||||
org.keycloak.models.dblock.DBLockSpi
|
||||
org.keycloak.storage.client.ClientStorageProviderSpi
|
||||
org.keycloak.storage.group.GroupStorageProviderSpi
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 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 ClearCrlCacheRealmAdminProvider 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-crl-cache";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||
return new ClearCrlCacheResource(session, auth, adminEvent);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 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.CacheCrlProvider;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
||||
import jakarta.ws.rs.POST;
|
||||
|
||||
public class ClearCrlCacheResource {
|
||||
|
||||
protected final AdminPermissionEvaluator auth;
|
||||
protected final RealmModel realm;
|
||||
private final AdminEventBuilder adminEvent;
|
||||
|
||||
protected final KeycloakSession session;
|
||||
|
||||
public ClearCrlCacheResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||
this.session = session;
|
||||
this.auth = auth;
|
||||
this.realm = session.getContext().getRealm();
|
||||
this.adminEvent = adminEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the crl cache (CRLs loaded for X509 authentication)
|
||||
*
|
||||
*/
|
||||
@POST
|
||||
public void clearCrlCache() {
|
||||
auth.realm().requireManageRealm();
|
||||
|
||||
CacheCrlProvider cache = session.getProvider(CacheCrlProvider.class);
|
||||
if (cache != null) {
|
||||
cache.clearCache();
|
||||
}
|
||||
|
||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
|
||||
}
|
||||
}
|
||||
@ -19,3 +19,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
|
||||
org.keycloak.services.resources.admin.ClearCrlCacheRealmAdminProvider
|
||||
|
||||
@ -73,6 +73,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl" simple-cache="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -26,7 +26,7 @@ public class CachingOptions {
|
||||
private static final String CACHE_METRICS_PREFIX = "cache-metrics";
|
||||
public static final String CACHE_METRICS_HISTOGRAMS_ENABLED_PROPERTY = CACHE_METRICS_PREFIX + "-histograms-enabled";
|
||||
|
||||
public static final String[] LOCAL_MAX_COUNT_CACHES = new String[]{"authorization", "keys", "realms", "users", };
|
||||
public static final String[] LOCAL_MAX_COUNT_CACHES = new String[]{"authorization", "crl", "keys", "realms", "users", };
|
||||
|
||||
public static final String[] CLUSTERED_MAX_COUNT_CACHES = new String[]{"clientSessions", "offlineSessions", "offlineClientSessions", "sessions"};
|
||||
|
||||
|
||||
@ -77,6 +77,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl" simple-cache="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -79,6 +79,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl" simple-cache="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="actionTokens">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -222,6 +222,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl" simple-cache="true" statistics="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<distributed-cache name="actionTokens" owners="2" statistics="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -85,6 +85,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -32,6 +32,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -361,4 +363,4 @@ Bootstrap Admin:
|
||||
Do NOT start the server using this command when deploying to production.
|
||||
|
||||
Use 'kc.sh start-dev --help-all' to list all available options, including build
|
||||
options.
|
||||
options.
|
||||
|
||||
@ -32,6 +32,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -502,4 +504,4 @@ Bootstrap Admin:
|
||||
Do NOT start the server using this command when deploying to production.
|
||||
|
||||
Use 'kc.sh start-dev --help-all' to list all available options, including build
|
||||
options.
|
||||
options.
|
||||
|
||||
@ -33,6 +33,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -372,4 +374,4 @@ By default, this command tries to update the server configuration by running a
|
||||
$ kc.sh start '--optimized'
|
||||
|
||||
By doing that, the server should start faster based on any previous
|
||||
configuration you have set when manually running the 'build' command.
|
||||
configuration you have set when manually running the 'build' command.
|
||||
|
||||
@ -33,6 +33,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -507,4 +509,4 @@ By default, this command tries to update the server configuration by running a
|
||||
$ kc.sh start '--optimized'
|
||||
|
||||
By doing that, the server should start faster based on any previous
|
||||
configuration you have set when manually running the 'build' command.
|
||||
configuration you have set when manually running the 'build' command.
|
||||
|
||||
@ -33,6 +33,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -306,4 +308,4 @@ By default, this command tries to update the server configuration by running a
|
||||
$ kc.sh start '--optimized'
|
||||
|
||||
By doing that, the server should start faster based on any previous
|
||||
configuration you have set when manually running the 'build' command.
|
||||
configuration you have set when manually running the 'build' command.
|
||||
|
||||
@ -33,6 +33,8 @@ Cache:
|
||||
The maximum number of entries that can be stored in-memory by the
|
||||
clientSessions cache. Available only when embedded Infinispan clusters
|
||||
configured.
|
||||
--cache-embedded-crl-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the crl cache.
|
||||
--cache-embedded-keys-max-count <max-count>
|
||||
The maximum number of entries that can be stored in-memory by the keys cache.
|
||||
--cache-embedded-mtls-enabled <true|false>
|
||||
@ -433,4 +435,4 @@ By default, this command tries to update the server configuration by running a
|
||||
$ kc.sh start '--optimized'
|
||||
|
||||
By doing that, the server should start faster based on any previous
|
||||
configuration you have set when manually running the 'build' command.
|
||||
configuration you have set when manually running the 'build' command.
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.X509CRL;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Crl Storage Provider interface
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public interface CrlStorageProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Returns the CRL for this key from cache or loading from the loader.
|
||||
* @param key The key for the CRL
|
||||
* @param loader The loader to get if the CRL is not in cache
|
||||
* @return The X509CRL placed in the cache
|
||||
* @throws GeneralSecurityException
|
||||
*/
|
||||
X509CRL get(String key, Callable<X509CRL> loader) throws GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* Refreshes the CRL in the cache for this key.
|
||||
* @param key The key for the CRL
|
||||
* @param loader The loader to get the CRL
|
||||
* @return true if refreshed or false if not
|
||||
* @throws GeneralSecurityException
|
||||
*/
|
||||
boolean refreshCache(String key, Callable<X509CRL> loader) throws GeneralSecurityException;
|
||||
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
|
||||
/*
|
||||
* Copyright 2025 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.crl;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface CrlStorageProviderFactory extends ProviderFactory<CrlStorageProvider> {
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 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.crl;
|
||||
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class CrlStorageSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "crlStorage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CrlStorageProvider> getProviderClass() {
|
||||
return CrlStorageProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CrlStorageProviderFactory> getProviderFactoryClass() {
|
||||
return CrlStorageProviderFactory.class;
|
||||
}
|
||||
|
||||
}
|
||||
@ -80,6 +80,7 @@ org.keycloak.transaction.TransactionManagerLookupSpi
|
||||
org.keycloak.credential.hash.PasswordHashSpi
|
||||
org.keycloak.credential.CredentialSpi
|
||||
org.keycloak.keys.PublicKeyStorageSpi
|
||||
org.keycloak.crl.CrlStorageSpi
|
||||
org.keycloak.keys.KeySpi
|
||||
org.keycloak.storage.DatastoreSpi
|
||||
org.keycloak.storage.role.RoleStorageProviderSpi
|
||||
|
||||
@ -21,7 +21,6 @@ package org.keycloak.authentication.authenticators.x509;
|
||||
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_POLICY_MODE_ANY;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@ -74,6 +73,7 @@ import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.crl.CrlStorageProvider;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
@ -279,7 +279,13 @@ public class CertificateValidator {
|
||||
}
|
||||
|
||||
public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
|
||||
X509CRL crl = loadCRL();
|
||||
if (cRLPath == null) {
|
||||
throw new GeneralSecurityException("Unable to load CRL because no crl path is defined");
|
||||
}
|
||||
|
||||
CrlStorageProvider crlCache = session.getProvider(CrlStorageProvider.class);
|
||||
final X509CRL crl = crlCache.get(cRLPath, this::loadCRL);
|
||||
|
||||
if (crl == null) {
|
||||
throw new GeneralSecurityException(String.format("Unable to load CRL from \"%s\"", cRLPath));
|
||||
}
|
||||
@ -298,25 +304,23 @@ public class CertificateValidator {
|
||||
private X509CRL loadCRL() throws GeneralSecurityException {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509CRL crl = null;
|
||||
if (cRLPath != null) {
|
||||
if (cRLPath.startsWith("http") || cRLPath.startsWith("https")) {
|
||||
// load CRL using remote URI
|
||||
try {
|
||||
crl = loadFromURI(cf, new URI(cRLPath));
|
||||
} catch (URISyntaxException e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
} else if (cRLPath.startsWith("ldap")) {
|
||||
// load CRL from LDAP
|
||||
try {
|
||||
crl = loadCRLFromLDAP(cf, new URI(cRLPath));
|
||||
} catch(URISyntaxException e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// load CRL from file
|
||||
crl = loadCRLFromFile(cf, cRLPath);
|
||||
if (cRLPath.startsWith("http") || cRLPath.startsWith("https")) {
|
||||
// load CRL using remote URI
|
||||
try {
|
||||
crl = loadFromURI(cf, new URI(cRLPath));
|
||||
} catch (URISyntaxException e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
} else if (cRLPath.startsWith("ldap")) {
|
||||
// load CRL from LDAP
|
||||
try {
|
||||
crl = loadCRLFromLDAP(cf, new URI(cRLPath));
|
||||
} catch (URISyntaxException e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// load CRL from file
|
||||
crl = loadCRLFromFile(cf, cRLPath);
|
||||
}
|
||||
return crl;
|
||||
}
|
||||
@ -330,10 +334,8 @@ public class CertificateValidator {
|
||||
get.setHeader("Pragma", "no-cache");
|
||||
get.setHeader("Cache-Control", "no-cache, no-store");
|
||||
try (CloseableHttpResponse response = httpClient.execute(get)) {
|
||||
try {
|
||||
InputStream content = response.getEntity().getContent();
|
||||
X509CRL crl = loadFromStream(cf, content);
|
||||
return crl;
|
||||
try (InputStream content = response.getEntity().getContent()) {
|
||||
return loadFromStream(cf, content);
|
||||
} finally {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
}
|
||||
@ -360,8 +362,9 @@ public class CertificateValidator {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new CertificateException(String.format("Failed to download CRL from \"%s\"", remoteURI.toString()));
|
||||
}
|
||||
X509CRL crl = loadFromStream(cf, new ByteArrayInputStream(data));
|
||||
return crl;
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
return loadFromStream(cf, is);
|
||||
}
|
||||
} finally {
|
||||
ctx.close();
|
||||
}
|
||||
@ -386,8 +389,7 @@ public class CertificateValidator {
|
||||
throw new IOException(String.format("Unable to read CRL from \"%s\"", f.getAbsolutePath()));
|
||||
}
|
||||
try (FileInputStream is = new FileInputStream(f.getAbsolutePath())) {
|
||||
X509CRL crl = loadFromStream(cf, is);
|
||||
return crl;
|
||||
return loadFromStream(cf, is);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -397,11 +399,9 @@ public class CertificateValidator {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private X509CRL loadFromStream(CertificateFactory cf, InputStream is) throws IOException, CRLException {
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
X509CRL crl = (X509CRL)cf.generateCRL(dis);
|
||||
dis.close();
|
||||
return crl;
|
||||
return (X509CRL) cf.generateCRL(is);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -60,6 +60,14 @@
|
||||
<expiration max-idle="3600000"/>
|
||||
<memory storage="HEAP" max-count="1000"/>
|
||||
</local-cache>
|
||||
<local-cache name="crl" simple-cache="true">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
<value media-type="application/x-java-object"/>
|
||||
</encoding>
|
||||
<expiration lifespan="-1"/>
|
||||
<memory max-count="1000"/>
|
||||
</local-cache>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<encoding>
|
||||
<key media-type="application/x-java-object"/>
|
||||
|
||||
@ -21,6 +21,8 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.undertow.Undertow;
|
||||
import io.undertow.io.Sender;
|
||||
@ -50,17 +52,21 @@ public class CRLRule extends ExternalResource {
|
||||
public static final String CRL_RESPONDER_ORIGIN = "http://" + CRL_RESPONDER_HOST + ":" + CRL_RESPONDER_PORT;
|
||||
|
||||
private Undertow crlResponder;
|
||||
private Map<String, CRLHandler> handlers;
|
||||
private PathHandler pathHandler;
|
||||
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
log.info("Starting CRL Responder");
|
||||
|
||||
PathHandler pathHandler = new PathHandler();
|
||||
pathHandler.addExactPath(AbstractX509AuthenticationTest.EMPTY_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.EMPTY_CRL_PATH));
|
||||
pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH));
|
||||
pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH));
|
||||
pathHandler.addExactPath(AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH));
|
||||
pathHandler.addExactPath(AbstractX509AuthenticationTest.INVALID_CRL_PATH, new CRLHandler(AbstractX509AuthenticationTest.INVALID_CRL_PATH));
|
||||
handlers = new HashMap<>();
|
||||
pathHandler = new PathHandler();
|
||||
addHandler(AbstractX509AuthenticationTest.EMPTY_CRL_PATH, AbstractX509AuthenticationTest.EMPTY_CRL_PATH);
|
||||
addHandler(AbstractX509AuthenticationTest.EMPTY_EXPIRED_CRL_PATH, AbstractX509AuthenticationTest.EMPTY_EXPIRED_CRL_PATH);
|
||||
addHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH, AbstractX509AuthenticationTest.INTERMEDIATE_CA_CRL_PATH);
|
||||
addHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH, AbstractX509AuthenticationTest.INTERMEDIATE_CA_INVALID_SIGNATURE_CRL_PATH);
|
||||
addHandler(AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH, AbstractX509AuthenticationTest.INTERMEDIATE_CA_3_CRL_PATH);
|
||||
addHandler(AbstractX509AuthenticationTest.INVALID_CRL_PATH, AbstractX509AuthenticationTest.INVALID_CRL_PATH);
|
||||
|
||||
crlResponder = Undertow.builder().addHttpListener(CRL_RESPONDER_PORT, CRL_RESPONDER_HOST)
|
||||
.setHandler(
|
||||
@ -76,15 +82,50 @@ public class CRLRule extends ExternalResource {
|
||||
crlResponder.stop();
|
||||
}
|
||||
|
||||
public void addHandler(String path, String crlFileName) {
|
||||
CRLHandler handler = new CRLHandler(crlFileName);
|
||||
handlers.put(path, handler);
|
||||
pathHandler.addExactPath(path, handler);
|
||||
}
|
||||
|
||||
public void removeHandler(String path) {
|
||||
handlers.remove(path);
|
||||
pathHandler.removeExactPath(path);
|
||||
}
|
||||
|
||||
public void setCrlForHandler(String path, String crlFileName) {
|
||||
handlers.get(path).SetFileName(crlFileName);
|
||||
}
|
||||
|
||||
public int getCounter(String path) {
|
||||
return handlers.get(path).getCounter();
|
||||
}
|
||||
|
||||
public void resetCounter(String path) {
|
||||
handlers.get(path).resetCounter();
|
||||
}
|
||||
|
||||
private class CRLHandler implements HttpHandler {
|
||||
|
||||
private String crlFileName;
|
||||
private int counter;
|
||||
|
||||
public CRLHandler(String crlFileName) {
|
||||
this.crlFileName = crlFileName;
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
public void SetFileName(String crlFileName) {
|
||||
this.crlFileName = crlFileName;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public void resetCounter() {
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||||
@ -93,6 +134,7 @@ public class CRLRule extends ExternalResource {
|
||||
return;
|
||||
}
|
||||
|
||||
counter++;
|
||||
String fullFile = AbstractX509AuthenticationTest.getAuthServerHome() + File.separator + crlFileName;
|
||||
InputStream is = new FileInputStream(new File(fullFile));
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import org.keycloak.events.Details;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
import org.keycloak.testsuite.util.HtmlUnitBrowser;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
@ -143,6 +144,54 @@ public class X509BrowserCRLTest extends AbstractX509AuthenticationTest {
|
||||
assertLoginFailedDueRevokedCertificate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginTestCRLCaching() {
|
||||
X509AuthenticatorConfigModel config =
|
||||
new X509AuthenticatorConfigModel()
|
||||
.setCRLEnabled(true)
|
||||
.setCRLRelativePath(CRLRule.CRL_RESPONDER_ORIGIN + "/cached-crl")
|
||||
.setConfirmationPageAllowed(true)
|
||||
.setMappingSourceType(SUBJECTDN_EMAIL)
|
||||
.setUserIdentityMapperType(USERNAME_EMAIL);
|
||||
AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
|
||||
String cfgId = createConfig(browserExecution.getId(), cfg);
|
||||
Assert.assertNotNull(cfgId);
|
||||
|
||||
try {
|
||||
// change CRL to the empty but expired, it should login OK
|
||||
crlRule.addHandler("cached-crl", EMPTY_EXPIRED_CRL_PATH);
|
||||
x509BrowserLogin(config, userId, "test-user@localhost", "test-user@localhost");
|
||||
AccountHelper.logout(testRealm(), "test-user@localhost");
|
||||
Assert.assertEquals(1, crlRule.getCounter("cached-crl"));
|
||||
|
||||
// change the CRL to the new one but it is cached the min time
|
||||
crlRule.setCrlForHandler("cached-crl", INTERMEDIATE_CA_CRL_PATH);
|
||||
x509BrowserLogin(config, userId, "test-user@localhost", "test-user@localhost");
|
||||
AccountHelper.logout(testRealm(), "test-user@localhost");
|
||||
Assert.assertEquals(1, crlRule.getCounter("cached-crl"));
|
||||
|
||||
// wait the min time and it should be refreshed now and fail
|
||||
setTimeOffset(10);
|
||||
assertLoginFailedDueRevokedCertificate();
|
||||
AccountHelper.logout(testRealm(), "test-user@localhost");
|
||||
Assert.assertEquals(2, crlRule.getCounter("cached-crl"));
|
||||
|
||||
// now it's cached until next update 50 years
|
||||
setTimeOffset(3600);
|
||||
assertLoginFailedDueRevokedCertificate();
|
||||
AccountHelper.logout(testRealm(), "test-user@localhost");
|
||||
Assert.assertEquals(2, crlRule.getCounter("cached-crl"));
|
||||
|
||||
// clear the cache
|
||||
testRealm().clearCrlCache();
|
||||
assertLoginFailedDueRevokedCertificate();
|
||||
AccountHelper.logout(testRealm(), "test-user@localhost");
|
||||
Assert.assertEquals(3, crlRule.getCounter("cached-crl"));
|
||||
} finally {
|
||||
crlRule.removeHandler("cached-crl");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginFailedWithIntermediateRevocationListFromHttp() {
|
||||
X509AuthenticatorConfigModel config =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user