Persisting revoked access tokens
Closes #31296 Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
parent
90ff35bcd2
commit
227c71f7f0
19 changed files with 631 additions and 15 deletions
|
@ -30,6 +30,11 @@ As a consequence, group-related events like the `GroupRemovedEvent` are no longe
|
||||||
|
|
||||||
For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}].
|
For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}].
|
||||||
|
|
||||||
|
= Persisting revoked access tokens across restarts
|
||||||
|
|
||||||
|
In this release, revoked access tokens are written to the database and reloaded when the cluster is restarted by default when using the embedded caches.
|
||||||
|
|
||||||
|
For information on how to migrate, see the link:{upgradingguide_link}[{upgradingguide_name}].
|
||||||
|
|
||||||
= Keycloak CR supports standard scheduling options
|
= Keycloak CR supports standard scheduling options
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,15 @@ In order to use a custom footer, create a `footer.ftl` file in your custom login
|
||||||
|
|
||||||
For more details, see link:{developerguide_link}#_theme_custom_footer[Adding a custom footer to a login theme].
|
For more details, see link:{developerguide_link}#_theme_custom_footer[Adding a custom footer to a login theme].
|
||||||
|
|
||||||
|
= Persisting revoked access tokens across restarts
|
||||||
|
|
||||||
|
In this release, revoked access tokens are written to the database and reloaded when the cluster is restarted by default when using the embedded caches.
|
||||||
|
|
||||||
|
To disable this behavior, use the SPI option `spi-single-use-object-infinispan-persist-revoked-tokens` as outlined in the https://www.keycloak.org/server/all-provider-config[All provider configuration] {section}.
|
||||||
|
|
||||||
|
The SPI behavior of `SingleUseObjectProvider` has changed that for revoked tokens only the methods `put` and `contains` must be used.
|
||||||
|
This is enforced by default, and can be disabled using the SPI option `spi-single-use-object-infinispan-persist-revoked-tokens`.
|
||||||
|
|
||||||
= Admin Bootstrapping
|
= Admin Bootstrapping
|
||||||
|
|
||||||
The environment variables `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` have been deprecated. You should use `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` instead. These are also general options, so they may be specified via the cli or other config sources, for example `--bootstrap-admin-username=admin`.
|
The environment variables `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` have been deprecated. You should use `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` instead. These are also general options, so they may be specified via the cli or other config sources, for example `--bootstrap-admin-username=admin`.
|
||||||
|
|
|
@ -27,7 +27,9 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.SingleUseObjectProvider;
|
import org.keycloak.models.SingleUseObjectProvider;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SingleUseObjectValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SingleUseObjectValueEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,11 +43,15 @@ public class InfinispanSingleUseObjectProvider implements SingleUseObjectProvide
|
||||||
|
|
||||||
public static final Logger logger = Logger.getLogger(InfinispanSingleUseObjectProvider.class);
|
public static final Logger logger = Logger.getLogger(InfinispanSingleUseObjectProvider.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
private final Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache;
|
private final Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache;
|
||||||
|
private final boolean persistRevokedTokens;
|
||||||
private final InfinispanKeycloakTransaction tx;
|
private final InfinispanKeycloakTransaction tx;
|
||||||
|
|
||||||
public InfinispanSingleUseObjectProvider(KeycloakSession session, Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache) {
|
public InfinispanSingleUseObjectProvider(KeycloakSession session, Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache, boolean persistRevokedTokens) {
|
||||||
|
this.session = session;
|
||||||
this.singleUseObjectCache = singleUseObjectCache;
|
this.singleUseObjectCache = singleUseObjectCache;
|
||||||
|
this.persistRevokedTokens = persistRevokedTokens;
|
||||||
this.tx = new InfinispanKeycloakTransaction();
|
this.tx = new InfinispanKeycloakTransaction();
|
||||||
session.getTransactionManager().enlistAfterCompletion(tx);
|
session.getTransactionManager().enlistAfterCompletion(tx);
|
||||||
}
|
}
|
||||||
|
@ -61,13 +67,22 @@ public class InfinispanSingleUseObjectProvider implements SingleUseObjectProvide
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debugf(re, "Failed when adding code %s", key);
|
logger.debugf(re, "Failed when adding code %s", key);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw re;
|
throw re;
|
||||||
}
|
}
|
||||||
|
if (persistRevokedTokens && key.endsWith(REVOKED_KEY)) {
|
||||||
|
if (!notes.isEmpty()) {
|
||||||
|
throw new ModelException("Notes are not supported for revoked tokens");
|
||||||
|
}
|
||||||
|
session.getProvider(RevokedTokenPersisterProvider.class).revokeToken(key.substring(0, key.length() - REVOKED_KEY.length()), lifespanSeconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> get(String key) {
|
public Map<String, String> get(String key) {
|
||||||
|
if (persistRevokedTokens && key.endsWith(REVOKED_KEY)) {
|
||||||
|
throw new ModelException("Revoked tokens can't be retrieved");
|
||||||
|
}
|
||||||
|
|
||||||
SingleUseObjectValueEntity singleUseObjectValueEntity;
|
SingleUseObjectValueEntity singleUseObjectValueEntity;
|
||||||
|
|
||||||
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
||||||
|
@ -77,6 +92,10 @@ public class InfinispanSingleUseObjectProvider implements SingleUseObjectProvide
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> remove(String key) {
|
public Map<String, String> remove(String key) {
|
||||||
|
if (persistRevokedTokens && key.endsWith(REVOKED_KEY)) {
|
||||||
|
throw new ModelException("Revoked tokens can't be removed");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
||||||
SingleUseObjectValueEntity existing = cache.remove(key);
|
SingleUseObjectValueEntity existing = cache.remove(key);
|
||||||
|
@ -94,12 +113,20 @@ public class InfinispanSingleUseObjectProvider implements SingleUseObjectProvide
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean replace(String key, Map<String, String> notes) {
|
public boolean replace(String key, Map<String, String> notes) {
|
||||||
|
if (persistRevokedTokens && key.endsWith(REVOKED_KEY)) {
|
||||||
|
throw new ModelException("Revoked tokens can't be replaced");
|
||||||
|
}
|
||||||
|
|
||||||
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
||||||
return cache.replace(key, new SingleUseObjectValueEntity(notes)) != null;
|
return cache.replace(key, new SingleUseObjectValueEntity(notes)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean putIfAbsent(String key, long lifespanInSeconds) {
|
public boolean putIfAbsent(String key, long lifespanInSeconds) {
|
||||||
|
if (persistRevokedTokens && key.endsWith(REVOKED_KEY)) {
|
||||||
|
throw new ModelException("Revoked tokens can't be used in putIfAbsent");
|
||||||
|
}
|
||||||
|
|
||||||
SingleUseObjectValueEntity tokenValue = new SingleUseObjectValueEntity(null);
|
SingleUseObjectValueEntity tokenValue = new SingleUseObjectValueEntity(null);
|
||||||
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
package org.keycloak.models.sessions.infinispan;
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
|
@ -26,30 +31,49 @@ import org.infinispan.client.hotrod.RemoteCache;
|
||||||
import org.infinispan.commons.api.BasicCache;
|
import org.infinispan.commons.api.BasicCache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.SingleUseObjectProvider;
|
||||||
import org.keycloak.models.SingleUseObjectProviderFactory;
|
import org.keycloak.models.SingleUseObjectProviderFactory;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SingleUseObjectValueEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SingleUseObjectValueEntity;
|
||||||
|
import org.keycloak.models.utils.PostMigrationEvent;
|
||||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
import org.keycloak.provider.ProviderEvent;
|
||||||
|
import org.keycloak.provider.ProviderEventListener;
|
||||||
|
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
||||||
|
import org.keycloak.services.scheduled.ClearExpiredRevokedTokens;
|
||||||
|
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
|
||||||
|
import org.keycloak.timer.TimerProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class InfinispanSingleUseObjectProviderFactory implements SingleUseObjectProviderFactory, EnvironmentDependentProviderFactory {
|
public class InfinispanSingleUseObjectProviderFactory implements SingleUseObjectProviderFactory<InfinispanSingleUseObjectProvider>, EnvironmentDependentProviderFactory, ServerInfoAwareProviderFactory {
|
||||||
|
|
||||||
|
public static final String CONFIG_PERSIST_REVOKED_TOKENS = "persistRevokedTokens";
|
||||||
|
private static final boolean DEFAULT_PERSIST_REVOKED_TOKENS = true;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(InfinispanSingleUseObjectProviderFactory.class);
|
private static final Logger LOG = Logger.getLogger(InfinispanSingleUseObjectProviderFactory.class);
|
||||||
|
|
||||||
private volatile Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache;
|
protected volatile Supplier<BasicCache<String, SingleUseObjectValueEntity>> singleUseObjectCache;
|
||||||
|
|
||||||
|
private volatile boolean initialized;
|
||||||
|
private boolean persistRevokedTokens;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfinispanSingleUseObjectProvider create(KeycloakSession session) {
|
public InfinispanSingleUseObjectProvider create(KeycloakSession session) {
|
||||||
return new InfinispanSingleUseObjectProvider(session, singleUseObjectCache);
|
initialize(session);
|
||||||
|
return new InfinispanSingleUseObjectProvider(session, singleUseObjectCache, persistRevokedTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Supplier getSingleUseObjectCache(KeycloakSession session) {
|
static Supplier<BasicCache<String, SingleUseObjectValueEntity>> getSingleUseObjectCache(KeycloakSession session) {
|
||||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
Cache cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
|
Cache cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
|
||||||
|
|
||||||
|
@ -66,16 +90,64 @@ public class InfinispanSingleUseObjectProviderFactory implements SingleUseObject
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
persistRevokedTokens = config.getBoolean(CONFIG_PERSIST_REVOKED_TOKENS, DEFAULT_PERSIST_REVOKED_TOKENS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static String LOADED = "loaded" + SingleUseObjectProvider.REVOKED_KEY;
|
||||||
|
|
||||||
|
private void initialize(KeycloakSession session) {
|
||||||
|
if (persistRevokedTokens && !initialized) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!initialized) {
|
||||||
|
RevokedTokenPersisterProvider provider = session.getProvider(RevokedTokenPersisterProvider.class);
|
||||||
|
BasicCache<String, SingleUseObjectValueEntity> cache = singleUseObjectCache.get();
|
||||||
|
if (cache.get(LOADED) == null) {
|
||||||
|
// in a cluster, multiple Keycloak instances might load the same data in parallel, but that wouldn't matter
|
||||||
|
provider.getAllRevokedTokens().forEach(revokedToken -> {
|
||||||
|
long lifespanSeconds = revokedToken.expiry() - Time.currentTime();
|
||||||
|
if (lifespanSeconds > 0) {
|
||||||
|
cache.put(revokedToken.tokenId() + SingleUseObjectProvider.REVOKED_KEY, new SingleUseObjectValueEntity(Collections.emptyMap()),
|
||||||
|
InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds)), TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cache.put(LOADED, new SingleUseObjectValueEntity(Collections.emptyMap()));
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
// It is necessary to put the cache initialization here, otherwise the cache would be initialized lazily, that
|
// It is necessary to put the cache initialization here, otherwise the cache would be initialized lazily, that
|
||||||
// means also listeners will start only after first cache initialization - that would be too late
|
// means also listeners will start only after first cache initialization - that would be too latedddd
|
||||||
if (singleUseObjectCache == null) {
|
if (singleUseObjectCache == null) {
|
||||||
this.singleUseObjectCache = getSingleUseObjectCache(factory.create());
|
this.singleUseObjectCache = getSingleUseObjectCache(factory.create());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (persistRevokedTokens) {
|
||||||
|
factory.register(new ProviderEventListener() {
|
||||||
|
public void onEvent(ProviderEvent event) {
|
||||||
|
if (event instanceof PostMigrationEvent) {
|
||||||
|
KeycloakSessionFactory sessionFactory = ((PostMigrationEvent) event).getFactory();
|
||||||
|
try (KeycloakSession session = sessionFactory.create()) {
|
||||||
|
TimerProvider timer = session.getProvider(TimerProvider.class);
|
||||||
|
if (timer != null) {
|
||||||
|
long interval = Config.scope("scheduled").getLong("interval", 900L) * 1000;
|
||||||
|
scheduleTask(sessionFactory, timer, interval);
|
||||||
|
}
|
||||||
|
// load sessions during startup, not on first request to avoid congestion
|
||||||
|
initialize(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTask(KeycloakSessionFactory sessionFactory, TimerProvider timer, long interval) {
|
||||||
|
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredRevokedTokens(), interval), interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -97,4 +169,27 @@ public class InfinispanSingleUseObjectProviderFactory implements SingleUseObject
|
||||||
public boolean isSupported(Config.Scope config) {
|
public boolean isSupported(Config.Scope config) {
|
||||||
return InfinispanUtils.isEmbeddedInfinispan();
|
return InfinispanUtils.isEmbeddedInfinispan();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getOperationalInfo() {
|
||||||
|
Map<String, String> info = new HashMap<>();
|
||||||
|
info.put(CONFIG_PERSIST_REVOKED_TOKENS, Boolean.toString(persistRevokedTokens));
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||||
|
ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create();
|
||||||
|
|
||||||
|
builder.property()
|
||||||
|
.name(CONFIG_PERSIST_REVOKED_TOKENS)
|
||||||
|
.type("boolean")
|
||||||
|
.helpText("If revoked tokens are stored persistently across restarts")
|
||||||
|
.defaultValue(DEFAULT_PERSIST_REVOKED_TOKENS)
|
||||||
|
.add();
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.jpa.entities;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a list of revoked tokens in the database, so it is available after restarts.
|
||||||
|
*
|
||||||
|
* @author Alexander Schwartz
|
||||||
|
*/
|
||||||
|
@Table(name="REVOKED_TOKEN")
|
||||||
|
@Entity
|
||||||
|
public class RevokedTokenEntity {
|
||||||
|
@Id
|
||||||
|
@Column(name="ID", length = 36)
|
||||||
|
protected String id;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expire time in seconds.
|
||||||
|
*/
|
||||||
|
@Column(name="EXPIRE")
|
||||||
|
protected long expire;
|
||||||
|
|
||||||
|
public long getExpire() {
|
||||||
|
return expire;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpire(long expire) {
|
||||||
|
this.expire = expire;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null) return false;
|
||||||
|
if (!(o instanceof RevokedTokenEntity that)) return false;
|
||||||
|
|
||||||
|
if (!id.equals(that.getId())) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.jpa.session;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
|
import jakarta.persistence.criteria.CriteriaDelete;
|
||||||
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.jpa.entities.RevokedTokenEntity;
|
||||||
|
import org.keycloak.models.session.RevokedToken;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterProvider;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexander Schwartz
|
||||||
|
*/
|
||||||
|
public class JpaRevokedTokensPersisterProvider implements RevokedTokenPersisterProvider {
|
||||||
|
|
||||||
|
private final EntityManager em;
|
||||||
|
|
||||||
|
public JpaRevokedTokensPersisterProvider(EntityManager em) {
|
||||||
|
this.em = em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revokeToken(String tokenId, long lifetime) {
|
||||||
|
RevokedTokenEntity revokedTokenEntity = new RevokedTokenEntity();
|
||||||
|
revokedTokenEntity.setId(tokenId);
|
||||||
|
revokedTokenEntity.setExpire(Time.currentTime() + lifetime);
|
||||||
|
em.persist(revokedTokenEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<RevokedToken> getAllRevokedTokens() {
|
||||||
|
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<RevokedTokenEntity> cq = cb.createQuery(RevokedTokenEntity.class);
|
||||||
|
Root<RevokedTokenEntity> rootEntry = cq.from(RevokedTokenEntity.class);
|
||||||
|
CriteriaQuery<RevokedTokenEntity> all = cq.select(rootEntry)
|
||||||
|
.where(cb.gt(rootEntry.get("expire"), Time.currentTime()));
|
||||||
|
|
||||||
|
TypedQuery<RevokedTokenEntity> allQuery = em.createQuery(all);
|
||||||
|
return allQuery.getResultStream().map(revokedTokenEntity -> new RevokedToken(revokedTokenEntity.getId(), revokedTokenEntity.getExpire()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void expireTokens() {
|
||||||
|
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||||
|
CriteriaDelete<RevokedTokenEntity> cd = cb.createCriteriaDelete(RevokedTokenEntity.class);
|
||||||
|
Root<RevokedTokenEntity> rootEntry = cd.from(RevokedTokenEntity.class);
|
||||||
|
cd.where(cb.lt(rootEntry.get("expire"), Time.currentTime()));
|
||||||
|
em.createQuery(cd).executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.jpa.session;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterProvider;
|
||||||
|
import org.keycloak.models.session.RevokedTokensPersisterProviderFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class JpaRevokedTokensPersisterProviderFactory implements RevokedTokensPersisterProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = "jpa";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RevokedTokenPersisterProvider create(KeycloakSession session) {
|
||||||
|
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||||
|
return new JpaRevokedTokensPersisterProvider(em);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int order() {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,4 +55,16 @@
|
||||||
<addNotNullConstraint tableName="USER_GROUP_MEMBERSHIP" columnName="MEMBERSHIP_TYPE" columnDataType="VARCHAR(255)"/>
|
<addNotNullConstraint tableName="USER_GROUP_MEMBERSHIP" columnName="MEMBERSHIP_TYPE" columnDataType="VARCHAR(255)"/>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="keycloak" id="31296-persist-revoked-access-tokens">
|
||||||
|
<createTable tableName="REVOKED_TOKEN">
|
||||||
|
<column name="ID" type="VARCHAR(255)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="EXPIRE" type="BIGINT">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_RT" tableName="REVOKED_TOKEN"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#
|
||||||
|
# Copyright 2024 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.jpa.session.JpaRevokedTokensPersisterProviderFactory
|
|
@ -50,6 +50,7 @@
|
||||||
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
|
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
|
||||||
<class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
|
<class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
|
||||||
<class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
|
<class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
|
||||||
|
<class>org.keycloak.models.jpa.entities.RevokedTokenEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.GroupEntity</class>
|
<class>org.keycloak.models.jpa.entities.GroupEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
|
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
|
||||||
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
|
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 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.session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Alexander Schwartz
|
||||||
|
*/
|
||||||
|
public record RevokedToken(String tokenId, long expiry) {
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.session;
|
||||||
|
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to revoke a token, so they will be available even after the restart of Keycloak.
|
||||||
|
* The store can be optimized in a way that entries are only added and are only removed by expiry.
|
||||||
|
*
|
||||||
|
* The first Keycloak instance starting up will re-load all expired tokens from it.
|
||||||
|
*
|
||||||
|
* @author Alexander Schwartz
|
||||||
|
*/
|
||||||
|
public interface RevokedTokenPersisterProvider extends Provider {
|
||||||
|
|
||||||
|
/** Revoke a token with a given ID */
|
||||||
|
void revokeToken(String tokenId, long lifetime);
|
||||||
|
|
||||||
|
Stream<RevokedToken> getAllRevokedTokens();
|
||||||
|
|
||||||
|
void expireTokens();
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.session;
|
||||||
|
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
import org.keycloak.provider.Spi;
|
||||||
|
|
||||||
|
public class RevokedTokenPersisterSpi implements Spi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInternal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "revokedTokenPersister";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Provider> getProviderClass() {
|
||||||
|
return RevokedTokenPersisterProvider.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||||
|
return RevokedTokensPersisterProviderFactory.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.session;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
|
||||||
|
public interface RevokedTokensPersisterProviderFactory extends ProviderFactory<RevokedTokenPersisterProvider> {
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.scheduled;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterProvider;
|
||||||
|
import org.keycloak.timer.ScheduledTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all expired revoked tokens.
|
||||||
|
*/
|
||||||
|
public class ClearExpiredRevokedTokens implements ScheduledTask {
|
||||||
|
|
||||||
|
protected static final Logger logger = Logger.getLogger(ClearExpiredRevokedTokens.class);
|
||||||
|
|
||||||
|
public static final String TASK_NAME = "ClearExpiredRevokedTokens";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(KeycloakSession session) {
|
||||||
|
long currentTimeMillis = Time.currentTimeMillis();
|
||||||
|
|
||||||
|
session.getProvider(RevokedTokenPersisterProvider.class).expireTokens();
|
||||||
|
|
||||||
|
long took = Time.currentTimeMillis() - currentTimeMillis;
|
||||||
|
logger.debugf("%s finished in %d ms", getTaskName(), took);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTaskName() {
|
||||||
|
return TASK_NAME;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,4 +23,5 @@ org.keycloak.storage.client.ClientStorageProviderSpi
|
||||||
org.keycloak.storage.group.GroupStorageProviderSpi
|
org.keycloak.storage.group.GroupStorageProviderSpi
|
||||||
org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi
|
org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi
|
||||||
org.keycloak.models.session.UserSessionPersisterSpi
|
org.keycloak.models.session.UserSessionPersisterSpi
|
||||||
|
org.keycloak.models.session.RevokedTokenPersisterSpi
|
||||||
org.keycloak.cluster.ClusterSpi
|
org.keycloak.cluster.ClusterSpi
|
||||||
|
|
|
@ -28,14 +28,17 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public interface SingleUseObjectProvider extends Provider {
|
public interface SingleUseObjectProvider extends Provider {
|
||||||
|
|
||||||
// suffix to a key to indicate that token is considered revoked
|
/**
|
||||||
|
* Suffix to a key to indicate that token is considered revoked.
|
||||||
|
* For revoked tokens, only the methods {@link #put} and {@link #contains} must be used.
|
||||||
|
*/
|
||||||
String REVOKED_KEY = ".revoked";
|
String REVOKED_KEY = ".revoked";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
|
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
|
||||||
* @param key
|
* @param key String
|
||||||
* @param lifespanSeconds
|
* @param lifespanSeconds Minimum lifespan for which successfully added key will be kept in the cache.
|
||||||
* @param notes
|
* @param notes For revoked tokens, this must be an empty Map.
|
||||||
*/
|
*/
|
||||||
void put(String key, long lifespanSeconds, Map<String, String> notes);
|
void put(String key, long lifespanSeconds, Map<String, String> notes);
|
||||||
|
|
||||||
|
@ -51,7 +54,7 @@ public interface SingleUseObjectProvider extends Provider {
|
||||||
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
|
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
|
||||||
* is allowed to succeed and return data back. It can't happen that both will succeed.
|
* is allowed to succeed and return data back. It can't happen that both will succeed.
|
||||||
*
|
*
|
||||||
* @param key
|
* @param key String
|
||||||
* @return context data associated to the key. It returns {@code null} if there are no context data available.
|
* @return context data associated to the key. It returns {@code null} if there are no context data available.
|
||||||
*/
|
*/
|
||||||
Map<String, String> remove(String key);
|
Map<String, String> remove(String key);
|
||||||
|
|
|
@ -28,7 +28,9 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSp
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
|
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
|
||||||
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
|
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
|
||||||
import org.keycloak.models.dblock.DBLockSpi;
|
import org.keycloak.models.dblock.DBLockSpi;
|
||||||
|
import org.keycloak.models.jpa.session.JpaRevokedTokensPersisterProviderFactory;
|
||||||
import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory;
|
import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory;
|
||||||
|
import org.keycloak.models.session.RevokedTokenPersisterSpi;
|
||||||
import org.keycloak.models.session.UserSessionPersisterSpi;
|
import org.keycloak.models.session.UserSessionPersisterSpi;
|
||||||
import org.keycloak.migration.MigrationProviderFactory;
|
import org.keycloak.migration.MigrationProviderFactory;
|
||||||
import org.keycloak.migration.MigrationSpi;
|
import org.keycloak.migration.MigrationSpi;
|
||||||
|
@ -61,6 +63,7 @@ public class Jpa extends KeycloakModelParameters {
|
||||||
.add(JpaUpdaterSpi.class)
|
.add(JpaUpdaterSpi.class)
|
||||||
.add(LiquibaseConnectionSpi.class)
|
.add(LiquibaseConnectionSpi.class)
|
||||||
.add(UserSessionPersisterSpi.class)
|
.add(UserSessionPersisterSpi.class)
|
||||||
|
.add(RevokedTokenPersisterSpi.class)
|
||||||
|
|
||||||
.add(DatastoreSpi.class)
|
.add(DatastoreSpi.class)
|
||||||
|
|
||||||
|
@ -92,6 +95,7 @@ public class Jpa extends KeycloakModelParameters {
|
||||||
.add(LiquibaseConnectionProviderFactory.class)
|
.add(LiquibaseConnectionProviderFactory.class)
|
||||||
.add(LiquibaseDBLockProviderFactory.class)
|
.add(LiquibaseDBLockProviderFactory.class)
|
||||||
.add(JpaUserSessionPersisterProviderFactory.class)
|
.add(JpaUserSessionPersisterProviderFactory.class)
|
||||||
|
.add(JpaRevokedTokensPersisterProviderFactory.class)
|
||||||
|
|
||||||
//required for migrateModel
|
//required for migrateModel
|
||||||
.add(MigrationProviderFactory.class)
|
.add(MigrationProviderFactory.class)
|
||||||
|
|
|
@ -27,9 +27,11 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.SingleUseObjectProvider;
|
import org.keycloak.models.SingleUseObjectProvider;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.scheduled.ClearExpiredRevokedTokens;
|
||||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||||
import org.keycloak.testsuite.model.RequireProvider;
|
import org.keycloak.testsuite.model.RequireProvider;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -163,6 +165,47 @@ public class SingleUseObjectModelTest extends KeycloakModelTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRevokedTokenIsPresentAfterRestartAndEventuallyExpires() {
|
||||||
|
String revokedKey = UUID.randomUUID() + SingleUseObjectProvider.REVOKED_KEY;
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
|
||||||
|
singleUseStore.put(revokedKey, 60, Collections.emptyMap());
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate restart
|
||||||
|
reinitializeKeycloakSessionFactory();
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
|
||||||
|
assertThat(singleUseStore.contains(revokedKey), Matchers.is(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeOffset(120);
|
||||||
|
|
||||||
|
// simulate restart
|
||||||
|
reinitializeKeycloakSessionFactory();
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
|
||||||
|
// not loaded as it is too old
|
||||||
|
assertThat(singleUseStore.contains(revokedKey), Matchers.is(false));
|
||||||
|
|
||||||
|
// remove it from the database
|
||||||
|
new ClearExpiredRevokedTokens().run(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeOffset(0);
|
||||||
|
|
||||||
|
inComittedTransaction(session -> {
|
||||||
|
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
|
||||||
|
// not loaded as it has been removed from the database
|
||||||
|
assertThat(singleUseStore.contains(revokedKey), Matchers.is(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCluster() throws InterruptedException {
|
public void testCluster() throws InterruptedException {
|
||||||
AtomicInteger index = new AtomicInteger();
|
AtomicInteger index = new AtomicInteger();
|
||||||
|
|
Loading…
Reference in a new issue