Merge pull request #2226 from mposolda/master

KEYCLOAK-1989 Refreshing offline tokens didn't work correctly in clus…
This commit is contained in:
Marek Posolda 2016-02-12 13:32:21 +01:00
commit a25658fb31
10 changed files with 74 additions and 39 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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