Merge pull request #2226 from mposolda/master
KEYCLOAK-1989 Refreshing offline tokens didn't work correctly in clus…
This commit is contained in:
commit
a25658fb31
10 changed files with 74 additions and 39 deletions
|
@ -24,6 +24,7 @@ import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.*;
|
import org.keycloak.models.sessions.infinispan.entities.*;
|
||||||
|
import org.keycloak.models.sessions.infinispan.initializer.TimeAwareInitializerState;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.*;
|
import org.keycloak.models.sessions.infinispan.stream.*;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.RealmInfoUtil;
|
import org.keycloak.models.utils.RealmInfoUtil;
|
||||||
|
@ -410,6 +411,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
|
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getClusterStartupTime() {
|
||||||
|
TimeAwareInitializerState state = (TimeAwareInitializerState) offlineSessionCache.get(InfinispanUserSessionProviderFactory.SESSION_INITIALIZER_STATE_KEY);
|
||||||
|
int startTime;
|
||||||
|
if (state == null) {
|
||||||
|
log.warn("Cluster startup time not yet available. Fallback to local startup time");
|
||||||
|
startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
|
||||||
|
} else {
|
||||||
|
startTime = state.getClusterStartupTime();
|
||||||
|
}
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,9 @@ import org.keycloak.provider.ProviderEventListener;
|
||||||
|
|
||||||
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
|
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
|
||||||
|
|
||||||
|
private static final String STATE_KEY_PREFIX = "initializerState";
|
||||||
|
public static final String SESSION_INITIALIZER_STATE_KEY = STATE_KEY_PREFIX + "::offlineUserSessions";
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
|
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
|
||||||
|
|
||||||
private Config.Scope config;
|
private Config.Scope config;
|
||||||
|
@ -84,7 +87,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||||
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
|
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
|
||||||
|
|
||||||
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
|
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, SESSION_INITIALIZER_STATE_KEY);
|
||||||
initializer.initCache();
|
initializer.initCache();
|
||||||
initializer.loadPersistentSessions();
|
initializer.loadPersistentSessions();
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
|
||||||
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
|
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
|
||||||
import org.infinispan.remoting.transport.Transport;
|
import org.infinispan.remoting.transport.Transport;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.KeycloakSessionTask;
|
import org.keycloak.models.KeycloakSessionTask;
|
||||||
|
@ -51,8 +52,6 @@ public class InfinispanUserSessionInitializer {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
|
private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
|
||||||
|
|
||||||
private static final String STATE_KEY_PREFIX = "initializerState";
|
|
||||||
|
|
||||||
private final KeycloakSessionFactory sessionFactory;
|
private final KeycloakSessionFactory sessionFactory;
|
||||||
private final Cache<String, SessionEntity> cache;
|
private final Cache<String, SessionEntity> cache;
|
||||||
private final SessionLoader sessionLoader;
|
private final SessionLoader sessionLoader;
|
||||||
|
@ -60,21 +59,18 @@ public class InfinispanUserSessionInitializer {
|
||||||
private final int sessionsPerSegment;
|
private final int sessionsPerSegment;
|
||||||
private final String stateKey;
|
private final String stateKey;
|
||||||
|
|
||||||
private volatile CountDownLatch latch = new CountDownLatch(1);
|
|
||||||
|
|
||||||
|
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKey) {
|
||||||
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) {
|
|
||||||
this.sessionFactory = sessionFactory;
|
this.sessionFactory = sessionFactory;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.sessionLoader = sessionLoader;
|
this.sessionLoader = sessionLoader;
|
||||||
this.maxErrors = maxErrors;
|
this.maxErrors = maxErrors;
|
||||||
this.sessionsPerSegment = sessionsPerSegment;
|
this.sessionsPerSegment = sessionsPerSegment;
|
||||||
this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix;
|
this.stateKey = stateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initCache() {
|
public void initCache() {
|
||||||
this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
|
this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
|
||||||
cache.getCacheManager().addListener(new ViewChangeListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,7 +82,7 @@ public class InfinispanUserSessionInitializer {
|
||||||
while (!isFinished()) {
|
while (!isFinished()) {
|
||||||
if (!isCoordinator()) {
|
if (!isCoordinator()) {
|
||||||
try {
|
try {
|
||||||
latch.await(500, TimeUnit.MILLISECONDS);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException ie) {
|
} catch (InterruptedException ie) {
|
||||||
log.error("Interrupted", ie);
|
log.error("Interrupted", ie);
|
||||||
}
|
}
|
||||||
|
@ -104,8 +100,10 @@ public class InfinispanUserSessionInitializer {
|
||||||
|
|
||||||
|
|
||||||
private InitializerState getOrCreateInitializerState() {
|
private InitializerState getOrCreateInitializerState() {
|
||||||
InitializerState state = (InitializerState) cache.get(stateKey);
|
TimeAwareInitializerState state = (TimeAwareInitializerState) cache.get(stateKey);
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
|
int startTime = (int)(sessionFactory.getServerStartupTimestamp() / 1000);
|
||||||
|
|
||||||
final int[] count = new int[1];
|
final int[] count = new int[1];
|
||||||
|
|
||||||
// Rather use separate transactions for update and counting
|
// Rather use separate transactions for update and counting
|
||||||
|
@ -113,7 +111,7 @@ public class InfinispanUserSessionInitializer {
|
||||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run(KeycloakSession session) {
|
public void run(KeycloakSession session) {
|
||||||
sessionLoader.init(session);
|
sessionLoader.init(session, startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -126,8 +124,9 @@ public class InfinispanUserSessionInitializer {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state = new InitializerState();
|
state = new TimeAwareInitializerState();
|
||||||
state.init(count[0], sessionsPerSegment);
|
state.init(count[0], sessionsPerSegment);
|
||||||
|
state.setClusterStartupTime(startTime);
|
||||||
saveStateToCache(state);
|
saveStateToCache(state);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
@ -251,24 +250,6 @@ public class InfinispanUserSessionInitializer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Listener
|
|
||||||
public class ViewChangeListener {
|
|
||||||
|
|
||||||
@ViewChanged
|
|
||||||
public void viewChanged(ViewChangedEvent event) {
|
|
||||||
boolean isCoordinator = isCoordinator();
|
|
||||||
log.debug("View Changed: is coordinator: " + isCoordinator);
|
|
||||||
|
|
||||||
if (isCoordinator) {
|
|
||||||
latch.countDown();
|
|
||||||
latch = new CountDownLatch(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class WorkerResult implements Serializable {
|
public static class WorkerResult implements Serializable {
|
||||||
|
|
||||||
private Integer segment;
|
private Integer segment;
|
||||||
|
|
|
@ -33,14 +33,13 @@ public class OfflineUserSessionLoader implements SessionLoader {
|
||||||
private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
|
private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(KeycloakSession session) {
|
public void init(KeycloakSession session, int clusterStartupTime) {
|
||||||
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||||
int startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
|
|
||||||
|
|
||||||
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", startTime);
|
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", clusterStartupTime);
|
||||||
|
|
||||||
persister.clearDetachedUserSessions();
|
persister.clearDetachedUserSessions();
|
||||||
persister.updateAllTimestamps(startTime);
|
persister.updateAllTimestamps(clusterStartupTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
*/
|
*/
|
||||||
public interface SessionLoader extends Serializable {
|
public interface SessionLoader extends Serializable {
|
||||||
|
|
||||||
void init(KeycloakSession session);
|
void init(KeycloakSession session, int clusterStartupTime);
|
||||||
|
|
||||||
int getSessionsCount(KeycloakSession session);
|
int getSessionsCount(KeycloakSession session);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.sessions.infinispan.initializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class TimeAwareInitializerState extends InitializerState {
|
||||||
|
|
||||||
|
private int clusterStartupTime;
|
||||||
|
|
||||||
|
public int getClusterStartupTime() {
|
||||||
|
return clusterStartupTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClusterStartupTime(int clusterStartupTime) {
|
||||||
|
this.clusterStartupTime = clusterStartupTime;
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,9 @@ public interface UserSessionProvider extends Provider {
|
||||||
void removeClientInitialAccessModel(RealmModel realm, String id);
|
void removeClientInitialAccessModel(RealmModel realm, String id);
|
||||||
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
|
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
|
||||||
|
|
||||||
|
// Will use startup time of this server in non-cluster environment
|
||||||
|
int getClusterStartupTime();
|
||||||
|
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,9 +193,9 @@ public class TokenManager {
|
||||||
int currentTime = Time.currentTime();
|
int currentTime = Time.currentTime();
|
||||||
|
|
||||||
if (realm.isRevokeRefreshToken()) {
|
if (realm.isRevokeRefreshToken()) {
|
||||||
int serverStartupTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
|
int clusterStartupTime = session.sessions().getClusterStartupTime();
|
||||||
|
|
||||||
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (serverStartupTime != validation.clientSession.getTimestamp())) {
|
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (clusterStartupTime != validation.clientSession.getTimestamp())) {
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,6 @@ public class KeycloakApplication extends Application {
|
||||||
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
||||||
|
|
||||||
migrateModel();
|
migrateModel();
|
||||||
sessionFactory.publish(new PostMigrationEvent());
|
|
||||||
|
|
||||||
boolean bootstrapAdminUser = false;
|
boolean bootstrapAdminUser = false;
|
||||||
|
|
||||||
|
@ -138,6 +137,8 @@ public class KeycloakApplication extends Application {
|
||||||
session.close();
|
session.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionFactory.publish(new PostMigrationEvent());
|
||||||
|
|
||||||
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
||||||
|
|
||||||
setupScheduledTasks(sessionFactory);
|
setupScheduledTasks(sessionFactory);
|
||||||
|
|
|
@ -83,7 +83,7 @@ public class UserSessionInitializerTest {
|
||||||
|
|
||||||
// Create and persist offline sessions
|
// Create and persist offline sessions
|
||||||
int started = Time.currentTime();
|
int started = Time.currentTime();
|
||||||
int serverStartTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
|
int serverStartTime = session.sessions().getClusterStartupTime();
|
||||||
|
|
||||||
for (UserSessionModel origSession : origSessions) {
|
for (UserSessionModel origSession : origSessions) {
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
|
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
|
||||||
|
|
Loading…
Reference in a new issue