KEYCLOAK-16378 User / client session map store

Co-authored-by: Martin Kanis <mkanis@redhat.com>
Co-authored-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
Martin Kanis 2021-01-29 13:04:35 +01:00 committed by Hynek Mlnařík
parent 5c4753ef20
commit 515bfb5064
81 changed files with 4945 additions and 1406 deletions

View file

@ -160,7 +160,7 @@ jobs:
run: |
declare -A PARAMS TESTGROUP
PARAMS["quarkus"]="-Pauth-server-quarkus"
PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map -Dkeycloak.authorization.provider=map"
PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.userSession.provider=map -Dkeycloak.loginFailure.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map -Dkeycloak.realm.provider=map -Dkeycloak.authorization.provider=map"
PARAMS["wildfly"]="-Pauth-server-wildfly"
TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r"
TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b"

View file

@ -31,7 +31,7 @@ public class StackUtil {
return getShortStackTrace("\n ");
}
private static final Pattern IGNORED = Pattern.compile("sun\\.|java\\.(lang|util|stream)\\.|org\\.jboss\\.(arquillian|logging).|org.apache.maven.surefire");
private static final Pattern IGNORED = Pattern.compile("sun\\.|java\\.(lang|util|stream)\\.|org\\.jboss\\.(arquillian|logging).|org.apache.maven.surefire|org\\.junit\\.|org.keycloak.testsuite.model.KeycloakModelTest\\.");
private static final StringBuilder EMPTY = new StringBuilder(0);
/**

View file

@ -114,7 +114,7 @@ public class Config {
@Override
public Integer getInt(String key, Integer defaultValue) {
String v = get(key, null);
return v != null ? Integer.parseInt(v) : defaultValue;
return v != null ? Integer.valueOf(v) : defaultValue;
}
@Override
@ -125,7 +125,7 @@ public class Config {
@Override
public Long getLong(String key, Long defaultValue) {
String v = get(key, null);
return v != null ? Long.parseLong(v) : defaultValue;
return v != null ? Long.valueOf(v) : defaultValue;
}
@Override
@ -137,7 +137,7 @@ public class Config {
public Boolean getBoolean(String key, Boolean defaultValue) {
String v = get(key, null);
if (v != null) {
return Boolean.parseBoolean(v);
return Boolean.valueOf(v);
} else {
return defaultValue;
}

View file

@ -117,7 +117,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public String getId() {
return null;
return entity.getId().toString();
}
@Override

View file

@ -0,0 +1,157 @@
/*
* Copyright 2021 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;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserLoginFailureProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.Tasks;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import java.util.concurrent.Future;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
/**
*
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class InfinispanUserLoginFailureProvider implements UserLoginFailureProvider {
private static final Logger log = Logger.getLogger(InfinispanUserLoginFailureProvider.class);
protected final KeycloakSession session;
protected final Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache;
protected final InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> loginFailuresTx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
public InfinispanUserLoginFailureProvider(KeycloakSession session,
RemoteCacheInvoker remoteCacheInvoker,
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache) {
this.session = session;
this.loginFailureCache = loginFailureCache;
this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(loginFailuresTx);
}
@Override
public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
log.tracef("getUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
LoginFailureEntity entity = getLoginFailureEntity(key);
return wrap(key, entity);
}
@Override
public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) {
log.tracef("addUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
LoginFailureEntity entity = new LoginFailureEntity();
entity.setRealmId(realm.getId());
entity.setUserId(userId);
SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = Tasks.addIfAbsentSync();
loginFailuresTx.addTask(key, createLoginFailureTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
return wrap(key, entity);
}
@Override
public void removeUserLoginFailure(RealmModel realm, String userId) {
log.tracef("removeUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
SessionUpdateTask<LoginFailureEntity> removeTask = Tasks.removeSync();
loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask);
}
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
log.tracef("removeAllUserLoginFailures(%s)%s", realm, getShortStackTrace());
clusterEventsSenderTx.addEvent(
RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserLoginFailureProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true),
ClusterProvider.DCNotify.LOCAL_DC_ONLY);
}
protected void removeAllLocalUserLoginFailuresEvent(String realmId) {
log.tracef("removeAllLocalUserLoginFailuresEvent(%s)%s", realmId, getShortStackTrace());
FuturesHelper futures = new FuturesHelper();
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCache = CacheDecorators.localCache(loginFailureCache);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
localCacheStoreIgnore
.entrySet()
.stream()
.filter(UserLoginFailurePredicate.create(realmId))
.map(Mappers.loginFailureId())
.forEach(loginFailureKey -> {
// Remove loginFailure from remoteCache too. Use removeAsync for better perf
Future future = localCache.removeAsync(loginFailureKey);
futures.addTask(future);
});
futures.waitForAllToFinish();
log.debugf("Removed %d login failures in realm %s", futures.size(), realmId);
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null;
}
private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) {
InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> tx = getLoginFailuresTx();
SessionEntityWrapper<LoginFailureEntity> entityWrapper = tx.get(key);
return entityWrapper==null ? null : entityWrapper.getEntity();
}
InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> getLoginFailuresTx() {
return loginFailuresTx;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,214 @@
/*
* Copyright 2021 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;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.UserLoginFailureProvider;
import org.keycloak.models.UserLoginFailureProviderFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import java.io.Serializable;
import java.util.Set;
import java.util.function.BiFunction;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class InfinispanUserLoginFailureProviderFactory implements UserLoginFailureProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanUserLoginFailureProviderFactory.class);
public static final String PROVIDER_ID = "infinispan";
public static final String REALM_REMOVED_SESSION_EVENT = "REALM_REMOVED_EVENT_SESSIONS";
public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT";
private Config.Scope config;
private RemoteCacheInvoker remoteCacheInvoker;
@Override
public UserLoginFailureProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
return new InfinispanUserLoginFailureProvider(session, remoteCacheInvoker, loginFailures);
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
this.remoteCacheInvoker = new RemoteCacheInvoker();
factory.register(event -> {
if (event instanceof PostMigrationEvent) {
KeycloakModelUtils.runJobInTransaction(factory, (KeycloakSession session) -> {
checkRemoteCaches(session);
registerClusterListeners(session);
loadLoginFailuresFromRemoteCaches(session);
});
} else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
UserLoginFailureProvider provider = userRemovedEvent.getKeycloakSession().getProvider(UserLoginFailureProvider.class, getId());
provider.removeUserLoginFailure(userRemovedEvent.getRealm(), userRemovedEvent.getUser().getId());
}
});
}
protected void registerClusterListeners(KeycloakSession session) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_REMOVED_SESSION_EVENT,
new AbstractUserSessionClusterListener<RealmRemovedSessionEvent, UserLoginFailureProvider>(sessionFactory, UserLoginFailureProvider.class) {
@Override
protected void eventReceived(KeycloakSession session, UserLoginFailureProvider provider, RealmRemovedSessionEvent sessionEvent) {
if (provider instanceof InfinispanUserLoginFailureProvider) {
((InfinispanUserLoginFailureProvider) provider).removeAllLocalUserLoginFailuresEvent(sessionEvent.getRealmId());
}
}
});
cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT,
new AbstractUserSessionClusterListener<RemoveAllUserLoginFailuresEvent, UserLoginFailureProvider>(sessionFactory, UserLoginFailureProvider.class) {
@Override
protected void eventReceived(KeycloakSession session, UserLoginFailureProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) {
if (provider instanceof InfinispanUserLoginFailureProvider) {
((InfinispanUserLoginFailureProvider) provider).removeAllLocalUserLoginFailuresEvent(sessionEvent.getRealmId());
}
}
});
log.debug("Registered cluster listeners");
}
protected void checkRemoteCaches(KeycloakSession session) {
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
checkRemoteCache(session, loginFailuresCache, (RealmModel realm) ->
Time.toMillis(realm.getMaxDeltaTimeSeconds()), SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
}
private <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader,
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
if (remoteStores.isEmpty()) {
log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
return null;
} else {
log.infof("Remote store configured for cache '%s'", ispnCache.getName());
RemoteCache<K, SessionEntityWrapper<V>> remoteCache = (RemoteCache) remoteStores.iterator().next().getRemoteCache();
if (remoteCache == null) {
throw new IllegalStateException("No remote cache available for the infinispan cache: " + ispnCache.getName());
}
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache, lifespanMsLoader, maxIdleTimeMsLoader);
remoteCache.addClientListener(hotrodListener);
return remoteCache;
}
}
// Max count of worker errors. Initialization will end with exception when this number is reached
private int getMaxErrors() {
return config.getInt("maxErrors", 20);
}
// Count of sessions to be computed in each segment
private int getSessionsPerSegment() {
return config.getInt("sessionsPerSegment", 64);
}
private void loadLoginFailuresFromRemoteCaches(KeycloakSession session) {
for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) {
loadLoginFailuresFromRemoteCaches(session.getKeycloakSessionFactory(), cacheName, getSessionsPerSegment(), getMaxErrors());
}
}
private void loadLoginFailuresFromRemoteCaches(final KeycloakSessionFactory sessionFactory, String cacheName, final int sessionsPerSegment, final int maxErrors) {
log.debugf("Check pre-loading sessions from remote cache '%s'", cacheName);
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
InfinispanCacheInitializer initializer = new InfinispanCacheInitializer(sessionFactory, workCache,
new RemoteCacheSessionsLoader(cacheName, sessionsPerSegment), "remoteCacheLoad::" + cacheName, sessionsPerSegment, maxErrors);
initializer.initCache();
initializer.loadSessions();
}
});
log.debugf("Pre-loading login failures from remote cache '%s' finished", cacheName);
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -32,7 +32,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
@ -46,17 +45,13 @@ import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedT
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
@ -95,13 +90,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
protected final Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache;
protected final Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache;
protected final Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache;
protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
protected final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionTx;
protected final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> offlineClientSessionTx;
protected final InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> loginFailuresTx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
@ -121,23 +114,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache) {
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache) {
this.session = session;
this.sessionCache = sessionCache;
this.clientSessionCache = clientSessionCache;
this.offlineSessionCache = offlineSessionCache;
this.offlineClientSessionCache = offlineClientSessionCache;
this.loginFailureCache = loginFailureCache;
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
this.lastSessionRefreshStore = lastSessionRefreshStore;
@ -151,7 +140,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
session.getTransactionManager().enlistAfterCompletion(clientSessionTx);
session.getTransactionManager().enlistAfterCompletion(offlineClientSessionTx);
session.getTransactionManager().enlistAfterCompletion(loginFailuresTx);
}
protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
@ -182,6 +170,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return persisterLastSessionRefreshStore;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
@Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache);
@ -528,72 +521,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline);
}
@Override
public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
LoginFailureEntity entity = getLoginFailureEntity(key);
return wrap(key, entity);
}
private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) {
InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> tx = getLoginFailuresTx();
SessionEntityWrapper<LoginFailureEntity> entityWrapper = tx.get(key);
return entityWrapper==null ? null : entityWrapper.getEntity();
}
@Override
public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) {
LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
LoginFailureEntity entity = new LoginFailureEntity();
entity.setRealmId(realm.getId());
entity.setUserId(userId);
SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = Tasks.addIfAbsentSync();
loginFailuresTx.addTask(key, createLoginFailureTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
return wrap(key, entity);
}
@Override
public void removeUserLoginFailure(RealmModel realm, String userId) {
SessionUpdateTask<LoginFailureEntity> removeTask = Tasks.removeSync();
loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask);
}
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
clusterEventsSenderTx.addEvent(
RemoveAllUserLoginFailuresEvent.createEvent(RemoveAllUserLoginFailuresEvent.class, InfinispanUserSessionProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, session, realm.getId(), true),
ClusterProvider.DCNotify.LOCAL_DC_ONLY);
}
protected void onRemoveAllUserLoginFailuresEvent(String realmId) {
removeAllLocalUserLoginFailuresEvent(realmId);
}
private void removeAllLocalUserLoginFailuresEvent(String realmId) {
FuturesHelper futures = new FuturesHelper();
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCache = CacheDecorators.localCache(loginFailureCache);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
localCacheStoreIgnore
.entrySet()
.stream()
.filter(UserLoginFailurePredicate.create(realmId))
.map(Mappers.loginFailureId())
.forEach(loginFailureKey -> {
// Remove loginFailure from remoteCache too. Use removeAsync for better perf
Future future = localCache.removeAsync(loginFailureKey);
futures.addTask(future);
});
futures.waitForAllToFinish();
log.debugf("Removed %d login failures in realm %s", futures.size(), realmId);
}
@Override
public void onRealmRemoved(RealmModel realm) {
// Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions.
@ -610,7 +537,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected void onRealmRemovedEvent(String realmId) {
removeLocalUserSessions(realmId, true);
removeLocalUserSessions(realmId, false);
removeAllLocalUserLoginFailuresEvent(realmId);
}
@Override
@ -633,8 +559,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
removeUserSessions(realm, user, true);
removeUserSessions(realm, user, false);
removeUserLoginFailure(realm, user.getId());
UserSessionPersisterProvider persisterProvider = session.getProvider(UserSessionPersisterProvider.class);
if (persisterProvider != null) {
persisterProvider.onUserRemoved(realm, user);
@ -653,10 +577,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask);
}
InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> getLoginFailuresTx() {
return loginFailuresTx;
}
UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
@ -669,10 +589,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, clientSessionUpdateTx, offline) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null;
}
UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) {
if (userSession instanceof UserSessionAdapter) {
if (!userSession.getRealm().equals(realm)) return null;

View file

@ -42,14 +42,11 @@ import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheIniti
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
@ -81,8 +78,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT";
public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT";
private Config.Scope config;
private RemoteCacheInvoker remoteCacheInvoker;
@ -98,11 +93,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore,
persisterLastSessionRefreshStore, keyGenerator,
cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, loginFailures);
persisterLastSessionRefreshStore, keyGenerator, cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache);
}
@Override
@ -112,7 +105,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void postInit(final KeycloakSessionFactory factory) {
factory.register(new ProviderEventListener() {
@Override
@ -202,38 +194,38 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<RealmRemovedSessionEvent>(sessionFactory) {
cluster.registerListener(REALM_REMOVED_SESSION_EVENT,
new AbstractUserSessionClusterListener<RealmRemovedSessionEvent, UserSessionProvider>(sessionFactory, UserSessionProvider.class) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
provider.onRealmRemovedEvent(sessionEvent.getRealmId());
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onRealmRemovedEvent(sessionEvent.getRealmId());
}
}
});
cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<ClientRemovedSessionEvent>(sessionFactory) {
cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT,
new AbstractUserSessionClusterListener<ClientRemovedSessionEvent, UserSessionProvider>(sessionFactory, UserSessionProvider.class) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
}
}
});
cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, new AbstractUserSessionClusterListener<RemoveUserSessionsEvent>(sessionFactory) {
cluster.registerListener(REMOVE_USER_SESSIONS_EVENT,
new AbstractUserSessionClusterListener<RemoveUserSessionsEvent, UserSessionProvider>(sessionFactory, UserSessionProvider.class) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId());
}
});
cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener<RemoveAllUserLoginFailuresEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) {
provider.onRemoveAllUserLoginFailuresEvent(sessionEvent.getRealmId());
protected void eventReceived(KeycloakSession session, UserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
if (provider instanceof InfinispanUserSessionProvider) {
((InfinispanUserSessionProvider) provider).onRemoveUserSessionsEvent(sessionEvent.getRealmId());
}
}
});
@ -276,11 +268,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
return Time.toMillis(realm.getOfflineSessionIdleTimeout());
}, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
return Time.toMillis(realm.getMaxDeltaTimeSeconds());
}, SessionTimeouts::getLoginFailuresLifespanMs, SessionTimeouts::getLoginFailuresMaxIdleMs);
}
private <K, V extends SessionEntity> RemoteCache checkRemoteCache(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader,

View file

@ -27,11 +27,11 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
*/
public class UserLoginFailureAdapter implements UserLoginFailureModel {
private InfinispanUserSessionProvider provider;
private InfinispanUserLoginFailureProvider provider;
private LoginFailureKey key;
private LoginFailureEntity entity;
public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, LoginFailureKey key, LoginFailureEntity entity) {
public UserLoginFailureAdapter(InfinispanUserLoginFailureProvider provider, LoginFailureKey key, LoginFailureEntity entity) {
this.provider = provider;
this.key = key;
this.entity = entity;

View file

@ -24,30 +24,31 @@ import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.TopologyInfo;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractUserSessionClusterListener<SE extends SessionClusterEvent> implements ClusterListener {
public abstract class AbstractUserSessionClusterListener<SE extends SessionClusterEvent, T extends Provider> implements ClusterListener {
private static final Logger log = Logger.getLogger(AbstractUserSessionClusterListener.class);
private final KeycloakSessionFactory sessionFactory;
public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory) {
private final Class<T> providerClazz;
public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory, Class<T> providerClazz) {
this.sessionFactory = sessionFactory;
this.providerClazz = providerClazz;
}
@Override
public void eventReceived(ClusterEvent event) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> {
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) session.getProvider(UserSessionProvider.class, InfinispanUserSessionProviderFactory.PROVIDER_ID);
T provider = session.getProvider(providerClazz);
SE sessionEvent = (SE) event;
boolean shouldResendEvent = shouldResendEvent(session, sessionEvent);
@ -65,7 +66,7 @@ public abstract class AbstractUserSessionClusterListener<SE extends SessionClust
});
}
protected abstract void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, SE sessionEvent);
protected abstract void eventReceived(KeycloakSession session, T provider, SE sessionEvent);
private boolean shouldResendEvent(KeycloakSession session, SessionClusterEvent event) {

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.sessions.infinispan.InfinispanUserLoginFailureProviderFactory

View file

@ -0,0 +1,129 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Objects;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractUserLoginFailureEntity<K> implements AbstractEntity<K> {
private K id;
private String realmId;
private String userId;
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
private int failedLoginNotBefore;
private int numFailures;
private long lastFailure;
private String lastIPFailure;
public AbstractUserLoginFailureEntity() {
this.id = null;
this.realmId = null;
this.userId = null;
}
public AbstractUserLoginFailureEntity(K id, String realmId, String userId) {
this.id = id;
this.realmId = realmId;
this.userId = userId;
}
@Override
public K getId() {
return this.id;
}
@Override
public boolean isUpdated() {
return this.updated;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.updated |= !Objects.equals(this.realmId, realmId);
this.realmId = realmId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.updated |= !Objects.equals(this.userId, userId);
this.userId = userId;
}
public int getFailedLoginNotBefore() {
return failedLoginNotBefore;
}
public void setFailedLoginNotBefore(int failedLoginNotBefore) {
this.updated |= this.failedLoginNotBefore != failedLoginNotBefore;
this.failedLoginNotBefore = failedLoginNotBefore;
}
public int getNumFailures() {
return numFailures;
}
public void setNumFailures(int numFailures) {
this.updated |= this.numFailures != numFailures;
this.numFailures = numFailures;
}
public long getLastFailure() {
return lastFailure;
}
public void setLastFailure(long lastFailure) {
this.updated |= this.lastFailure != lastFailure;
this.lastFailure = lastFailure;
}
public String getLastIPFailure() {
return lastIPFailure;
}
public void setLastIPFailure(String lastIPFailure) {
this.updated |= !Objects.equals(this.lastIPFailure, lastIPFailure);
this.lastIPFailure = lastIPFailure;
}
public void clearFailures() {
this.updated |= this.failedLoginNotBefore != 0 || this.numFailures != 0 ||
this.lastFailure != 0l || this.lastIPFailure != null;
this.failedLoginNotBefore = this.numFailures = 0;
this.lastFailure = 0l;
this.lastIPFailure = null;
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Objects;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractUserLoginFailureModel <E extends AbstractEntity> implements UserLoginFailureModel {
protected final KeycloakSession session;
protected final RealmModel realm;
protected final E entity;
public AbstractUserLoginFailureModel(KeycloakSession session, RealmModel realm, E entity) {
Objects.requireNonNull(entity, "entity");
Objects.requireNonNull(realm, "realm");
this.session = session;
this.realm = realm;
this.entity = entity;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserLoginFailureModel)) return false;
MapUserLoginFailureAdapter that = (MapUserLoginFailureAdapter) o;
return Objects.equals(that.entity.getId(), entity.getId());
}
@Override
public int hashCode() {
return entity.getId().hashCode();
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserLoginFailureAdapter extends AbstractUserLoginFailureModel<MapUserLoginFailureEntity> {
public MapUserLoginFailureAdapter(KeycloakSession session, RealmModel realm, MapUserLoginFailureEntity entity) {
super(session, realm, entity);
}
@Override
public String getUserId() {
return entity.getUserId();
}
@Override
public int getFailedLoginNotBefore() {
return entity.getFailedLoginNotBefore();
}
@Override
public void setFailedLoginNotBefore(int notBefore) {
entity.setFailedLoginNotBefore(notBefore);
}
@Override
public int getNumFailures() {
return entity.getNumFailures();
}
@Override
public void incrementFailures() {
entity.setNumFailures(getNumFailures() + 1);
}
@Override
public void clearFailures() {
entity.clearFailures();
}
@Override
public long getLastFailure() {
return entity.getLastFailure();
}
@Override
public void setLastFailure(long lastFailure) {
entity.setLastFailure(lastFailure);
}
@Override
public String getLastIPFailure() {
return entity.getLastIPFailure();
}
@Override
public void setLastIPFailure(String ip) {
entity.setLastIPFailure(ip);
}
@Override
public String toString() {
return String.format("%s@%08x", entity.getId(), hashCode());
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import java.util.UUID;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserLoginFailureEntity extends AbstractUserLoginFailureEntity<UUID> {
protected MapUserLoginFailureEntity() {
super();
}
public MapUserLoginFailureEntity(UUID id, String realmId, String userId) {
super(id, realmId, userId);
}
}

View file

@ -0,0 +1,121 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserLoginFailureProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.map.common.Serialization;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import java.util.UUID;
import java.util.function.Function;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserLoginFailureProvider implements UserLoginFailureProvider {
private static final Logger LOG = Logger.getLogger(MapUserLoginFailureProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<UUID, MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureTx;
private final MapStorage<UUID, MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureStore;
public MapUserLoginFailureProvider(KeycloakSession session, MapStorage<UUID, MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureStore) {
this.session = session;
this.userLoginFailureStore = userLoginFailureStore;
userLoginFailureTx = userLoginFailureStore.createTransaction(session);
session.getTransactionManager().enlistAfterCompletion(userLoginFailureTx);
}
private Function<MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureEntityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> new MapUserLoginFailureAdapter(session, realm, registerEntityForChanges(origEntity));
}
private MapUserLoginFailureEntity registerEntityForChanges(MapUserLoginFailureEntity origEntity) {
MapUserLoginFailureEntity res = userLoginFailureTx.read(origEntity.getId(), id -> Serialization.from(origEntity));
userLoginFailureTx.updateIfChanged(origEntity.getId(), res, MapUserLoginFailureEntity::isUpdated);
return res;
}
@Override
public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
ModelCriteriaBuilder<UserLoginFailureModel> mcb = userLoginFailureStore.createCriteriaBuilder()
.compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId);
LOG.tracef("getUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
return userLoginFailureTx.getUpdatedNotRemoved(mcb)
.findFirst()
.map(userLoginFailureEntityToAdapterFunc(realm))
.orElse(null);
}
@Override
public UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) {
ModelCriteriaBuilder<UserLoginFailureModel> mcb = userLoginFailureStore.createCriteriaBuilder()
.compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId);
LOG.tracef("addUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
MapUserLoginFailureEntity userLoginFailureEntity = userLoginFailureTx.getUpdatedNotRemoved(mcb).findFirst().orElse(null);
if (userLoginFailureEntity == null) {
userLoginFailureEntity = new MapUserLoginFailureEntity(UUID.randomUUID(), realm.getId(), userId);
userLoginFailureTx.create(userLoginFailureEntity.getId(), userLoginFailureEntity);
}
return userLoginFailureEntityToAdapterFunc(realm).apply(userLoginFailureEntity);
}
@Override
public void removeUserLoginFailure(RealmModel realm, String userId) {
ModelCriteriaBuilder<UserLoginFailureModel> mcb = userLoginFailureStore.createCriteriaBuilder()
.compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserLoginFailureModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, userId);
LOG.tracef("removeUserLoginFailure(%s, %s)%s", realm, userId, getShortStackTrace());
userLoginFailureTx.delete(UUID.randomUUID(), mcb);
}
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
ModelCriteriaBuilder<UserLoginFailureModel> mcb = userLoginFailureStore.createCriteriaBuilder()
.compare(UserLoginFailureModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId());
LOG.tracef("removeAllUserLoginFailures(%s)%s", realm, getShortStackTrace());
userLoginFailureTx.delete(UUID.randomUUID(), mcb);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.loginFailure;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureProvider;
import org.keycloak.models.UserLoginFailureProviderFactory;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.MapStorageProvider;
import java.util.UUID;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserLoginFailureProviderFactory extends AbstractMapProviderFactory<UserLoginFailureProvider>
implements UserLoginFailureProviderFactory {
private MapStorage<UUID, MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureStore;
@Override
public void postInit(KeycloakSessionFactory factory) {
MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class);
userLoginFailureStore = sp.getStorage("userLoginFailures", UUID.class, MapUserLoginFailureEntity.class, UserLoginFailureModel.class);
factory.register(event -> {
if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
MapUserLoginFailureProvider provider = MapUserLoginFailureProviderFactory.this.create(userRemovedEvent.getKeycloakSession());
provider.removeUserLoginFailure(userRemovedEvent.getRealm(), userRemovedEvent.getUser().getId());
}
});
factory.register(event -> {
if (event instanceof RealmModel.RealmRemovedEvent) {
RealmModel.RealmRemovedEvent realmRemovedEvent = (RealmModel.RealmRemovedEvent) event;
MapUserLoginFailureProvider provider = MapUserLoginFailureProviderFactory.this.create(realmRemovedEvent.getKeycloakSession());
provider.removeAllUserLoginFailures(realmRemovedEvent.getRealm());
}
});
}
@Override
public MapUserLoginFailureProvider create(KeycloakSession session) {
return new MapUserLoginFailureProvider(session, userLoginFailureStore);
}
}

View file

@ -1547,4 +1547,9 @@ public class MapRealmAdapter extends AbstractRealmModel<MapRealmEntity> implemen
public OAuth2DeviceConfig getOAuth2DeviceConfig() {
return new OAuth2DeviceConfig(this);
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -21,12 +21,15 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.authSession.AbstractRootAuthenticationSessionEntity;
import org.keycloak.models.map.authorization.entity.AbstractPermissionTicketEntity;
import org.keycloak.models.map.authorization.entity.AbstractPolicyEntity;
@ -39,6 +42,9 @@ import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.group.AbstractGroupEntity;
import org.keycloak.models.map.realm.AbstractRealmEntity;
import org.keycloak.models.map.role.AbstractRoleEntity;
import org.keycloak.models.map.userSession.AbstractAuthenticatedClientSessionEntity;
import org.keycloak.models.map.loginFailure.AbstractUserLoginFailureEntity;
import org.keycloak.models.map.userSession.AbstractUserSessionEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.HashMap;
import java.util.Map;
@ -55,6 +61,8 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID;
/**
*
* @author hmlnarik
@ -73,6 +81,9 @@ public class MapFieldPredicates {
public static final Map<SearchableModelField<Scope>, UpdatePredicatesFunc<Object, AbstractScopeEntity<Object>, Scope>> AUTHZ_SCOPE_PREDICATES = basePredicates(Scope.SearchableFields.ID);
public static final Map<SearchableModelField<PermissionTicket>, UpdatePredicatesFunc<Object, AbstractPermissionTicketEntity<Object>, PermissionTicket>> AUTHZ_PERMISSION_TICKET_PREDICATES = basePredicates(PermissionTicket.SearchableFields.ID);
public static final Map<SearchableModelField<Policy>, UpdatePredicatesFunc<Object, AbstractPolicyEntity<Object>, Policy>> AUTHZ_POLICY_PREDICATES = basePredicates(Policy.SearchableFields.ID);
public static final Map<SearchableModelField<UserSessionModel>, UpdatePredicatesFunc<Object, AbstractUserSessionEntity<Object>, UserSessionModel>> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID);
public static final Map<SearchableModelField<AuthenticatedClientSessionModel>, UpdatePredicatesFunc<Object, AbstractAuthenticatedClientSessionEntity<Object>, AuthenticatedClientSessionModel>> CLIENT_SESSION_PREDICATES = basePredicates(AuthenticatedClientSessionModel.SearchableFields.ID);
public static final Map<SearchableModelField<UserLoginFailureModel>, UpdatePredicatesFunc<Object, AbstractUserLoginFailureEntity<Object>, UserLoginFailureModel>> USER_LOGIN_FAILURE_PREDICATES = basePredicates(UserLoginFailureModel.SearchableFields.ID);
@SuppressWarnings("unchecked")
private static final Map<Class<?>, Map> PREDICATES = new HashMap<>();
@ -154,6 +165,24 @@ public class MapFieldPredicates {
put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.SCOPE_ID, MapFieldPredicates::checkPolicyScopes);
put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.CONFIG, MapFieldPredicates::checkPolicyConfig);
put(AUTHZ_POLICY_PREDICATES, Policy.SearchableFields.ASSOCIATED_POLICY_ID, MapFieldPredicates::checkAssociatedPolicy);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, use -> use.getNote(CORRESPONDING_SESSION_ID));
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.REALM_ID, AbstractUserSessionEntity::getRealmId);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.USER_ID, AbstractUserSessionEntity::getUserId);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.CLIENT_ID, MapFieldPredicates::checkUserSessionContainsAuthenticatedClientSession);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.BROKER_SESSION_ID, AbstractUserSessionEntity::getBrokerSessionId);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.BROKER_USER_ID, AbstractUserSessionEntity::getBrokerUserId);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.IS_OFFLINE, AbstractUserSessionEntity::isOffline);
put(USER_SESSION_PREDICATES, UserSessionModel.SearchableFields.LAST_SESSION_REFRESH, AbstractUserSessionEntity::getLastSessionRefresh);
put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.REALM_ID, AbstractAuthenticatedClientSessionEntity::getRealmId);
put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.CLIENT_ID, AbstractAuthenticatedClientSessionEntity::getClientId);
put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, AbstractAuthenticatedClientSessionEntity::getUserSessionId);
put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.IS_OFFLINE, AbstractAuthenticatedClientSessionEntity::isOffline);
put(CLIENT_SESSION_PREDICATES, AuthenticatedClientSessionModel.SearchableFields.TIMESTAMP, AbstractAuthenticatedClientSessionEntity::getTimestamp);
put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.REALM_ID, AbstractUserLoginFailureEntity::getRealmId);
put(USER_LOGIN_FAILURE_PREDICATES, UserLoginFailureModel.SearchableFields.USER_ID, AbstractUserLoginFailureEntity::getUserId);
}
static {
@ -169,6 +198,9 @@ public class MapFieldPredicates {
PREDICATES.put(Scope.class, AUTHZ_SCOPE_PREDICATES);
PREDICATES.put(PermissionTicket.class, AUTHZ_PERMISSION_TICKET_PREDICATES);
PREDICATES.put(Policy.class, AUTHZ_POLICY_PREDICATES);
PREDICATES.put(UserSessionModel.class, USER_SESSION_PREDICATES);
PREDICATES.put(AuthenticatedClientSessionModel.class, CLIENT_SESSION_PREDICATES);
PREDICATES.put(UserLoginFailureModel.class, USER_LOGIN_FAILURE_PREDICATES);
}
private static <K, V extends AbstractEntity<K>, M> void put(
@ -423,6 +455,13 @@ public class MapFieldPredicates {
private static MapModelCriteriaBuilder<Object, AbstractRealmEntity<Object>, RealmModel> checkRealmsWithComponentType(MapModelCriteriaBuilder<Object, AbstractRealmEntity<Object>, RealmModel> mcb, Operator op, Object[] values) {
String providerType = ensureEqSingleValue(RealmModel.SearchableFields.COMPONENT_PROVIDER_TYPE, "component_provider_type", op, values);
Function<AbstractRealmEntity<Object>, ?> getter = realmEntity -> realmEntity.getComponents().anyMatch(component -> component.getProviderType().equals(providerType));
return mcb.fieldCompare(Boolean.TRUE::equals, getter);
}
private static MapModelCriteriaBuilder<Object, AbstractUserSessionEntity<Object>, UserSessionModel> checkUserSessionContainsAuthenticatedClientSession(MapModelCriteriaBuilder<Object, AbstractUserSessionEntity<Object>, UserSessionModel> mcb, Operator op, Object[] values) {
String clientId = ensureEqSingleValue(UserSessionModel.SearchableFields.CLIENT_ID, "client_id", op, values);
Function<AbstractUserSessionEntity<Object>, ?> getter;
getter = use -> (use.getAuthenticatedClientSessions().containsKey(clientId));
return mcb.fieldCompare(Boolean.TRUE::equals, getter);
}

View file

@ -388,7 +388,7 @@ public class MapKeycloakTransaction<K, V extends AbstractEntity<K>, M> implement
Predicate<? super V> entityFilter = mmcb.getEntityFilter();
Predicate<? super K> keyFilter = ((MapModelCriteriaBuilder) mcb).getKeyFilter();
return v -> v != null && ! (keyFilter.test(v.getId()) && entityFilter.test(v));
return v -> v == null || ! (keyFilter.test(v.getId()) && entityFilter.test(v));
}
@Override

View file

@ -17,8 +17,10 @@
package org.keycloak.models.map.storage.chm;
import org.keycloak.Config.Scope;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.Serialization;
import com.fasterxml.jackson.databind.JavaType;
@ -31,6 +33,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import java.util.UUID;
/**
*
@ -92,7 +96,14 @@ public class ConcurrentHashMapStorageProvider implements MapStorageProvider {
private <K, V extends AbstractEntity<K>, M> ConcurrentHashMapStorage<K, V, M> loadMap(String fileName,
Class<V> valueType, Class<M> modelType, EnumSet<Flag> flags) {
ConcurrentHashMapStorage<K, V, M> store = new ConcurrentHashMapStorage<>(modelType);
ConcurrentHashMapStorage<K, V, M> store;
if (modelType == UserSessionModel.class) {
ConcurrentHashMapStorage clientSessionStore =
getStorage("clientSessions", UUID.class, MapAuthenticatedClientSessionEntity.class, AuthenticatedClientSessionModel.class);
store = new UserSessionConcurrentHashMapStorage<>(clientSessionStore);
} else {
store = new ConcurrentHashMapStorage<>(modelType);
}
if (! flags.contains(Flag.INITIALIZE_EMPTY)) {
final File f = getFile(fileName);

View file

@ -0,0 +1,79 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.chm;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.userSession.AbstractAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.AbstractUserSessionEntity;
import java.util.Set;
import java.util.stream.Collectors;
/**
* User session storage with a naive implementation of referential integrity in client to user session relation, restricted to
* ON DELETE CASCADE functionality.
*
* @author hmlnarik
*/
public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapStorage<K, AbstractUserSessionEntity<K>, UserSessionModel> {
private final ConcurrentHashMapStorage<K, AbstractAuthenticatedClientSessionEntity<K>, AuthenticatedClientSessionModel> clientSessionStore;
private class Transaction extends MapKeycloakTransaction<K, AbstractUserSessionEntity<K>, UserSessionModel> {
private final MapKeycloakTransaction<K, AbstractAuthenticatedClientSessionEntity<K>, AuthenticatedClientSessionModel> clientSessionTr;
public Transaction(MapKeycloakTransaction<K, AbstractAuthenticatedClientSessionEntity<K>, AuthenticatedClientSessionModel> clientSessionTr) {
super(UserSessionConcurrentHashMapStorage.this);
this.clientSessionTr = clientSessionTr;
}
@Override
public long delete(K artificialKey, ModelCriteriaBuilder<UserSessionModel> mcb) {
Set<K> ids = getUpdatedNotRemoved(mcb).map(AbstractEntity::getId).collect(Collectors.toSet());
ModelCriteriaBuilder<AuthenticatedClientSessionModel> csMcb = clientSessionStore.createCriteriaBuilder().compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, Operator.IN, ids);
clientSessionTr.delete(artificialKey, csMcb);
return super.delete(artificialKey, mcb);
}
@Override
public void delete(K key) {
ModelCriteriaBuilder<AuthenticatedClientSessionModel> csMcb = clientSessionStore.createCriteriaBuilder().compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, Operator.EQ, key);
clientSessionTr.delete(key, csMcb);
super.delete(key);
}
}
@SuppressWarnings("unchecked")
public UserSessionConcurrentHashMapStorage(ConcurrentHashMapStorage<K, AbstractAuthenticatedClientSessionEntity<K>, AuthenticatedClientSessionModel> clientSessionStore) {
super(UserSessionModel.class);
this.clientSessionStore = clientSessionStore;
}
@Override
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<K, AbstractUserSessionEntity<K>, UserSessionModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session)) : (MapKeycloakTransaction<K, AbstractUserSessionEntity<K>, UserSessionModel>) sessionTransaction;
}
}

View file

@ -304,4 +304,9 @@ public abstract class MapUserAdapter extends AbstractUserModel<MapUserEntity> {
public void deleteRoleMapping(RoleModel role) {
entity.removeRolesMembership(role.getId());
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,205 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.common.util.Time;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractAuthenticatedClientSessionEntity<K> implements AbstractEntity<K> {
private K id;
private String userSessionId;
private String realmId;
private String clientId;
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
private String authMethod;
private String redirectUri;
private volatile int timestamp;
private long expiration;
private String action;
private Map<String, String> notes = new ConcurrentHashMap<>();
private String currentRefreshToken;
private int currentRefreshTokenUseCount;
private boolean offline;
public AbstractAuthenticatedClientSessionEntity() {
this.id = null;
this.realmId = null;
}
public AbstractAuthenticatedClientSessionEntity(K id, String userSessionId, String realmId, String clientId, boolean offline) {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(userSessionId, "userSessionId");
Objects.requireNonNull(realmId, "realmId");
Objects.requireNonNull(clientId, "clientId");
this.id = id;
this.userSessionId = userSessionId;
this.realmId = realmId;
this.clientId = clientId;
this.offline = offline;
this.timestamp = Time.currentTime();
}
@Override
public K getId() {
return this.id;
}
@Override
public boolean isUpdated() {
return this.updated;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.updated |= !Objects.equals(this.realmId, realmId);
this.realmId = realmId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.updated |= !Objects.equals(this.clientId, clientId);
this.clientId = clientId;
}
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.updated |= !Objects.equals(this.userSessionId, userSessionId);
this.userSessionId = userSessionId;
}
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.updated |= !Objects.equals(this.authMethod, authMethod);
this.authMethod = authMethod;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.updated |= !Objects.equals(this.redirectUri, redirectUri);
this.redirectUri = redirectUri;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.updated |= this.timestamp != timestamp;
this.timestamp = timestamp;
}
public long getExpiration() {
return expiration;
}
public void setExpiration(long expiration) {
this.updated |= this.expiration != expiration;
this.expiration = expiration;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.updated |= !Objects.equals(this.action, action);
this.action = action;
}
public Map<String, String> getNotes() {
return notes;
}
public void setNotes(Map<String, String> notes) {
this.updated |= !Objects.equals(this.notes, notes);
this.notes = notes;
}
public String removeNote(String name) {
String note = this.notes.remove(name);
this.updated |= note != null;
return note;
}
public void addNote(String name, String value) {
this.updated |= !Objects.equals(this.notes.put(name, value), value);
}
public String getCurrentRefreshToken() {
return currentRefreshToken;
}
public void setCurrentRefreshToken(String currentRefreshToken) {
this.updated |= !Objects.equals(this.currentRefreshToken, currentRefreshToken);
this.currentRefreshToken = currentRefreshToken;
}
public int getCurrentRefreshTokenUseCount() {
return currentRefreshTokenUseCount;
}
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
this.updated |= this.currentRefreshTokenUseCount != currentRefreshTokenUseCount;
this.currentRefreshTokenUseCount = currentRefreshTokenUseCount;
}
public boolean isOffline() {
return offline;
}
public void setOffline(boolean offline) {
this.updated |= this.offline != offline;
this.offline = offline;
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Objects;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractAuthenticatedClientSessionModel<E extends AbstractEntity> implements AuthenticatedClientSessionModel {
protected final KeycloakSession session;
protected final RealmModel realm;
protected ClientModel client;
protected UserSessionModel userSession;
protected final E entity;
public AbstractAuthenticatedClientSessionModel(KeycloakSession session, RealmModel realm, ClientModel client,
UserSessionModel userSession, E entity) {
Objects.requireNonNull(entity, "entity");
Objects.requireNonNull(realm, "realm");
Objects.requireNonNull(client, "client");
Objects.requireNonNull(userSession, "userSession");
this.session = session;
this.realm = realm;
this.client = client;
this.userSession = userSession;
this.entity = entity;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AuthenticatedClientSessionModel)) return false;
AuthenticatedClientSessionModel that = (AuthenticatedClientSessionModel) o;
return Objects.equals(that.getId(), getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -0,0 +1,288 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.common.util.Time;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractUserSessionEntity<K> implements AbstractEntity<K> {
private K id;
private String realmId;
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
private String userId;
private String brokerSessionId;
private String brokerUserId;
private String loginUsername;
private String ipAddress;
private String authMethod;
private boolean rememberMe;
private int started;
private int lastSessionRefresh;
private long expiration;
private Map<String, String> notes = new ConcurrentHashMap<>();
private UserSessionModel.State state;
private UserSessionModel.SessionPersistenceState persistenceState = UserSessionModel.SessionPersistenceState.PERSISTENT;
private Map<String, K> authenticatedClientSessions = new ConcurrentHashMap<>();
private boolean offline;
public AbstractUserSessionEntity() {
this.id = null;
this.realmId = null;
}
public AbstractUserSessionEntity(K id, String realmId) {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(realmId, "realmId");
this.id = id;
this.realmId = realmId;
}
public AbstractUserSessionEntity(K id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId,
boolean offline) {
this.id = id;
this.realmId = realm.getId();
this.userId = user.getId();
this.loginUsername = loginUsername;
this.ipAddress = ipAddress;
this.authMethod = authMethod;
this.rememberMe = rememberMe;
this.brokerSessionId = brokerSessionId;
this.brokerUserId = brokerUserId;
this.started = Time.currentTime();
this.lastSessionRefresh = started;
this.offline = offline;
}
@Override
public K getId() {
return this.id;
}
@Override
public boolean isUpdated() {
return this.updated;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.updated |= !Objects.equals(this.realmId, realmId);
this.realmId = realmId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.updated |= !Objects.equals(this.userId, userId);
this.userId = userId;
}
public String getBrokerSessionId() {
return brokerSessionId;
}
public void setBrokerSessionId(String brokerSessionId) {
this.updated |= !Objects.equals(this.brokerSessionId, brokerSessionId);
this.brokerSessionId = brokerSessionId;
}
public String getBrokerUserId() {
return brokerUserId;
}
public void setBrokerUserId(String brokerUserId) {
this.updated |= !Objects.equals(this.brokerUserId, brokerUserId);
this.brokerUserId = brokerUserId;
}
public String getLoginUsername() {
return loginUsername;
}
public void setLoginUsername(String loginUsername) {
this.updated |= !Objects.equals(this.loginUsername, loginUsername);
this.loginUsername = loginUsername;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.updated |= !Objects.equals(this.ipAddress, ipAddress);
this.ipAddress = ipAddress;
}
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.updated |= !Objects.equals(this.authMethod, authMethod);
this.authMethod = authMethod;
}
public boolean isRememberMe() {
return rememberMe;
}
public void setRememberMe(boolean rememberMe) {
this.updated |= this.rememberMe != rememberMe;
this.rememberMe = rememberMe;
}
public int getStarted() {
return started;
}
public void setStarted(int started) {
this.updated |= this.started != started;
this.started = started;
}
public int getLastSessionRefresh() {
return lastSessionRefresh;
}
public void setLastSessionRefresh(int lastSessionRefresh) {
this.updated |= this.lastSessionRefresh != lastSessionRefresh;
this.lastSessionRefresh = lastSessionRefresh;
}
public long getExpiration() {
return expiration;
}
public void setExpiration(long expiration) {
this.updated |= this.expiration != expiration;
this.expiration = expiration;
}
public Map<String, String> getNotes() {
return notes;
}
public String getNote(String name) {
return notes.get(name);
}
public void setNotes(Map<String, String> notes) {
this.updated |= !Objects.equals(this.notes, notes);
this.notes = notes;
}
public String removeNote(String name) {
String note = this.notes.remove(name);
this.updated |= note != null;
return note;
}
public void addNote(String name, String value) {
this.updated |= !Objects.equals(this.notes.put(name, value), value);
}
public UserSessionModel.State getState() {
return state;
}
public void setState(UserSessionModel.State state) {
this.updated |= !Objects.equals(this.state, state);
this.state = state;
}
public Map<String, K> getAuthenticatedClientSessions() {
return authenticatedClientSessions;
}
public void setAuthenticatedClientSessions(Map<String, K> authenticatedClientSessions) {
this.updated |= !Objects.equals(this.authenticatedClientSessions, authenticatedClientSessions);
this.authenticatedClientSessions = authenticatedClientSessions;
}
public void addAuthenticatedClientSession(String clientId, K clientSessionId) {
this.updated |= !Objects.equals(this.authenticatedClientSessions.put(clientId, clientSessionId), clientSessionId);
}
public K removeAuthenticatedClientSession(String clientId) {
K entity = this.authenticatedClientSessions.remove(clientId);
this.updated |= entity != null;
return entity;
}
public void clearAuthenticatedClientSessions() {
this.updated |= !authenticatedClientSessions.isEmpty();
this.authenticatedClientSessions.clear();
}
public boolean isOffline() {
return offline;
}
public void setOffline(boolean offline) {
this.updated |= this.offline != offline;
this.offline = offline;
}
public UserSessionModel.SessionPersistenceState getPersistenceState() {
return persistenceState;
}
public void setPersistenceState(UserSessionModel.SessionPersistenceState persistenceState) {
this.updated |= !Objects.equals(this.persistenceState, persistenceState);
this.persistenceState = persistenceState;
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Objects;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractUserSessionModel<E extends AbstractEntity> implements UserSessionModel {
protected final KeycloakSession session;
protected final RealmModel realm;
protected final E entity;
public AbstractUserSessionModel(KeycloakSession session, RealmModel realm, E entity) {
Objects.requireNonNull(entity, "entity");
Objects.requireNonNull(realm, "realm");
this.session = session;
this.realm = realm;
this.entity = entity;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserSessionModel)) return false;
UserSessionModel that = (UserSessionModel) o;
return Objects.equals(that.getId(), getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -0,0 +1,148 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import java.util.Map;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class MapAuthenticatedClientSessionAdapter extends AbstractAuthenticatedClientSessionModel<MapAuthenticatedClientSessionEntity> {
public MapAuthenticatedClientSessionAdapter(KeycloakSession session, RealmModel realm, ClientModel client,
UserSessionModel userSession, MapAuthenticatedClientSessionEntity entity) {
super(session, realm, client, userSession, entity);
}
@Override
public String getId() {
return entity.getId().toString();
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp);
}
@Override
public UserSessionModel getUserSession() {
return userSession;
}
@Override
public String getCurrentRefreshToken() {
return entity.getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
entity.setCurrentRefreshToken(currentRefreshToken);
}
@Override
public int getCurrentRefreshTokenUseCount() {
return entity.getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override
public String getNote(String name) {
return (name != null) ? entity.getNotes().get(name) : null;
}
@Override
public void setNote(String name, String value) {
if (name != null) {
if (value == null) {
entity.removeNote(name);
} else {
entity.addNote(name, value);
}
}
}
@Override
public void removeNote(String name) {
if (name != null) {
entity.removeNote(name);
}
}
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
@Override
public String getRedirectUri() {
return entity.getRedirectUri();
}
@Override
public void setRedirectUri(String uri) {
entity.setRedirectUri(uri);
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public ClientModel getClient() {
return client;
}
@Override
public String getAction() {
return entity.getAction();
}
@Override
public void setAction(String action) {
entity.setAction(action);
}
@Override
public String getProtocol() {
return entity.getAuthMethod();
}
@Override
public void setProtocol(String method) {
entity.setAuthMethod(method);
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import java.util.UUID;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapAuthenticatedClientSessionEntity extends AbstractAuthenticatedClientSessionEntity<UUID> {
protected MapAuthenticatedClientSessionEntity() {
super();
}
public MapAuthenticatedClientSessionEntity(UUID id, String userSessionId, String realmId, String clientId, boolean offline) {
super(id, userSessionId, realmId, clientId, offline);
}
}

View file

@ -0,0 +1,223 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class MapUserSessionAdapter extends AbstractUserSessionModel<MapUserSessionEntity> {
public MapUserSessionAdapter(KeycloakSession session, RealmModel realm, MapUserSessionEntity entity) {
super(session, realm, entity);
}
@Override
public String getId() {
return entity.getId().toString();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public String getBrokerSessionId() {
return entity.getBrokerSessionId();
}
@Override
public String getBrokerUserId() {
return entity.getBrokerUserId();
}
@Override
public UserModel getUser() {
return session.users().getUserById(getRealm(), entity.getUserId());
}
@Override
public String getLoginUsername() {
return entity.getLoginUsername();
}
@Override
public String getIpAddress() {
return entity.getIpAddress();
}
@Override
public String getAuthMethod() {
return entity.getAuthMethod();
}
@Override
public boolean isRememberMe() {
return entity.isRememberMe();
}
@Override
public int getStarted() {
return entity.getStarted();
}
@Override
public int getLastSessionRefresh() {
return entity.getLastSessionRefresh();
}
@Override
public void setLastSessionRefresh(int seconds) {
entity.setLastSessionRefresh(seconds);
}
@Override
public boolean isOffline() {
return entity.isOffline();
}
@Override
public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
Map<String, AuthenticatedClientSessionModel> result = new HashMap<>();
List<String> removedClientUUIDS = new LinkedList<>();
entity.getAuthenticatedClientSessions().entrySet()
.stream()
.forEach(entry -> {
String clientUUID = entry.getKey();
ClientModel client = realm.getClientById(clientUUID);
if (client != null) {
AuthenticatedClientSessionModel clientSession = session.sessions()
.getClientSession(this, client, entry.getValue(), isOffline());
if (clientSession != null) {
result.put(clientUUID, clientSession);
}
} else {
removedClientUUIDS.add(clientUUID);
}
});
removeAuthenticatedClientSessions(removedClientUUIDS);
return Collections.unmodifiableMap(result);
}
@Override
public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) {
UUID clientSessionId = entity.getAuthenticatedClientSessions().get(clientUUID);
if (clientSessionId == null) {
return null;
}
ClientModel client = realm.getClientById(clientUUID);
if (client != null) {
return session.sessions().getClientSession(this, client, clientSessionId, isOffline());
}
removeAuthenticatedClientSessions(Collections.singleton(clientUUID));
return null;
}
@Override
public String getNote(String name) {
return (name != null) ? entity.getNotes().get(name) : null;
}
@Override
public void setNote(String name, String value) {
if (name != null) {
if (value == null) {
entity.removeNote(name);
} else {
entity.addNote(name, value);
}
}
}
@Override
public void removeNote(String name) {
if (name != null) {
entity.removeNote(name);
}
}
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
@Override
public State getState() {
return entity.getState();
}
@Override
public void setState(State state) {
entity.setState(state);
}
@Override
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod,
boolean rememberMe, String brokerSessionId, String brokerUserId) {
entity.setRealmId(realm.getId());
entity.setUserId(user.getId());
entity.setLoginUsername(loginUsername);
entity.setIpAddress(ipAddress);
entity.setAuthMethod(authMethod);
entity.setRememberMe(rememberMe);
entity.setBrokerSessionId(brokerSessionId);
entity.setBrokerUserId(brokerUserId);
int currentTime = Time.currentTime();
entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);
entity.setState(null);
String correspondingSessionId = entity.getNote(CORRESPONDING_SESSION_ID);
entity.setNotes(new ConcurrentHashMap<>());
if (correspondingSessionId != null)
entity.addNote(CORRESPONDING_SESSION_ID, correspondingSessionId);
entity.clearAuthenticatedClientSessions();
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import java.util.UUID;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserSessionEntity extends AbstractUserSessionEntity<UUID> {
protected MapUserSessionEntity() {
super();
}
public MapUserSessionEntity(UUID id, String realmId) {
super(id, realmId);
}
public MapUserSessionEntity(UUID id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId,
boolean offline) {
super(id, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId, offline);
}
}

View file

@ -0,0 +1,680 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.device.DeviceActivityManager;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.map.common.Serialization;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID;
import static org.keycloak.models.UserSessionModel.SessionPersistenceState.TRANSIENT;
import static org.keycloak.models.map.userSession.SessionExpiration.setClientSessionExpiration;
import static org.keycloak.models.map.userSession.SessionExpiration.setUserSessionExpiration;
import static org.keycloak.utils.StreamsUtil.paginatedStream;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserSessionProvider implements UserSessionProvider {
private static final Logger LOG = Logger.getLogger(MapUserSessionProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<UUID, MapUserSessionEntity, UserSessionModel> userSessionTx;
protected final MapKeycloakTransaction<UUID, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTx;
private final MapStorage<UUID, MapUserSessionEntity, UserSessionModel> userSessionStore;
private final MapStorage<UUID, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionStore;
/**
* Storage for transient user sessions which lifespan is limited to one request.
*/
private final Map<UUID, MapUserSessionEntity> transientUserSessions = new HashMap<>();
public MapUserSessionProvider(KeycloakSession session, MapStorage<UUID, MapUserSessionEntity, UserSessionModel> userSessionStore,
MapStorage<UUID, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionStore) {
this.session = session;
this.userSessionStore = userSessionStore;
this.clientSessionStore = clientSessionStore;
userSessionTx = userSessionStore.createTransaction(session);
clientSessionTx = clientSessionStore.createTransaction(session);
session.getTransactionManager().enlistAfterCompletion(userSessionTx);
session.getTransactionManager().enlistAfterCompletion(clientSessionTx);
}
private Function<MapUserSessionEntity, UserSessionModel> userEntityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return (origEntity) -> {
if (origEntity.getExpiration() <= Time.currentTime()) {
if (Objects.equals(origEntity.getPersistenceState(), TRANSIENT)) {
transientUserSessions.remove(origEntity.getId());
}
userSessionTx.delete(origEntity.getId());
return null;
} else {
return new MapUserSessionAdapter(session, realm,
Objects.equals(origEntity.getPersistenceState(), TRANSIENT) ? origEntity : registerEntityForChanges(origEntity)) {
@Override
public void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS) {
removedClientUUIDS.forEach(entity::removeAuthenticatedClientSession);
}
@Override
public void setLastSessionRefresh(int lastSessionRefresh) {
entity.setLastSessionRefresh(lastSessionRefresh);
// whenever the lastSessionRefresh is changed recompute the expiration time
setUserSessionExpiration(entity, realm);
}
};
}
};
}
private Function<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientEntityToAdapterFunc(RealmModel realm,
ClientModel client,
UserSessionModel userSession) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> {
if (origEntity.getExpiration() <= Time.currentTime()) {
userSession.removeAuthenticatedClientSessions(Arrays.asList(origEntity.getClientId()));
clientSessionTx.delete(origEntity.getId());
return null;
} else {
return new MapAuthenticatedClientSessionAdapter(session, realm, client, userSession, registerEntityForChanges(origEntity)) {
@Override
public void detachFromUserSession() {
this.userSession = null;
clientSessionTx.delete(entity.getId());
}
@Override
public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp);
// whenever the timestamp is changed recompute the expiration time
setClientSessionExpiration(entity, realm, client);
}
};
}
};
}
private MapUserSessionEntity registerEntityForChanges(MapUserSessionEntity origEntity) {
MapUserSessionEntity res = userSessionTx.read(origEntity.getId(), id -> Serialization.from(origEntity));
userSessionTx.updateIfChanged(origEntity.getId(), res, MapUserSessionEntity::isUpdated);
return res;
}
private MapAuthenticatedClientSessionEntity registerEntityForChanges(MapAuthenticatedClientSessionEntity origEntity) {
MapAuthenticatedClientSessionEntity res = clientSessionTx.read(origEntity.getId(), id -> Serialization.from(origEntity));
clientSessionTx.updateIfChanged(origEntity.getId(), res, MapAuthenticatedClientSessionEntity::isUpdated);
return res;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
@Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
MapAuthenticatedClientSessionEntity entity =
new MapAuthenticatedClientSessionEntity(UUID.randomUUID(), userSession.getId(), realm.getId(), client.getId(), false);
setClientSessionExpiration(entity, realm, client);
LOG.tracef("createClientSession(%s, %s, %s)%s", realm, client, userSession, getShortStackTrace());
clientSessionTx.create(entity.getId(), entity);
MapUserSessionEntity userSessionEntity = getUserSessionById(UUID.fromString(userSession.getId()));
if (userSessionEntity == null) {
throw new IllegalStateException("User session entity does not exist: " + userSession.getId());
}
userSessionEntity.addAuthenticatedClientSession(client.getId(), entity.getId());
return clientEntityToAdapterFunc(realm, client, userSession).apply(entity);
}
@Override
public AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client,
UUID clientSessionId, boolean offline) {
LOG.tracef("getClientSession(%s, %s, %s, %s)%s", userSession, client,
clientSessionId, offline, getShortStackTrace());
Objects.requireNonNull(userSession, "The provided user session cannot be null!");
Objects.requireNonNull(client, "The provided client cannot be null!");
if (clientSessionId == null) {
return null;
}
ModelCriteriaBuilder<AuthenticatedClientSessionModel> mcb = clientSessionStore.createCriteriaBuilder()
.compare(AuthenticatedClientSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, clientSessionId)
.compare(AuthenticatedClientSessionModel.SearchableFields.USER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, userSession.getId())
.compare(AuthenticatedClientSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, userSession.getRealm().getId())
.compare(AuthenticatedClientSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId())
.compare(AuthenticatedClientSessionModel.SearchableFields.IS_OFFLINE, ModelCriteriaBuilder.Operator.EQ, offline);
return clientSessionTx.getUpdatedNotRemoved(mcb)
.findFirst()
.map(clientEntityToAdapterFunc(client.getRealm(), client, userSession))
.orElse(null);
}
@Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress,
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
return createUserSession(null, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId,
brokerUserId, UserSessionModel.SessionPersistenceState.PERSISTENT);
}
@Override
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername,
String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId,
String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) {
final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id);
LOG.tracef("createUserSession(%s, %s, %s, %s)%s", id, realm, loginUsername, persistenceState, getShortStackTrace());
MapUserSessionEntity entity = new MapUserSessionEntity(entityId, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId, false);
entity.setPersistenceState(persistenceState);
setUserSessionExpiration(entity, realm);
if (Objects.equals(persistenceState, TRANSIENT)) {
transientUserSessions.put(entityId, entity);
} else {
if (userSessionTx.read(entity.getId()) != null) {
throw new ModelDuplicateException("User session exists: " + entity.getId());
}
userSessionTx.create(entity.getId(), entity);
}
UserSessionModel userSession = userEntityToAdapterFunc(realm).apply(entity);
if (userSession != null) {
DeviceActivityManager.attachDevice(userSession, session);
}
return userSession;
}
@Override
public UserSessionModel getUserSession(RealmModel realm, String id) {
Objects.requireNonNull(realm, "The provided realm can't be null!");
LOG.tracef("getUserSession(%s, %s)%s", realm, id, getShortStackTrace());
UUID uuid = toUUID(id);
if (uuid == null) {
return null;
}
MapUserSessionEntity userSessionEntity = transientUserSessions.get(uuid);
if (userSessionEntity != null) {
return userEntityToAdapterFunc(realm).apply(userSessionEntity);
}
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, uuid);
return userSessionTx.getUpdatedNotRemoved(mcb)
.findFirst()
.map(userEntityToAdapterFunc(realm))
.orElse(null);
}
@Override
public Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, UserModel user) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId());
LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull);
}
@Override
public Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, ClientModel client) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId());
LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, client, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull);
}
@Override
public Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, ClientModel client,
Integer firstResult, Integer maxResults) {
return paginatedStream(getUserSessionsStream(realm, client)
.sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults);
}
@Override
public Stream<UserSessionModel> getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.BROKER_USER_ID, ModelCriteriaBuilder.Operator.EQ, brokerUserId);
LOG.tracef("getUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull);
}
@Override
public UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.BROKER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, brokerSessionId);
LOG.tracef("getUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.findFirst()
.map(userEntityToAdapterFunc(realm))
.orElse(null);
}
@Override
public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline,
Predicate<UserSessionModel> predicate) {
LOG.tracef("getUserSessionWithPredicate(%s, %s, %s)%s", realm, id, offline, getShortStackTrace());
Stream<UserSessionModel> userSessionEntityStream;
if (offline) {
userSessionEntityStream = getOfflineUserSessionEntityStream(realm, id)
.map(userEntityToAdapterFunc(realm)).filter(Objects::nonNull);
} else {
UserSessionModel userSession = getUserSession(realm, id);
userSessionEntityStream = userSession != null ? Stream.of(userSession) : Stream.empty();
}
return userSessionEntityStream
.filter(predicate)
.findFirst()
.orElse(null);
}
@Override
public long getActiveUserSessions(RealmModel realm, ClientModel client) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId());
LOG.tracef("getActiveUserSessions(%s, %s)%s", realm, client, getShortStackTrace());
return userSessionTx.getCount(mcb);
}
@Override
public Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, offline);
LOG.tracef("getActiveClientSessionStats(%s, %s)%s", realm, offline, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull)
.map(UserSessionModel::getAuthenticatedClientSessions)
.map(Map::keySet)
.flatMap(Collection::stream)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}
@Override
public void removeUserSession(RealmModel realm, UserSessionModel session) {
Objects.requireNonNull(session, "The provided user session can't be null!");
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false)
.compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(session.getId()));
LOG.tracef("removeUserSession(%s, %s)%s", realm, session, getShortStackTrace());
userSessionTx.delete(UUID.randomUUID(), mcb);
}
@Override
public void removeUserSessions(RealmModel realm, UserModel user) {
ModelCriteriaBuilder<UserSessionModel> mcb = userSessionStore.createCriteriaBuilder()
.compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId());
LOG.tracef("removeUserSessions(%s, %s)%s", realm, user, getShortStackTrace());
userSessionTx.delete(UUID.randomUUID(), mcb);
}
@Override
public void removeAllExpired() {
LOG.tracef("removeAllExpired()%s", getShortStackTrace());
}
@Override
public void removeExpired(RealmModel realm) {
LOG.tracef("removeExpired(%s)%s", realm, getShortStackTrace());
}
@Override
public void removeUserSessions(RealmModel realm) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, false);
LOG.tracef("removeUserSessions(%s)%s", realm, getShortStackTrace());
userSessionTx.delete(UUID.randomUUID(), mcb);
}
@Override
public void onRealmRemoved(RealmModel realm) {
LOG.tracef("onRealmRemoved(%s)%s", realm, getShortStackTrace());
removeUserSessions(realm);
}
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
}
@Override
public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
LOG.tracef("createOfflineUserSession(%s)%s", userSession, getShortStackTrace());
MapUserSessionEntity offlineUserSession = createUserSessionEntityInstance(userSession, true);
// set a reference for the offline user session to the original online user session
userSession.setNote(CORRESPONDING_SESSION_ID, offlineUserSession.getId().toString());
int currentTime = Time.currentTime();
offlineUserSession.setStarted(currentTime);
offlineUserSession.setLastSessionRefresh(currentTime);
setUserSessionExpiration(offlineUserSession, userSession.getRealm());
userSessionTx.create(offlineUserSession.getId(), offlineUserSession);
return userEntityToAdapterFunc(userSession.getRealm()).apply(offlineUserSession);
}
@Override
public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) {
LOG.tracef("getOfflineUserSession(%s, %s)%s", realm, userSessionId, getShortStackTrace());
return getOfflineUserSessionEntityStream(realm, userSessionId)
.findFirst()
.map(userEntityToAdapterFunc(realm))
.orElse(null);
}
@Override
public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) {
Objects.requireNonNull(userSession, "The provided user session can't be null!");
LOG.tracef("removeOfflineUserSession(%s, %s)%s", realm, userSession, getShortStackTrace());
ModelCriteriaBuilder<UserSessionModel> mcb;
if (userSession.isOffline()) {
userSessionTx.delete(UUID.fromString(userSession.getId()));
} else if (userSession.getNote(CORRESPONDING_SESSION_ID) != null) {
mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(userSession.getNote(CORRESPONDING_SESSION_ID)));
userSessionTx.delete(UUID.randomUUID(), mcb);
userSession.removeNote(CORRESPONDING_SESSION_ID);
}
}
@Override
public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession,
UserSessionModel offlineUserSession) {
LOG.tracef("createOfflineClientSession(%s, %s)%s", clientSession, offlineUserSession, getShortStackTrace());
MapAuthenticatedClientSessionEntity clientSessionEntity = createAuthenticatedClientSessionInstance(clientSession, offlineUserSession, true);
clientSessionEntity.setTimestamp(Time.currentTime());
setClientSessionExpiration(clientSessionEntity, clientSession.getRealm(), clientSession.getClient());
Optional<MapUserSessionEntity> userSessionEntity = getOfflineUserSessionEntityStream(clientSession.getRealm(), offlineUserSession.getId()).findFirst();
if (userSessionEntity.isPresent()) {
userSessionEntity.get().addAuthenticatedClientSession(clientSession.getClient().getId(), clientSessionEntity.getId());
}
clientSessionTx.create(clientSessionEntity.getId(), clientSessionEntity);
return clientEntityToAdapterFunc(clientSession.getRealm(),
clientSession.getClient(), offlineUserSession).apply(clientSessionEntity);
}
@Override
public Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, UserModel user) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, user.getId());
LOG.tracef("getOfflineUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull);
}
@Override
public UserSessionModel getOfflineUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.BROKER_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, brokerSessionId);
LOG.tracef("getOfflineUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.findFirst()
.map(userEntityToAdapterFunc(realm))
.orElse(null);
}
@Override
public Stream<UserSessionModel> getOfflineUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.BROKER_USER_ID, ModelCriteriaBuilder.Operator.EQ, brokerUserId);
LOG.tracef("getOfflineUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace());
return userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull);
}
@Override
public long getOfflineSessionsCount(RealmModel realm, ClientModel client) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId());
LOG.tracef("getOfflineSessionsCount(%s, %s)%s", realm, client, getShortStackTrace());
return userSessionTx.getCount(mcb);
}
@Override
public Stream<UserSessionModel> getOfflineUserSessionsStream(RealmModel realm, ClientModel client,
Integer firstResult, Integer maxResults) {
ModelCriteriaBuilder<UserSessionModel> mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, client.getId());
LOG.tracef("getOfflineUserSessionsStream(%s, %s, %s, %s)%s", realm, client, firstResult, maxResults, getShortStackTrace());
return paginatedStream(userSessionTx.getUpdatedNotRemoved(mcb)
.map(userEntityToAdapterFunc(realm))
.filter(Objects::nonNull)
.sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh)), firstResult, maxResults);
}
@Override
public void importUserSessions(Collection<UserSessionModel> persistentUserSessions, boolean offline) {
if (persistentUserSessions == null || persistentUserSessions.isEmpty()) {
return;
}
persistentUserSessions.stream()
.map(pus -> {
MapUserSessionEntity userSessionEntity = new MapUserSessionEntity(UUID.randomUUID(), pus.getRealm(), pus.getUser(),
pus.getLoginUsername(), pus.getIpAddress(), pus.getAuthMethod(),
pus.isRememberMe(), pus.getBrokerSessionId(), pus.getBrokerUserId(), offline);
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : pus.getAuthenticatedClientSessions().entrySet()) {
MapAuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(entry.getValue(), entry.getValue().getUserSession(), offline);
// Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value
clientSession.setTimestamp(userSessionEntity.getLastSessionRefresh());
userSessionEntity.addAuthenticatedClientSession(entry.getKey(), clientSession.getId());
clientSessionTx.create(clientSession.getId(), clientSession);
}
return userSessionEntity;
})
.forEach(use -> userSessionTx.create(use.getId(), use));
}
@Override
public void close() {
}
private Stream<MapUserSessionEntity> getOfflineUserSessionEntityStream(RealmModel realm, String userSessionId) {
UUID uuid = toUUID(userSessionId);
if (uuid == null) {
return Stream.empty();
}
// first get a user entity by ID
ModelCriteriaBuilder<UserSessionModel> mcb = userSessionStore.createCriteriaBuilder()
.compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, uuid);
// check if it's an offline user session
MapUserSessionEntity userSessionEntity = userSessionTx.getUpdatedNotRemoved(mcb).findFirst().orElse(null);
if (userSessionEntity != null) {
if (userSessionEntity.isOffline()) {
return Stream.of(userSessionEntity);
}
} else {
// no session found by the given ID, try to find by corresponding session ID
mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, ModelCriteriaBuilder.Operator.EQ, userSessionId);
return userSessionTx.getUpdatedNotRemoved(mcb);
}
// it's online user session so lookup offline user session by corresponding session id reference
String offlineUserSessionId = userSessionEntity.getNote(CORRESPONDING_SESSION_ID);
if (offlineUserSessionId != null) {
mcb = realmAndOfflineCriteriaBuilder(realm, true)
.compare(UserSessionModel.SearchableFields.ID, ModelCriteriaBuilder.Operator.EQ, UUID.fromString(offlineUserSessionId));
return userSessionTx.getUpdatedNotRemoved(mcb);
}
return Stream.empty();
}
private ModelCriteriaBuilder<UserSessionModel> realmAndOfflineCriteriaBuilder(RealmModel realm, boolean offline) {
return userSessionStore.createCriteriaBuilder()
.compare(UserSessionModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, realm.getId())
.compare(UserSessionModel.SearchableFields.IS_OFFLINE, ModelCriteriaBuilder.Operator.EQ, offline);
}
private MapUserSessionEntity getUserSessionById(UUID id) {
MapUserSessionEntity userSessionEntity = transientUserSessions.get(id);
if (userSessionEntity == null) {
MapUserSessionEntity userSession = userSessionTx.read(id);
return userSession != null ? registerEntityForChanges(userSession) : null;
}
return userSessionEntity;
}
private MapUserSessionEntity createUserSessionEntityInstance(UserSessionModel userSession, boolean offline) {
MapUserSessionEntity entity = new MapUserSessionEntity(UUID.randomUUID(), userSession.getRealm().getId());
entity.setAuthMethod(userSession.getAuthMethod());
entity.setBrokerSessionId(userSession.getBrokerSessionId());
entity.setBrokerUserId(userSession.getBrokerUserId());
entity.setIpAddress(userSession.getIpAddress());
entity.setNotes(new ConcurrentHashMap<>(userSession.getNotes()));
entity.addNote(CORRESPONDING_SESSION_ID, userSession.getId());
entity.clearAuthenticatedClientSessions();
entity.setRememberMe(userSession.isRememberMe());
entity.setState(userSession.getState());
entity.setLoginUsername(userSession.getLoginUsername());
entity.setUserId(userSession.getUser().getId());
entity.setStarted(userSession.getStarted());
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
entity.setOffline(offline);
return entity;
}
private MapAuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession,
UserSessionModel userSession, boolean offline) {
MapAuthenticatedClientSessionEntity entity = new MapAuthenticatedClientSessionEntity(UUID.randomUUID(),
userSession.getId(), clientSession.getRealm().getId(), clientSession.getClient().getId(), offline);
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());
entity.setNotes(new ConcurrentHashMap<>(clientSession.getNotes()));
entity.setRedirectUri(clientSession.getRedirectUri());
entity.setTimestamp(clientSession.getTimestamp());
return entity;
}
private UUID toUUID(String id) {
try {
return UUID.fromString(id);
} catch (IllegalArgumentException ex) {
return null;
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.MapStorageProvider;
import java.util.UUID;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapUserSessionProviderFactory extends AbstractMapProviderFactory<UserSessionProvider>
implements UserSessionProviderFactory {
private MapStorage<UUID, MapUserSessionEntity, UserSessionModel> userSessionStore;
private MapStorage<UUID, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionStore;
@Override
public void postInit(KeycloakSessionFactory factory) {
MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class);
userSessionStore = sp.getStorage("userSessions", UUID.class, MapUserSessionEntity.class, UserSessionModel.class);
clientSessionStore = sp.getStorage("clientSessions", UUID.class, MapAuthenticatedClientSessionEntity.class, AuthenticatedClientSessionModel.class);
factory.register(event -> {
if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
MapUserSessionProvider provider = MapUserSessionProviderFactory.this.create(userRemovedEvent.getKeycloakSession());
provider.removeUserSessions(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
}
});
}
@Override
public void loadPersistentSessions(KeycloakSessionFactory sessionFactory, int maxErrors, int sessionsPerSegment) {
}
@Override
public MapUserSessionProvider create(KeycloakSession session) {
return new MapUserSessionProvider(session, userSessionStore, clientSessionStore);
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.userSession;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SessionExpiration {
public static void setClientSessionExpiration(MapAuthenticatedClientSessionEntity entity, RealmModel realm, ClientModel client) {
if (entity.isOffline()) {
long sessionExpires = entity.getTimestamp() + realm.getOfflineSessionIdleTimeout();
if (realm.isOfflineSessionMaxLifespanEnabled()) {
sessionExpires = entity.getTimestamp() + realm.getOfflineSessionMaxLifespan();
long clientOfflineSessionMaxLifespan;
String clientOfflineSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
if (clientOfflineSessionMaxLifespanPerClient != null && !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) {
clientOfflineSessionMaxLifespan = Long.parseLong(clientOfflineSessionMaxLifespanPerClient);
} else {
clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
}
if (clientOfflineSessionMaxLifespan > 0) {
long clientOfflineSessionMaxExpiration = entity.getTimestamp() + clientOfflineSessionMaxLifespan;
sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration);
}
}
long expiration = entity.getTimestamp() + realm.getOfflineSessionIdleTimeout();
long clientOfflineSessionIdleTimeout;
String clientOfflineSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT);
if (clientOfflineSessionIdleTimeoutPerClient != null && !clientOfflineSessionIdleTimeoutPerClient.trim().isEmpty()) {
clientOfflineSessionIdleTimeout = Long.parseLong(clientOfflineSessionIdleTimeoutPerClient);
} else {
clientOfflineSessionIdleTimeout = realm.getClientOfflineSessionIdleTimeout();
}
if (clientOfflineSessionIdleTimeout > 0) {
long clientOfflineSessionIdleExpiration = entity.getTimestamp() + clientOfflineSessionIdleTimeout;
expiration = Math.min(expiration, clientOfflineSessionIdleExpiration);
}
entity.setExpiration(Math.min(expiration, sessionExpires));
} else {
long sessionExpires = (long) entity.getTimestamp() + (realm.getSsoSessionMaxLifespanRememberMe() > 0
? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
long clientSessionMaxLifespan;
String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
clientSessionMaxLifespan = Long.parseLong(clientSessionMaxLifespanPerClient);
} else {
clientSessionMaxLifespan = realm.getClientSessionMaxLifespan();
}
if (clientSessionMaxLifespan > 0) {
long clientSessionMaxExpiration = entity.getTimestamp() + clientSessionMaxLifespan;
sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration);
}
long expiration = (long) entity.getTimestamp() + (realm.getSsoSessionIdleTimeoutRememberMe() > 0
? realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout());
long clientSessionIdleTimeout;
String clientSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT);
if (clientSessionIdleTimeoutPerClient != null && !clientSessionIdleTimeoutPerClient.trim().isEmpty()) {
clientSessionIdleTimeout = Long.parseLong(clientSessionIdleTimeoutPerClient);
} else {
clientSessionIdleTimeout = realm.getClientSessionIdleTimeout();
}
if (clientSessionIdleTimeout > 0) {
long clientSessionIdleExpiration = entity.getTimestamp() + clientSessionIdleTimeout;
expiration = Math.min(expiration, clientSessionIdleExpiration);
}
entity.setExpiration(Math.min(expiration, sessionExpires));
}
}
public static void setUserSessionExpiration(MapUserSessionEntity entity, RealmModel realm) {
if (entity.isOffline()) {
long sessionExpires = entity.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout();
if (realm.isOfflineSessionMaxLifespanEnabled()) {
sessionExpires = entity.getStarted() + realm.getOfflineSessionMaxLifespan();
long clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
if (clientOfflineSessionMaxLifespan > 0) {
long clientOfflineSessionMaxExpiration = entity.getStarted() + clientOfflineSessionMaxLifespan;
sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration);
}
}
long expiration = entity.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout();
long clientOfflineSessionIdleTimeout = realm.getClientOfflineSessionIdleTimeout();
if (clientOfflineSessionIdleTimeout > 0) {
long clientOfflineSessionIdleExpiration = Time.currentTime() + clientOfflineSessionIdleTimeout;
expiration = Math.min(expiration, clientOfflineSessionIdleExpiration);
}
entity.setExpiration(Math.min(expiration, sessionExpires));
} else {
long sessionExpires = (long) entity.getStarted()
+ (entity.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0
? realm.getSsoSessionMaxLifespanRememberMe()
: realm.getSsoSessionMaxLifespan());
long clientSessionMaxLifespan = realm.getClientSessionMaxLifespan();
if (clientSessionMaxLifespan > 0) {
long clientSessionMaxExpiration = entity.getStarted() + clientSessionMaxLifespan;
sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration);
}
long expiration = (long) entity.getLastSessionRefresh() + (entity.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0
? realm.getSsoSessionIdleTimeoutRememberMe()
: realm.getSsoSessionIdleTimeout());
long clientSessionIdleTimeout = realm.getClientSessionIdleTimeout();
if (clientSessionIdleTimeout > 0) {
long clientSessionIdleExpiration = entity.getLastSessionRefresh() + clientSessionIdleTimeout;
expiration = Math.min(expiration, clientSessionIdleExpiration);
}
entity.setExpiration(Math.min(expiration, sessionExpires));
}
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.models.map.userSession.MapUserSessionProviderFactory

View file

@ -0,0 +1,26 @@
/*
* Copyright 2021 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;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public interface UserLoginFailureProviderFactory extends ProviderFactory<UserLoginFailureProvider> {
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2021 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;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class UserLoginFailureSpi implements Spi {
public static final String NAME = "loginFailure";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return UserLoginFailureProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return UserLoginFailureProviderFactory.class;
}
}

View file

@ -33,6 +33,7 @@ org.keycloak.models.SamlArtifactSessionMappingStoreSpi
org.keycloak.models.SingleUseTokenStoreSpi
org.keycloak.models.TokenRevocationStoreSpi
org.keycloak.models.UserSessionSpi
org.keycloak.models.UserLoginFailureSpi
org.keycloak.models.UserSpi
org.keycloak.models.session.UserSessionPersisterSpi
org.keycloak.models.dblock.DBLockSpi

View file

@ -21,12 +21,22 @@ package org.keycloak.models;
import java.util.Map;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.storage.SearchableModelField;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
class SearchableFields {
public static final SearchableModelField<AuthenticatedClientSessionModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<AuthenticatedClientSessionModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<AuthenticatedClientSessionModel> CLIENT_ID = new SearchableModelField<>("clientId", String.class);
public static final SearchableModelField<AuthenticatedClientSessionModel> USER_SESSION_ID = new SearchableModelField<>("userSessionId", String.class);
public static final SearchableModelField<AuthenticatedClientSessionModel> IS_OFFLINE = new SearchableModelField<>("isOffline", Boolean.class);
public static final SearchableModelField<AuthenticatedClientSessionModel> TIMESTAMP = new SearchableModelField<>("timestamp", Integer.class);
}
String getId();
int getTimestamp();

View file

@ -20,7 +20,6 @@ package org.keycloak.models;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.cache.UserCache;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
import org.keycloak.provider.Provider;
import org.keycloak.services.clientpolicy.ClientPolicyManager;
import org.keycloak.sessions.AuthenticationSessionProvider;
@ -178,6 +177,14 @@ public interface KeycloakSession extends InvalidationHandler {
*/
UserSessionProvider sessions();
/**
* Returns a managed provider instance. Will start a provider transaction. This transaction is managed by the KeycloakSession
* transaction.
*
* @return {@link UserLoginFailureProvider}
* @throws IllegalStateException if transaction is not active
*/
UserLoginFailureProvider loginFailures();
AuthenticationSessionProvider authenticationSessions();

View file

@ -17,12 +17,20 @@
package org.keycloak.models;
import org.keycloak.storage.SearchableModelField;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserLoginFailureModel
{
public interface UserLoginFailureModel {
class SearchableFields {
public static final SearchableModelField<UserLoginFailureModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<UserLoginFailureModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<UserLoginFailureModel> USER_ID = new SearchableModelField<>("userId", String.class);
}
String getUserId();
int getFailedLoginNotBefore();
void setFailedLoginNotBefore(int notBefore);

View file

@ -0,0 +1,55 @@
/*
* Copyright 2021 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;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public interface UserLoginFailureProvider extends Provider {
/**
* Returns the {@link UserLoginFailureModel} for the given realm and user id.
* @param realm {@link RealmModel}
* @param userId {@link String} Id of the user.
* @return Returns the {@link UserLoginFailureModel} for the given realm and user id.
*/
UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId);
/**
* Adds a {@link UserLoginFailureModel} for the given realm and user id.
* @param realm {@link RealmModel}
* @param userId {@link String} Id of the user.
* @return Returns newly created {@link UserLoginFailureModel}.
*/
UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId);
/**
* Removes a {@link UserLoginFailureModel} for the given realm and user id.
* @param realm {@link RealmModel}
* @param userId {@link String} Id of the user.
*/
void removeUserLoginFailure(RealmModel realm, String userId);
/**
* Removes all the {@link UserLoginFailureModel} for the given realm.
* @param realm {@link RealmModel}
*/
void removeAllUserLoginFailures(RealmModel realm);
}

View file

@ -17,6 +17,8 @@
package org.keycloak.models;
import org.keycloak.storage.SearchableModelField;
import java.util.Collection;
import java.util.Map;
@ -25,6 +27,28 @@ import java.util.Map;
*/
public interface UserSessionModel {
class SearchableFields {
public static final SearchableModelField<UserSessionModel> ID = new SearchableModelField<>("id", String.class);
/**
* Represents the corresponding offline user session for the online user session.
* null if there is no corresponding offline user session.
*/
public static final SearchableModelField<UserSessionModel> CORRESPONDING_SESSION_ID = new SearchableModelField<>("correspondingSessionId", String.class);
public static final SearchableModelField<UserSessionModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<UserSessionModel> USER_ID = new SearchableModelField<>("userId", String.class);
public static final SearchableModelField<UserSessionModel> CLIENT_ID = new SearchableModelField<>("clientId", String.class);
public static final SearchableModelField<UserSessionModel> BROKER_SESSION_ID = new SearchableModelField<>("brokerSessionId", String.class);
public static final SearchableModelField<UserSessionModel> BROKER_USER_ID = new SearchableModelField<>("brokerUserId", String.class);
public static final SearchableModelField<UserSessionModel> IS_OFFLINE = new SearchableModelField<>("isOffline", Boolean.class);
public static final SearchableModelField<UserSessionModel> LAST_SESSION_REFRESH = new SearchableModelField<>("lastSessionRefresh", Integer.class);
}
/**
* Represents the corresponding online/offline user session.
*/
String CORRESPONDING_SESSION_ID = "correspondingSessionId";
String getId();
RealmModel getRealm();

View file

@ -33,6 +33,12 @@ import java.util.stream.Stream;
*/
public interface UserSessionProvider extends Provider {
/**
* Returns currently used Keycloak session.
* @return {@link KeycloakSession}
*/
KeycloakSession getKeycloakSession();
AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession);
AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline);
@ -52,11 +58,11 @@ public interface UserSessionProvider extends Provider {
}
/**
* Obtains the user sessions associated with the specified user.
* Obtains the online user sessions associated with the specified user.
*
* @param realm a reference to the realm.
* @param user the user whose sessions are being searched.
* @return a non-null {@link Stream} of user sessions.
* @return a non-null {@link Stream} of online user sessions.
*/
Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, UserModel user);
@ -69,11 +75,11 @@ public interface UserSessionProvider extends Provider {
}
/**
* Obtains the user sessions associated with the specified client.
* Obtains the online user sessions associated with the specified client.
*
* @param realm a reference to the realm.
* @param client the client whose user sessions are being searched.
* @return a non-null {@link Stream} of user sessions.
* @return a non-null {@link Stream} of online user sessions.
*/
Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, ClientModel client);
@ -86,14 +92,14 @@ public interface UserSessionProvider extends Provider {
}
/**
* Obtains the user sessions associated with the specified client, starting from the {@code firstResult} and containing
* Obtains the online user sessions associated with the specified client, starting from the {@code firstResult} and containing
* at most {@code maxResults}.
*
* @param realm a reference tot he realm.
* @param client the client whose user sessions are being searched.
* @param firstResult first result to return. Ignored if negative or {@code null}.
* @param maxResults maximum number of results to return. Ignored if negative or {@code null}.
* @return a non-null {@link Stream} of user sessions.
* @return a non-null {@link Stream} of online user sessions.
*/
Stream<UserSessionModel> getUserSessionsStream(RealmModel realm, ClientModel client, Integer firstResult, Integer maxResults);
@ -107,11 +113,11 @@ public interface UserSessionProvider extends Provider {
}
/**
* Obtains the user sessions associated with the user that matches the specified {@code brokerUserId}.
* Obtains the online user sessions associated with the user that matches the specified {@code brokerUserId}.
*
* @param realm a reference to the realm.
* @param brokerUserId the id of the broker user whose sessions are being searched.
* @return a non-null {@link Stream} of user sessions.
* @return a non-null {@link Stream} of online user sessions.
*/
Stream<UserSessionModel> getUserSessionByBrokerUserIdStream(RealmModel realm, String brokerUserId);
@ -154,10 +160,37 @@ public interface UserSessionProvider extends Provider {
void removeUserSessions(RealmModel realm);
UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId);
UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId);
void removeUserLoginFailure(RealmModel realm, String userId);
void removeAllUserLoginFailures(RealmModel realm);
/**
* @deprecated Use {@link UserLoginFailureProvider#getUserLoginFailure(RealmModel, String) getUserLoginFailure} instead.
*/
@Deprecated
default UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
return getKeycloakSession().loginFailures().getUserLoginFailure(realm, userId);
}
/**
* @deprecated Use {@link UserLoginFailureProvider#addUserLoginFailure(RealmModel, String) addUserLoginFailure} instead.
*/
@Deprecated
default UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId) {
return getKeycloakSession().loginFailures().addUserLoginFailure(realm, userId);
}
/**
* @deprecated Use {@link UserLoginFailureProvider#removeUserLoginFailure(RealmModel, String) removeUserLoginFailure} instead.
*/
@Deprecated
default void removeUserLoginFailure(RealmModel realm, String userId) {
getKeycloakSession().loginFailures().removeUserLoginFailure(realm, userId);
}
/**
* @deprecated Use {@link UserLoginFailureProvider#removeAllUserLoginFailures(RealmModel) removeAllUserLoginFailures} instead.
*/
@Deprecated
default void removeAllUserLoginFailures(RealmModel realm) {
getKeycloakSession().loginFailures().removeAllUserLoginFailures(realm);
}
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);

View file

@ -49,7 +49,6 @@ import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
@ -92,7 +91,6 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import static org.keycloak.representations.IDToken.NONCE;
import static org.keycloak.representations.IDToken.PHONE_NUMBER;
/**
* Stateless object that creates tokens and manages oauth access codes
@ -265,7 +263,12 @@ public class TokenManager {
}
if (valid) {
userSession.setLastSessionRefresh(Time.currentTime());
int currentTime = Time.currentTime();
userSession.setLastSessionRefresh(currentTime);
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null) {
clientSession.setTimestamp(currentTime);
}
}
}

View file

@ -24,6 +24,7 @@ import org.keycloak.keys.DefaultKeyManager;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.UserLoginFailureProvider;
import org.keycloak.models.TokenManager;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
@ -40,7 +41,6 @@ import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.InvalidationHandler.InvalidableObjectType;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.clientpolicy.ClientPolicyManager;
@ -90,6 +90,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
private GroupStorageManager groupStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
private UserSessionProvider sessionProvider;
private UserLoginFailureProvider userLoginFailureProvider;
private AuthenticationSessionProvider authenticationSessionProvider;
private UserFederatedStorageProvider userFederatedStorageProvider;
private KeycloakContext context;
@ -447,6 +448,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
return sessionProvider;
}
@Override
public UserLoginFailureProvider loginFailures() {
if (userLoginFailureProvider == null) {
userLoginFailureProvider = getProvider(UserLoginFailureProvider.class);
}
return userLoginFailureProvider;
}
@Override
public AuthenticationSessionProvider authenticationSessions() {
if (authenticationSessionProvider == null) {

View file

@ -68,7 +68,6 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
@ -101,6 +100,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
import static org.keycloak.services.util.CookieHelper.getCookie;
@ -282,7 +282,11 @@ public class AuthenticationManager {
new UserSessionManager(session).revokeOfflineUserSession(userSession);
// Check if "online" session still exists and remove it too
UserSessionModel onlineUserSession = session.sessions().getUserSession(realm, userSession.getId());
String onlineUserSessionId = userSession.getNote(CORRESPONDING_SESSION_ID);
UserSessionModel onlineUserSession = (onlineUserSessionId != null) ?
session.sessions().getUserSession(realm, onlineUserSessionId) :
session.sessions().getUserSession(realm, userSession.getId());
if (onlineUserSession != null) {
session.sessions().removeUserSession(realm, onlineUserSession);
}

View file

@ -106,7 +106,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
UserLoginFailureModel userLoginFailure = getUserModel(session, event);
if (userLoginFailure == null) {
userLoginFailure = session.sessions().addUserLoginFailure(realm, userId);
userLoginFailure = session.loginFailures().addUserLoginFailure(realm, userId);
}
userLoginFailure.setLastIPFailure(event.ip);
long currentTime = Time.currentTimeMillis();
@ -172,7 +172,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
protected UserLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
RealmModel realm = getRealmModel(session, event);
if (realm == null) return null;
UserLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.userId);
UserLoginFailureModel user = session.loginFailures().getUserLoginFailure(realm, event.userId);
if (user == null) return null;
return user;
}
@ -304,7 +304,7 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector
@Override
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserModel user) {
UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId());
UserLoginFailureModel failure = session.loginFailures().getUserLoginFailure(realm, user.getId());
if (failure != null) {
int currTime = (int) (Time.currentTimeMillis() / 1000);

View file

@ -94,7 +94,7 @@ public class AttackDetectionResource {
if (!realm.isBruteForceProtected()) return data;
UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId);
UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId);
if (model == null) return data;
boolean disabled;
@ -129,9 +129,9 @@ public class AttackDetectionResource {
} else {
auth.users().requireManage(user);
}
UserLoginFailureModel model = session.sessions().getUserLoginFailure(realm, userId);
UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, userId);
if (model != null) {
session.sessions().removeUserLoginFailure(realm, userId);
session.loginFailures().removeUserLoginFailure(realm, userId);
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
}
}
@ -147,7 +147,7 @@ public class AttackDetectionResource {
public void clearAllBruteForce() {
auth.users().requireManage();
session.sessions().removeAllUserLoginFailures(realm);
session.loginFailures().removeAllUserLoginFailures(realm);
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
}

View file

@ -163,7 +163,7 @@ public class UserResource {
try {
if (rep.isEnabled() != null && rep.isEnabled()) {
UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, user.getId());
UserLoginFailureModel failureModel = session.loginFailures().getUserLoginFailure(realm, user.getId());
if (failureModel != null) {
failureModel.clearFailures();
}

View file

@ -50,6 +50,7 @@ public abstract class AbstractShowTokensServlet extends HttpServlet {
return new StringBuilder("<span id=\"accessToken\">" + accessTokenPretty + "</span>")
.append("<span id=\"refreshToken\">" + refreshTokenPretty + "</span>")
.append("<span id=\"accessTokenString\">" + ctx.getTokenString() + "</span>")
.append("<span id=\"refreshTokenString\">" + ctx.getRefreshToken() + "</span>")
.toString();
}

View file

@ -41,6 +41,8 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl
@FindBy(id = "accessTokenString")
private WebElement accessTokenString;
@FindBy(id = "refreshTokenString")
private WebElement refreshTokenString;
public AccessToken getAccessToken() {
try {
@ -77,4 +79,14 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl
return null;
}
public String getRefreshTokenString() {
try {
return refreshTokenString.getText();
} catch (NoSuchElementException nsee) {
log.warn("No refreshTokenString element found on the page");
}
return null;
}
}

View file

@ -26,8 +26,9 @@ public class OfflineToken extends AbstractShowTokensPage {
}
public void logout() {
log.info("Logging out, navigating to: " + getUriBuilder().path("/logout").build().toASCIIString());
driver.navigate().to(getUriBuilder().path("/logout").build().toASCIIString());
String uri = getUriBuilder().path("/logout").build().toASCIIString();
log.info("Logging out, navigating to: " + uri);
driver.navigate().to(uri);
pause(300); // this is needed for FF for some reason
waitUntilElement(By.tagName("body")).is().visible();
}

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.adapter.servlet;
import javax.ws.rs.core.UriBuilder;
import java.util.List;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive;
@ -19,6 +20,8 @@ import org.keycloak.testsuite.adapter.page.OfflineToken;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.LoginPage;
@ -66,6 +69,9 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
@Page
protected OAuthGrantPage oauthGrantPage;
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
private final String DEFAULT_USERNAME = "test-user@localhost";
private final String DEFAULT_PASSWORD = "password";
private final String OFFLINE_CLIENT_ID = "offline-client";
@ -94,6 +100,8 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
String servletUri = UriBuilder.fromUri(offlineTokenPage.toString())
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
.build().toString();
oauth.redirectUri(offlineTokenPage.toString());
oauth.clientId("offline-client");
driver.navigate().to(servletUri);
waitUntilElement(By.tagName("body")).is().visible();
@ -110,17 +118,31 @@ public class OfflineServletsAdapterTest extends AbstractServletsAdapterTest {
String accessTokenId = offlineTokenPage.getAccessToken().getId();
String refreshTokenId = offlineTokenPage.getRefreshToken().getId();
// online user session will be expired and removed
setAdapterAndServerTimeOffset(9999);
// still able to access the page using the offline token
offlineTokenPage.navigateTo();
assertCurrentUrlStartsWith(offlineTokenPage);
// assert successful refresh
assertThat(offlineTokenPage.getRefreshToken().getId(), not(refreshTokenId));
assertThat(offlineTokenPage.getAccessToken().getId(), not(accessTokenId));
// Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
offlineTokenPage.logout();
assertCurrentUrlDoesntStartWith(offlineTokenPage);
// logout doesn't make sense because online user session is gone and there is no KEYCLOAK_IDENTITY / KEYCLOAK_SESSION cookie in the browser
// navigate to login page which won't be possible if there's valid online session
driver.navigate().to(oauth.getLoginFormUrl());
WaitUtils.waitForPageToLoad();
loginPage.assertCurrent();
// navigate back to offlineTokenPage to verify the offline session is still valid
offlineTokenPage.navigateTo();
assertCurrentUrlStartsWith(offlineTokenPage);
// logout the offline user session using the offline refresh token
oauth.doLogout(offlineTokenPage.getRefreshTokenString(), "secret1");
// can't access the offlineTokenPage anymore
offlineTokenPage.navigateTo();
assertCurrentUrlDoesntStartWith(offlineTokenPage);
loginPage.assertCurrent();

View file

@ -42,13 +42,17 @@ import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
@ -78,8 +82,11 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
protected static final int CLIENTS_PER_THREAD = 30;
protected static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
private String userSessionProvider;
@Before
public void beforeTest() {
userSessionProvider = testingClient.server().fetch(session -> Config.getProvider(UserSessionSpi.NAME), String.class);
createClients();
}
@ -105,6 +112,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
@Test
public void concurrentLoginSingleUser() throws Throwable {
Assume.assumeThat("Test runs only with InfinispanUserSessionProvider",
userSessionProvider,
Matchers.is(InfinispanUserSessionProviderFactory.PROVIDER_ID));
log.info("*********************************************");
long start = System.currentTimeMillis();
@ -169,6 +180,10 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
@Test
public void concurrentLoginMultipleUsers() throws Throwable {
Assume.assumeThat("Test runs only with InfinispanUserSessionProvider",
userSessionProvider,
Matchers.is(InfinispanUserSessionProviderFactory.PROVIDER_ID));
log.info("*********************************************");
long start = System.currentTimeMillis();

View file

@ -265,7 +265,7 @@ public class BruteForceCrossDCTest extends AbstractAdminCrossDCTest {
private void addUserLoginFailure(KeycloakTestingClient testingClient) throws URISyntaxException, IOException {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
UserLoginFailureModel loginFailure = session.sessions().addUserLoginFailure(realm, "login-test-1");
UserLoginFailureModel loginFailure = session.loginFailures().addUserLoginFailure(realm, "login-test-1");
loginFailure.incrementFailures();
});
}

View file

@ -28,6 +28,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.events.Details;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.ClientAdapter;
import org.keycloak.representations.AccessToken;
@ -468,8 +469,11 @@ public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "password");
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
String offlineUserSessionId = testingClient.server().fetch((KeycloakSession session) ->
session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionState()).getId(), String.class);
Assert.assertEquals(200, response.getStatusCode());
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
Assert.assertEquals(offlineUserSessionId, refreshedToken.getSessionState());
// Assert new refreshToken in the response
String newRefreshToken = response.getRefreshToken();

View file

@ -42,7 +42,6 @@ import org.keycloak.testsuite.pages.LoginPage;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -55,6 +54,7 @@ import org.keycloak.testsuite.auth.page.account.AccountManagement;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.testsuite.util.WaitUtils;
@ -68,6 +68,9 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Page
protected AppPage appPage;
@ -190,9 +193,8 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String idTokenString = tokenResponse.getIdToken();
// wait for a timeout
// setTimeOffset doesn't work because session cookie is not invalidated thus the logout flow would continue with browser logout
TimeUnit.SECONDS.sleep(3);
// expire online user session
setTimeOffset(9999);
String logoutUrl = oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idTokenString).build();
driver.navigate().to(logoutUrl);

View file

@ -29,6 +29,7 @@ import org.keycloak.models.cache.infinispan.RealmAdapter;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertNotNull;
@ -111,7 +112,7 @@ public class CacheTest extends AbstractTestRealmKeycloakTest {
user.setFirstName("firstName");
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache",
UserSessionModel userSession = session.sessions().createUserSession(UUID.randomUUID().toString(), realm, user, "testAddUserNotAddedToCache",
"127.0.0.1", "auth", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
user = userSession.getUser();

View file

@ -1,278 +0,0 @@
/*
* 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.testsuite.model;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
public class UserSessionInitializerTest extends AbstractTestRealmKeycloakTest {
private final String realmName = "test";
@Before
public void before() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealm("test");
session.users().addUser(realm, "user1").setEmail("user1@localhost");
session.users().addUser(realm, "user2").setEmail("user2@localhost");
});
}
@After
public void after() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
session.sessions().removeUserSessions(realm);
UserModel user1 = session.users().getUserByUsername(realm, "user1");
UserModel user2 = session.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(session);
if (user1 != null)
um.removeUser(realm, user1);
if (user2 != null)
um.removeUser(realm, user2);
});
}
@Test
@ModelTest
public void testUserSessionInitializer(KeycloakSession session) {
AtomicReference<Integer> startedAtomic = new AtomicReference<>();
AtomicReference<UserSessionModel[]> origSessionsAtomic = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInit1) -> {
KeycloakSession currentSession = inheritClientConnection(session, SessionInit1);
int started = Time.currentTime();
startedAtomic.set(started);
UserSessionModel[] origSessions = createSessionsInPersisterOnly(currentSession);
origSessionsAtomic.set(origSessions);
// Load sessions from persister into infinispan/memory
UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) currentSession.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
userSessionFactory.loadPersistentSessions(currentSession.getKeycloakSessionFactory(), 1, 2);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInit2) -> {
KeycloakSession currentSession = inheritClientConnection(session, SessionInit2);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
int started = startedAtomic.get();
UserSessionModel[] origSessions = origSessionsAtomic.get();
// Assert sessions are in
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sesions for client 'test-app'", currentSession.sessions().getOfflineSessionsCount(realm, testApp), is((long) 3));
assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1));
List<UserSessionModel> loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10)
.collect(Collectors.toList());
UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
assertSessionLoaded(loadedSessions, origSessions[1].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
assertSessionLoaded(loadedSessions, origSessions[2].getId(), currentSession.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
});
}
@Test
@ModelTest
public void testUserSessionInitializerWithDeletingClient(KeycloakSession session) {
AtomicReference<Integer> startedAtomic = new AtomicReference<>();
AtomicReference<UserSessionModel[]> origSessionsAtomic = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting1) -> {
KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting1);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
int started = Time.currentTime();
startedAtomic.set(started);
origSessionsAtomic.set(createSessionsInPersisterOnly(currentSession));
// Delete one of the clients now. Delete it directly in DB just for the purpose of simulating the issue (normally clients should be removed through ClientManager)
ClientModel testApp = realm.getClientByClientId("test-app");
realm.removeClient(testApp.getId());
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting2) -> {
KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting2);
// Load sessions from persister into infinispan/memory
UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) currentSession.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
userSessionFactory.loadPersistentSessions(currentSession.getKeycloakSessionFactory(), 1, 2);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession SessionInitWithDeleting3) -> {
KeycloakSession currentSession = inheritClientConnection(session, SessionInitWithDeleting3);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
int started = startedAtomic.get();
UserSessionModel[] origSessions = origSessionsAtomic.get();
// Assert sessions are in
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sesions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1));
List<UserSessionModel> loadedSessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10)
.collect(Collectors.toList());
assertThat("Size of loaded Sessions", loadedSessions.size(), is(1));
assertSessionLoaded(loadedSessions, origSessions[0].getId(), currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "third-party");
// Revert client
realm.addClient("test-app");
});
}
// Create sessions in persister + infinispan, but then delete them from infinispan cache. This is to allow later testing of initializer. Return the list of "origSessions"
private UserSessionModel[] createSessionsInPersisterOnly(KeycloakSession session) {
AtomicReference<UserSessionModel[]> origSessionsAtomic = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister1) -> {
KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister1);
UserSessionModel[] origSessions = createSessions(currentSession);
origSessionsAtomic.set(origSessions);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister2) -> {
KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister2);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
UserSessionManager sessionManager = new UserSessionManager(currentSession);
UserSessionModel[] origSessions = origSessionsAtomic.get();
for (UserSessionModel origSession : origSessions) {
UserSessionModel userSession = currentSession.sessions().getUserSession(realm, origSession.getId());
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
}
}
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister3) -> {
KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister3);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
// Delete local user cache (persisted sessions are still kept)
InfinispanUserSessionProvider userSessionProvider = (InfinispanUserSessionProvider) currentSession.getProvider(UserSessionProvider.class);
userSessionProvider.removeLocalUserSessions(realm.getId(), true);
// Clear ispn cache to ensure initializerState is removed as well
InfinispanConnectionProvider infinispan = currentSession.getProvider(InfinispanConnectionProvider.class);
infinispan.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME).clear();
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession createSessionPersister4) -> {
KeycloakSession currentSession = inheritClientConnection(session, createSessionPersister4);
RealmModel realm = currentSession.realms().getRealmByName(realmName);
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sessions for client 'test-app'", currentSession.sessions().getOfflineSessionsCount(realm, testApp), is((long) 0));
assertThat("Count of offline sessions for client 'third-party'", currentSession.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 0));
});
return origSessionsAtomic.get();
}
private AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) {
RealmModel realm = session.realms().getRealmByName(realmName);
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
clientSession.setRedirectUri(redirect);
if (state != null)
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
return clientSession;
}
private UserSessionModel[] createSessions(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(realmName);
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
return sessions;
}
private void assertSessionLoaded(List<UserSessionModel> sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
for (UserSessionModel session : sessions) {
if (session.getId().equals(id)) {
UserSessionProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients);
return;
}
}
Assert.fail("Session with ID " + id + " not found in the list");
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
}

View file

@ -1,613 +0,0 @@
/*
* 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.testsuite.model;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
public class UserSessionPersisterProviderTest extends AbstractTestRealmKeycloakTest {
@Before
public void before() {
testingClient.server().run(session -> {
initStuff(session);
});
}
public static void initStuff(KeycloakSession session) {
RealmModel realm = session.realms().getRealm("test");
session.users().addUser(realm, "user1").setEmail("user1@localhost");
session.users().addUser(realm, "user2").setEmail("user2@localhost");
}
@After
public void after() {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealm("test");
session.sessions().removeUserSessions(realm);
UserModel user1 = session.users().getUserByUsername(realm, "user1");
UserModel user2 = session.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(session);
if (user1 != null) {
um.removeUser(realm, user1);
}
if (user2 != null) {
um.removeUser(realm, user2);
}
});
}
@Test
@ModelTest
public void testPersistenceWithLoad(KeycloakSession session) {
int started = Time.currentTime();
UserSessionModel[][] origSessions = new UserSessionModel[1][1];
final UserSessionModel[] userSession = new UserSessionModel[1];
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL) -> {
// Create some sessions in infinispan
origSessions[0] = createSessions(sessionWL);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL22) -> {
// Persist 3 created userSessions and clientSessions as offline
RealmModel realm = sessionWL22.realms().getRealm("test");
ClientModel testApp = realm.getClientByClientId("test-app");
sessionWL22.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList())
.forEach(userSessionLooper -> persistUserSession(sessionWL22, userSessionLooper, true));
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL2) -> {
// Persist 1 online session
RealmModel realm = sessionWL2.realms().getRealm("test");
userSession[0] = sessionWL2.sessions().getUserSession(realm, origSessions[0][0].getId());
persistUserSession(sessionWL2, userSession[0], false);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL3) -> {// Assert online session
RealmModel realm = sessionWL3.realms().getRealm("test");
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(sessionWL3, false, 1, 1, 1);
UserSessionProviderTest.assertSession(loadedSessions.get(0), sessionWL3.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionWL4) -> {
// Assert offline sessions
RealmModel realm = sessionWL4.realms().getRealm("test");
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(sessionWL4, true, 2, 2, 3);
UserSessionProviderTest.assertSessions(loadedSessions, origSessions[0]);
assertSessionLoaded(loadedSessions, origSessions[0][0].getId(), sessionWL4.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
assertSessionLoaded(loadedSessions, origSessions[0][1].getId(), sessionWL4.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
assertSessionLoaded(loadedSessions, origSessions[0][2].getId(), sessionWL4.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
});
}
@Test
@ModelTest
public void testUpdateAndRemove(KeycloakSession session) {
int started = Time.currentTime();
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
AtomicReference<List<UserSessionModel>> loadedSessionsAt = new AtomicReference<>();
AtomicReference<UserSessionModel> userSessionAt = new AtomicReference<>();
AtomicReference<UserSessionModel> persistedSessionAt = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove1) -> {
KeycloakSession currentSession = sesUpdateRemove1;
// Create some sessions in infinispan
UserSessionModel[] origSessions = createSessions(currentSession);
origSessionsAt.set(origSessions);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove2) -> {
KeycloakSession currentSession = sesUpdateRemove2;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionModel[] origSessions = origSessionsAt.get();
// Persist 1 offline session
UserSessionModel userSession = currentSession.sessions().getUserSession(realm, origSessions[1].getId());
userSessionAt.set(userSession);
persistUserSession(currentSession, userSession, true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove3) -> {
KeycloakSession currentSession = sesUpdateRemove3;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Load offline session
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
loadedSessionsAt.set(loadedSessions);
UserSessionModel persistedSession = loadedSessions.get(0);
persistedSessionAt.set(persistedSession);
UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
// create new clientSession
AuthenticatedClientSessionModel clientSession = createClientSession(currentSession, realm.getClientByClientId("third-party"), currentSession.sessions().getUserSession(realm, persistedSession.getId()),
"http://redirect", "state");
persister.createClientSession(clientSession, true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove4) -> {
KeycloakSession currentSession = sesUpdateRemove4;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class);
UserSessionModel userSession = userSessionAt.get();
// Remove clientSession
persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove5) -> {
KeycloakSession currentSession = sesUpdateRemove5;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class);
List<UserSessionModel> loadedSessions = loadedSessionsAt.get();
UserSessionModel persistedSession = persistedSessionAt.get();
// Assert clientSession removed
loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
persistedSession = loadedSessions.get(0);
UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
// Remove userSession
persister.removeUserSession(persistedSession.getId(), true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sesUpdateRemove6) -> {
KeycloakSession currentSession = sesUpdateRemove6;
// Assert nothing found
loadPersistedSessionsPaginated(currentSession, true, 10, 0, 0);
});
}
@Test
@ModelTest
public void testOnRealmRemoved(KeycloakSession session) {
AtomicReference<String> userSessionID = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR1) -> {
KeycloakSession currentSession = sessionRR1;
RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.addClient("foo-app");
currentSession.users().addUser(fooRealm, "user3");
UserSessionModel userSession = currentSession.sessions().createUserSession(fooRealm, currentSession.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null);
userSessionID.set(userSession.getId());
createClientSession(currentSession, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR2) -> {
KeycloakSession currentSession = sessionRR2;
// Persist offline session
RealmModel fooRealm = currentSession.realms().getRealm("foo");
UserSessionModel userSession = currentSession.sessions().getUserSession(fooRealm, userSessionID.get());
persistUserSession(currentSession, userSession, true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR3) -> {
KeycloakSession currentSession = sessionRR3;
// Assert session was persisted
loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
// Remove realm
RealmManager realmMgr = new RealmManager(currentSession);
realmMgr.removeRealm(realmMgr.getRealm("foo"));
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR4) -> {
KeycloakSession currentSession = sessionRR4;
// Assert nothing loaded
loadPersistedSessionsPaginated(currentSession, true, 10, 0, 0);
});
}
@Test
@ModelTest
public void testOnClientRemoved(KeycloakSession session) {
int started = Time.currentTime();
AtomicReference<String> userSessionID = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR1) -> {
KeycloakSession currentSession = sessionCR1;
RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX));
fooRealm.addClient("foo-app");
fooRealm.addClient("bar-app");
currentSession.users().addUser(fooRealm, "user3");
UserSessionModel userSession = currentSession.sessions().createUserSession(fooRealm, currentSession.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null);
userSessionID.set(userSession.getId());
createClientSession(currentSession, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
createClientSession(currentSession, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR2) -> {
KeycloakSession currentSession = sessionCR2;
RealmModel fooRealm = currentSession.realms().getRealm("foo");
// Persist offline session
UserSessionModel userSession = currentSession.sessions().getUserSession(fooRealm, userSessionID.get());
persistUserSession(currentSession, userSession, true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR3) -> {
KeycloakSession currentSession = sessionCR3;
RealmManager realmMgr = new RealmManager(currentSession);
ClientManager clientMgr = new ClientManager(realmMgr);
RealmModel fooRealm = realmMgr.getRealm("foo");
// Assert session was persisted with both clientSessions
UserSessionModel persistedSession = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1).get(0);
UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "foo-app", "bar-app");
// Remove foo-app client
ClientModel client = fooRealm.getClientByClientId("foo-app");
clientMgr.removeClient(fooRealm, client);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR4) -> {
KeycloakSession currentSession = sessionCR4;
RealmManager realmMgr = new RealmManager(currentSession);
ClientManager clientMgr = new ClientManager(realmMgr);
RealmModel fooRealm = realmMgr.getRealm("foo");
// Assert just one bar-app clientSession persisted now
UserSessionModel persistedSession = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1).get(0);
UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "bar-app");
// Remove bar-app client
ClientModel client = fooRealm.getClientByClientId("bar-app");
clientMgr.removeClient(fooRealm, client);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR5) -> {
KeycloakSession currentSession = sessionCR5;
// Assert loading still works - last userSession is still there, but no clientSession on it
loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
// Cleanup
RealmManager realmMgr = new RealmManager(currentSession);
realmMgr.removeRealm(realmMgr.getRealm("foo"));
});
}
@Test
@ModelTest
public void testOnUserRemoved(KeycloakSession session) {
int started = Time.currentTime();
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR1) -> {
KeycloakSession currentSession = sessionOR1;
// Create some sessions in infinispan
UserSessionModel[] origSessions = createSessions(currentSession);
origSessionsAt.set(origSessions);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR2) -> {
KeycloakSession currentSession = sessionOR2;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionModel[] origSessions = origSessionsAt.get();
// Persist 2 offline sessions of 2 users
UserSessionModel userSession1 = currentSession.sessions().getUserSession(realm, origSessions[1].getId());
UserSessionModel userSession2 = currentSession.sessions().getUserSession(realm, origSessions[2].getId());
persistUserSession(currentSession, userSession1, true);
persistUserSession(currentSession, userSession2, true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR3) -> {
KeycloakSession currentSession = sessionOR3;
RealmModel realm = currentSession.realms().getRealm("test");
// Load offline sessions
loadPersistedSessionsPaginated(currentSession, true, 10, 1, 2);
// Properly delete user and assert his offlineSession removed
UserModel user1 = currentSession.users().getUserByUsername(realm, "user1");
new UserManager(currentSession).removeUser(realm, user1);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionOR4) -> {
KeycloakSession currentSession = sessionOR4;
RealmModel realm = currentSession.realms().getRealm("test");
UserSessionPersisterProvider persister = currentSession.getProvider(UserSessionPersisterProvider.class);
Assert.assertEquals(1, persister.getUserSessionsCount(true));
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
UserSessionModel persistedSession = loadedSessions.get(0);
UserSessionProviderTest.assertSession(persistedSession, currentSession.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
// KEYCLOAK-2431 Assert that userSessionPersister is resistent even to situation, when users are deleted "directly".
// No exception will happen. However session will be still there
UserModel user2 = currentSession.users().getUserByUsername(realm, "user2");
currentSession.users().removeUser(realm, user2);
loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 10, 1, 1);
// Cleanup
UserSessionModel userSession = loadedSessions.get(0);
currentSession.sessions().removeUserSession(realm, userSession);
persister.removeUserSession(userSession.getId(), userSession.isOffline());
});
}
// KEYCLOAK-1999
@Test
@ModelTest
public void testNoSessions(KeycloakSession session) {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionNS) -> {
UserSessionPersisterProvider persister = sessionNS.getProvider(UserSessionPersisterProvider.class);
Stream<UserSessionModel> sessions = persister.loadUserSessionsStream(0, 1, true, 0, "abc");
Assert.assertEquals(0, sessions.count());
});
}
@Test
@ModelTest
public void testMoreSessions(KeycloakSession session) {
AtomicReference<List<UserSessionModel>> userSessionsAt = new AtomicReference<>();
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS1) -> {
KeycloakSession currentSession = sessionMS1;
RealmModel realm = currentSession.realms().getRealm("test");
// Create 10 userSessions - each having 1 clientSession
List<UserSessionModel> userSessions = new ArrayList<>();
UserModel user = currentSession.users().getUserByUsername(realm, "user1");
for (int i = 0; i < 20; i++) {
// Having different offsets for each session (to ensure that lastSessionRefresh is also different)
Time.setOffset(i);
UserSessionModel userSession = currentSession.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null);
createClientSession(currentSession, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
userSessions.add(userSession);
}
userSessionsAt.set(userSessions);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS2) -> {
KeycloakSession currentSession = sessionMS2;
RealmModel realm = currentSession.realms().getRealm("test");
List<UserSessionModel> userSessions = userSessionsAt.get();
for (UserSessionModel userSession : userSessions) {
UserSessionModel userSession2 = currentSession.sessions().getUserSession(realm, userSession.getId());
persistUserSession(currentSession, userSession2, true);
}
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionMS3) -> {
KeycloakSession currentSession = sessionMS3;
RealmModel realm = currentSession.realms().getRealm("test");
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(currentSession, true, 2, 10, 20);
UserModel user = currentSession.users().getUserByUsername(realm, "user1");
ClientModel testApp = realm.getClientByClientId("test-app");
for (UserSessionModel loadedSession : loadedSessions) {
assertEquals(user.getId(), loadedSession.getUser().getId());
assertEquals("127.0.0.1", loadedSession.getIpAddress());
assertEquals(user.getUsername(), loadedSession.getLoginUsername());
assertEquals(1, loadedSession.getAuthenticatedClientSessions().size());
assertTrue(loadedSession.getAuthenticatedClientSessions().containsKey(testApp.getId()));
}
});
}
@Test
@ModelTest
public void testExpiredSessions(KeycloakSession session) {
UserSessionModel[][] origSessions = {new UserSessionModel[1]};
int started = Time.currentTime();
final UserSessionModel[] userSession1 = {null};
final UserSessionModel[] userSession2 = {null};
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES) -> {
// Create some sessions in infinispan
UserSessionPersisterProvider persister = sessionES.getProvider(UserSessionPersisterProvider.class);
origSessions[0] = createSessions(sessionES);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES2) -> {
// Persist 2 offline sessions of 2 users
RealmModel realm = sessionES2.realms().getRealm("test");
UserSessionPersisterProvider persister = sessionES2.getProvider(UserSessionPersisterProvider.class);
userSession1[0] = sessionES2.sessions().getUserSession(realm, origSessions[0][1].getId());
userSession2[0] = sessionES2.sessions().getUserSession(realm, origSessions[0][2].getId());
persistUserSession(sessionES2, userSession1[0], true);
persistUserSession(sessionES2, userSession2[0], true);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionES3) -> {
// Update one of the sessions with lastSessionRefresh of 20 days ahead
int lastSessionRefresh = Time.currentTime() + 1728000;
RealmModel realm = sessionES3.realms().getRealm("test");
UserSessionPersisterProvider persister = sessionES3.getProvider(UserSessionPersisterProvider.class);
persister.updateLastSessionRefreshes(realm, lastSessionRefresh, Collections.singleton(userSession1[0].getId()), true);
// Increase time offset - 40 days
Time.setOffset(3456000);
try {
// Run expiration thread
persister.removeExpired(realm);
// Test the updated session is still in persister. Not updated session is not there anymore
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(sessionES3, true, 10, 1, 1);
UserSessionModel persistedSession = loadedSessions.get(0);
UserSessionProviderTest.assertSession(persistedSession, sessionES3.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, lastSessionRefresh, "test-app");
} finally {
// Cleanup
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
});
}
private AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) {
RealmModel realm = session.realms().getRealm("test");
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
return clientSession;
}
private UserSessionModel[] createSessions(KeycloakSession session) {
RealmModel realm = session.realms().getRealm("test");
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
return sessions;
}
private void persistUserSession(KeycloakSession session, UserSessionModel userSession, boolean offline) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
persister.createUserSession(userSession, offline);
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
persister.createClientSession(clientSession, offline);
}
}
public static void assertSessionLoaded(List<UserSessionModel> sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
for (UserSessionModel session : sessions) {
if (session.getId().equals(id)) {
UserSessionProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients);
return;
}
}
Assert.fail("Session with ID " + id + " not found in the list");
}
private List<UserSessionModel> loadPersistedSessionsPaginated(KeycloakSession session, boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int count = persister.getUserSessionsCount(offline);
int pageCount = 0;
boolean next = true;
List<UserSessionModel> result = new ArrayList<>();
int lastCreatedOn = 0;
String lastSessionId = "abc";
while (next) {
List<UserSessionModel> sess = persister
.loadUserSessionsStream(0, sessionsPerPage, offline, lastCreatedOn, lastSessionId)
.collect(Collectors.toList());
if (sess.size() < sessionsPerPage) {
next = false;
// We had at least some session
if (sess.size() > 0) {
pageCount++;
}
} else {
pageCount++;
UserSessionModel lastSession = sess.get(sess.size() - 1);
lastCreatedOn = lastSession.getStarted();
lastSessionId = lastSession.getId();
}
result.addAll(sess);
}
Assert.assertEquals(expectedPageCount, pageCount);
Assert.assertEquals(expectedSessionsCount, result.size());
return result;
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
}

View file

@ -20,7 +20,6 @@ package org.keycloak.testsuite.model;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -30,10 +29,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ClientManager;
@ -42,8 +38,6 @@ import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.timer.TimerProvider;
import java.util.HashMap;
import java.util.HashSet;
@ -64,20 +58,15 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
@AuthServerContainerExclude(AuthServer.REMOTE)
public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTest {
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
private static KeycloakSession currentSession;
private static RealmModel realm;
private static UserSessionManager sessionManager;
private static UserSessionPersisterProvider persister;
@Before
public void before() {
testingClient.server().run(session -> {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionBefore) -> {
reloadState(sessionBefore, true);
persister = sessionBefore.getProvider(UserSessionPersisterProvider.class);
});
});
}
@ -115,7 +104,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession = sessionCrud2;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Key is userSession ID, values are client UUIDS
// Persist 3 created userSessions and clientSessions as offline
@ -128,7 +116,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession = sessionCrud3;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Assert all previously saved offline sessions found
for (Map.Entry<String, Set<String>> entry : offlineSessions.entrySet()) {
@ -165,17 +152,10 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession = sessionCrud4;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Assert userSession revoked
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
// Still 2 sessions. The count of sessions by client may not be accurate after revoke due the
// performance optimizations (the "127.0.0.1" currentSession still has another client "thirdparty" in it)
Assert.assertEquals(2, currentSession.sessions().getOfflineSessionsCount(realm, testApp));
Assert.assertEquals(1, currentSession.sessions().getOfflineSessionsCount(realm, thirdparty));
List<UserSessionModel> thirdpartySessions = currentSession.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10)
.collect(Collectors.toList());
Assert.assertEquals(1, thirdpartySessions.size());
@ -201,7 +181,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession = sessionCrud5;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
@ -231,9 +210,12 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionRR1) -> {
currentSession = sessionRR1;
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.setSsoSessionIdleTimeout(1800);
fooRealm.setSsoSessionMaxLifespan(36000);
fooRealm.setOfflineSessionIdleTimeout(2592000);
fooRealm.setOfflineSessionMaxLifespan(5184000);
fooRealm.addClient("foo-app");
currentSession.users().addUser(fooRealm, "user3");
@ -296,9 +278,12 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionCR1) -> {
currentSession = sessionCR1;
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.setSsoSessionIdleTimeout(1800);
fooRealm.setSsoSessionMaxLifespan(36000);
fooRealm.setOfflineSessionIdleTimeout(2592000);
fooRealm.setOfflineSessionMaxLifespan(5184000);
fooRealm.addClient("foo-app");
fooRealm.addClient("bar-app");
@ -392,6 +377,10 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession = sessionUR1;
RealmModel fooRealm = currentSession.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(currentSession.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.setSsoSessionIdleTimeout(1800);
fooRealm.setSsoSessionMaxLifespan(36000);
fooRealm.setOfflineSessionIdleTimeout(2592000);
fooRealm.setOfflineSessionMaxLifespan(5184000);
fooRealm.addClient("foo-app");
currentSession.users().addUser(fooRealm, "user3");
@ -443,141 +432,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
});
}
@Test
@ModelTest
public void testExpired(KeycloakSession session) {
// Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider
TimerProvider timer = session.getProvider(TimerProvider.class);
TimerProvider.TimerTaskContext timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
try {
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
// Key is userSessionId, value is set of client UUIDS
Map<String, Set<String>> offlineSessions = new HashMap<>();
ClientModel[] testApp = new ClientModel[1];
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired1) -> {
// Create some online sessions in infinispan
currentSession = sessionExpired1;
reloadState(currentSession);
UserSessionModel[] origSessions = createSessions(currentSession);
origSessionsAt.set(origSessions);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired2) -> {
currentSession = sessionExpired2;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Persist 3 created userSessions and clientSessions as offline
testApp[0] = realm.getClientByClientId("test-app");
currentSession.sessions().getUserSessionsStream(realm, testApp[0]).collect(Collectors.toList())
.forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(currentSession, userSession)));
// Assert all previously saved offline sessions found
for (Map.Entry<String, Set<String>> entry : offlineSessions.entrySet()) {
UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey());
Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue());
}
});
log.info("Persisted 3 sessions to UserSessionPersisterProvider");
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired3) -> {
currentSession = sessionExpired3;
realm = currentSession.realms().getRealm("test");
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
UserSessionModel[] origSessions = origSessionsAt.get();
UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId());
Assert.assertNotNull(session0);
// sessions are in persister too
Assert.assertEquals(3, persister.getUserSessionsCount(true));
Time.setOffset(300);
log.infof("Set time offset to 300. Time is: %d", Time.currentTime());
// Set lastSessionRefresh to currentSession[0] to 0
session0.setLastSessionRefresh(Time.currentTime());
});
// Increase timeOffset and update LSR of the session two times - first to 20 days and then to 21 days. At least one of updates
// will propagate to PersisterLastSessionRefreshStore and update DB (Single update is not 100% sure as there is still a
// chance of delayed periodic task to be run in the meantime and causing race-condition, which would mean LSR not updated in the DB)
for (int i=0 ; i<2 ; i++) {
int timeOffset = 1728000 + (i * 86400);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired4) -> {
currentSession = sessionExpired4;
realm = currentSession.realms().getRealm("test");
UserSessionModel[] origSessions = origSessionsAt.get();
Time.setOffset(timeOffset);
log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime());
UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId());
session0.setLastSessionRefresh(Time.currentTime());
});
}
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired5) -> {
currentSession = sessionExpired5;
realm = currentSession.realms().getRealm("test");
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
// Increase timeOffset - 40 days
Time.setOffset(3456000);
log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime());
// Expire and ensure that all sessions despite session0 were removed
currentSession.sessions().removeExpired(realm);
persister.removeExpired(realm);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired6) -> {
currentSession = sessionExpired6;
realm = currentSession.realms().getRealm("test");
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
UserSessionModel[] origSessions = origSessionsAt.get();
// assert session0 is the only session found
Assert.assertNotNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId()));
Assert.assertNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[1].getId()));
Assert.assertNull(currentSession.sessions().getOfflineUserSession(realm, origSessions[2].getId()));
Assert.assertEquals(1, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found
Time.setOffset(7000000);
currentSession.sessions().removeExpired(realm);
persister.removeExpired(realm);
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired7) -> {
currentSession = sessionExpired7;
realm = currentSession.realms().getRealm("test");
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
for (String userSessionId : offlineSessions.keySet()) {
Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId));
}
Assert.assertEquals(0, persister.getUserSessionsCount(true));
});
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
}
}
private static Set<String> createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel
userSession) {
Set<String> offlineSessions = new HashSet<>();
@ -655,7 +509,6 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
currentSession.users().addUser(realm, "user2").setEmail("user2@localhost");
}
sessionManager = new UserSessionManager(currentSession);
persister = currentSession.getProvider(UserSessionPersisterProvider.class);
}
@Override

View file

@ -26,7 +26,6 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserManager;
@ -47,12 +46,11 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
@ -121,9 +119,10 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
public void testUpdateSession(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel[] sessions = createSessions(session);
session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000);
int lastRefresh = Time.currentTime();
session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(lastRefresh);
assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh());
assertEquals(lastRefresh, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh());
}
@Test
@ -131,8 +130,9 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
public void testUpdateSessionInSameTransaction(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel[] sessions = createSessions(session);
session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(1000);
assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh());
int lastRefresh = Time.currentTime();
session.sessions().getUserSession(realm, sessions[0].getId()).setLastSessionRefresh(lastRefresh);
assertEquals(lastRefresh, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh());
}
@Test
@ -249,13 +249,6 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel[] sessions = createSessions(session);
KeycloakTransaction transaction = session.getTransactionManager();
if (!transaction.getRollbackOnly()) {
transaction.commit();
}
assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user1"))
.collect(Collectors.toList()), sessions[0], sessions[1]);
assertSessions(session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user2"))
@ -266,11 +259,8 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
@ModelTest
public void testRemoveUserSessionsByUser(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
createSessions(session);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
inheritClientConnection(session, kcSession);
createSessions(kcSession);
});
Map<String, Integer> clientSessionsKept = session.sessions().getUserSessionsStream(realm,
session.users().getUserByUsername(realm, "user2"))
.collect(Collectors.toMap(model -> model.getId(), model -> model.getAuthenticatedClientSessions().keySet().size()));
@ -307,10 +297,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
@ModelTest
public void testRemoveUserSessionsByRealm(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
inheritClientConnection(session, kcSession);
createSessions(kcSession);
});
createSessions(session);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
kcSession.sessions().removeUserSessions(realm);
@ -411,12 +398,13 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
public void testTransientUserSession(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
ClientModel client = realm.getClientByClientId("test-app");
String userSessionId = UUID.randomUUID().toString();
// create an user session, but don't persist it to infinispan
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> {
long sessionsBefore = session1.sessions().getActiveUserSessions(realm, client);
UserSessionModel userSession = session1.sessions().createUserSession("123", realm, session1.users().getUserByUsername(realm, "user1"),
UserSessionModel userSession = session1.sessions().createUserSession(userSessionId, realm, session1.users().getUserByUsername(realm, "user1"),
"user1", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
AuthenticatedClientSessionModel clientSession = session1.sessions().createClientSession(realm, client, userSession);
assertEquals(userSession, clientSession.getUserSession());
@ -424,7 +412,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
assertSession(userSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.1", userSession.getStarted(), userSession.getStarted(), "test-app");
// Can find session by ID in current transaction
UserSessionModel foundSession = session1.sessions().getUserSession(realm, "123");
UserSessionModel foundSession = session1.sessions().getUserSession(realm, userSessionId);
Assert.assertEquals(userSession, foundSession);
// Count of sessions should be still the same
@ -433,7 +421,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
// create an user session whose last refresh exceeds the max session idle timeout.
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> {
UserSessionModel userSession = session1.sessions().getUserSession(realm, "123");
UserSessionModel userSession = session1.sessions().getUserSession(realm, userSessionId);
Assert.assertNull(userSession);
});
}
@ -548,12 +536,6 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel[] sessions = createSessions(session);
KeycloakTransaction transaction = session.getTransactionManager();
if (!transaction.getRollbackOnly()) {
transaction.commit();
}
assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("test-app"))
.collect(Collectors.toList()), sessions[0], sessions[1], sessions[2]);
assertSessions(session.sessions().getUserSessionsStream(realm, realm.getClientByClientId("third-party"))
@ -564,25 +546,23 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
@ModelTest
public void testGetByClientPaginated(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
try {
for (int i = 0; i < 25; i++) {
Time.setOffset(i);
UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0." + i, "form", false, null, null);
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession);
assertNotNull(clientSession);
clientSession.setRedirectUri("http://redirect");
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state");
clientSession.setTimestamp(userSession.getStarted());
userSession.setLastSessionRefresh(userSession.getStarted());
}
} finally {
Time.setOffset(0);
}
KeycloakTransaction transaction = session.getTransactionManager();
if (!transaction.getRollbackOnly()) {
transaction.commit();
}
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
try {
for (int i = 0; i < 25; i++) {
Time.setOffset(i);
UserSessionModel userSession = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0." + i, "form", false, null, null);
AuthenticatedClientSessionModel clientSession = kcSession.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession);
assertNotNull(clientSession);
clientSession.setRedirectUri("http://redirect");
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state");
clientSession.setTimestamp(userSession.getStarted());
userSession.setLastSessionRefresh(userSession.getStarted());
}
} finally {
Time.setOffset(0);
}
});
assertPaginatedSession(session, realm, realm.getClientByClientId("test-app"), 0, 1, 1);
assertPaginatedSession(session, realm, realm.getClientByClientId("test-app"), 0, 10, 10);
@ -612,6 +592,8 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
@ModelTest
public void testAuthenticatedClientSessions(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
realm.setSsoSessionIdleTimeout(1800);
realm.setSsoSessionMaxLifespan(36000);
UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
ClientModel client1 = realm.getClientByClientId("test-app");
@ -620,19 +602,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
// Create client1 session
AuthenticatedClientSessionModel clientSession1 = session.sessions().createClientSession(realm, client1, userSession);
clientSession1.setAction("foo1");
clientSession1.setTimestamp(100);
int currentTime1 = Time.currentTime();
clientSession1.setTimestamp(currentTime1);
// Create client2 session
AuthenticatedClientSessionModel clientSession2 = session.sessions().createClientSession(realm, client2, userSession);
clientSession2.setAction("foo2");
clientSession2.setTimestamp(200);
int currentTime2 = Time.currentTime();
clientSession2.setTimestamp(currentTime2);
// Ensure sessions are here
userSession = session.sessions().getUserSession(realm, userSession.getId());
Map<String, AuthenticatedClientSessionModel> clientSessions = userSession.getAuthenticatedClientSessions();
Assert.assertEquals(2, clientSessions.size());
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", 100);
testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", 200);
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", currentTime1);
testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", currentTime2);
// Update session1
clientSessions.get(client1.getId()).setAction("foo1-updated");
@ -641,20 +625,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
// Ensure updated
userSession = session.sessions().getUserSession(realm, userSession.getId());
clientSessions = userSession.getAuthenticatedClientSessions();
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100);
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", currentTime1);
// Rewrite session2
clientSession2 = session.sessions().createClientSession(realm, client2, userSession);
clientSession2.setAction("foo2-rewrited");
clientSession2.setTimestamp(300);
int currentTime3 = Time.currentTime();
clientSession2.setTimestamp(currentTime3);
// Ensure updated
userSession = session.sessions().getUserSession(realm, userSession.getId());
clientSessions = userSession.getAuthenticatedClientSessions();
Assert.assertEquals(2, clientSessions.size());
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100);
testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", 300);
testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", currentTime1);
testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", currentTime3);
// remove session
clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId());
@ -675,19 +660,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
}
private static void assertPaginatedSession(KeycloakSession session, RealmModel realm, ClientModel client, int start, int max, int expectedSize) {
List<UserSessionModel> sessions = session.sessions().getUserSessionsStream(realm, client, start, max).collect(Collectors.toList());
String[] actualIps = new String[sessions.size()];
for (int i = 0; i < actualIps.length; i++) {
actualIps[i] = sessions.get(i).getIpAddress();
}
String[] expectedIps = new String[expectedSize];
for (int i = 0; i < expectedSize; i++) {
expectedIps[i] = "127.0.0." + (i + start);
}
assertArrayEquals(expectedIps, actualIps);
assertEquals(expectedSize, session.sessions().getUserSessionsStream(realm, client, start, max).count());
}
@Test
@ -698,60 +671,61 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
RealmModel realm = session.realms().getRealmByName("test");
createSessions(session);
KeycloakTransaction transaction = session.getTransactionManager();
if (!transaction.getRollbackOnly()) {
transaction.commit();
}
assertEquals(3, session.sessions().getActiveUserSessions(realm, realm.getClientByClientId("test-app")));
assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.getClientByClientId("third-party")));
}
@Test
public void loginFailures() {
testingClient.server().run(UserSessionProviderTest::loginFailures);
}
public static void loginFailures(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
UserLoginFailureModel failure1 = session.sessions().addUserLoginFailure(realm, "user1");
failure1.incrementFailures();
testingClient.server().run((KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
UserLoginFailureModel failure1 = kcSession.loginFailures().addUserLoginFailure(realm, "user1");
failure1.incrementFailures();
UserLoginFailureModel failure2 = session.sessions().addUserLoginFailure(realm, "user2");
failure2.incrementFailures();
failure2.incrementFailures();
UserLoginFailureModel failure2 = kcSession.loginFailures().addUserLoginFailure(realm, "user2");
failure2.incrementFailures();
failure2.incrementFailures();
});
session.getTransactionManager().commit();
testingClient.server().run((KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
failure1 = session.sessions().getUserLoginFailure(realm, "user1");
assertEquals(1, failure1.getNumFailures());
UserLoginFailureModel failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1");
assertEquals(1, failure1.getNumFailures());
failure2 = session.sessions().getUserLoginFailure(realm, "user2");
assertEquals(2, failure2.getNumFailures());
UserLoginFailureModel failure2 = kcSession.loginFailures().getUserLoginFailure(realm, "user2");
assertEquals(2, failure2.getNumFailures());
//session.getTransactionManager().commit();
// Add the failure, which already exists
failure1.incrementFailures();
// Add the failure, which already exists
//failure1 = session.sessions().addUserLoginFailure(realm, "user1");
failure1.incrementFailures();
assertEquals(2, failure1.getNumFailures());
//failure1 = session.sessions().getUserLoginFailure(realm, "user1");
assertEquals(2, failure1.getNumFailures());
failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1");
failure1.clearFailures();
failure1 = session.sessions().getUserLoginFailure(realm, "user1");
failure1.clearFailures();
failure1 = kcSession.loginFailures().getUserLoginFailure(realm, "user1");
assertEquals(0, failure1.getNumFailures());
});
session.getTransactionManager().commit();
testingClient.server().run((KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
kcSession.loginFailures().removeUserLoginFailure(realm, "user1");
});
failure1 = session.sessions().getUserLoginFailure(realm, "user1");
assertEquals(0, failure1.getNumFailures());
testingClient.server().run((KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
session.sessions().removeUserLoginFailure(realm, "user1");
session.sessions().removeUserLoginFailure(realm, "user2");
assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user1"));
assertNull(session.sessions().getUserLoginFailure(realm, "user1"));
kcSession.loginFailures().removeAllUserLoginFailures(realm);
});
session.sessions().removeAllUserLoginFailures(realm);
assertNull(session.sessions().getUserLoginFailure(realm, "user2"));
testingClient.server().run((KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user1"));
assertNull(kcSession.loginFailures().getUserLoginFailure(realm, "user2"));
});
}
@Test
@ -760,43 +734,20 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
}
public static void testOnUserRemoved(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user1 = session.users().getUserByUsername(realm, "user1");
UserModel user2 = session.users().getUserByUsername(realm, "user2");
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
createSessions(session);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
assertEquals(2, session.sessions().getUserSessionsStream(realm, user1).count());
assertEquals(1, session.sessions().getUserSessionsStream(realm, user2).count());
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
//createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), sessions[2]);
clientSession.setRedirectUri("http://redirct");
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state");
session.sessions().addUserLoginFailure(realm, user1.getId());
session.sessions().addUserLoginFailure(realm, user2.getId());
session.userStorageManager().removeUser(realm, user1);
// remove user1
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) ->
(new UserManager(kcSession)).removeUser(realm, user1));
assertEquals(0, session.sessions().getUserSessionsStream(realm, user1).count());
session.getTransactionManager().commit();
assertNotEquals(0, session.sessions().getUserSessionsStream(realm, session.users().getUserByUsername(realm, "user2")).count());
user1 = session.users().getUserByUsername(realm, "user1");
user2 = session.users().getUserByUsername(realm, "user2");
// it seems as if Null does not happen with the new test suite. The sizes of these are ZERO so the removes worked at this point.
//assertNull(session.sessions().getUserLoginFailure(realm, user1.getId()));
//assertNotNull(session.sessions().getUserLoginFailure(realm, user2.getId()));
assertEquals(1, session.sessions().getUserSessionsStream(realm, user2).count());
}
private static AuthenticatedClientSessionModel createClientSession(KeycloakSession session, ClientModel client, UserSessionModel userSession, String redirect, String state) {
@ -808,20 +759,21 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
}
private static UserSessionModel[] createSessions(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
RealmModel realm = kcSession.realms().getRealmByName("test");
createClientSession(session, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(session, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
sessions[0] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(session, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(kcSession, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
sessions[1] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = kcSession.sessions().createUserSession(realm, kcSession.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(kcSession, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
});
return sessions;
}

View file

@ -529,8 +529,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest {
assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm);
assertNoSessionsInClient(nbc.subConsumerRealmName(), accountClientIdSubConsumerRealm, userIdSubConsumerRealm,
sessionIdSubConsumerRealm);
assertActiveSessionInClient(nbc.providerRealmName(), brokerClientIdProviderRealm, userIdProviderRealm,
@ -570,8 +569,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest {
assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm);
assertNoSessionsInClient(nbc.subConsumerRealmName(), accountClientIdSubConsumerRealm, userIdSubConsumerRealm,
sessionIdSubConsumerRealm);
assertActiveSessionInClient(nbc.providerRealmName(), brokerClientIdProviderRealm, userIdProviderRealm,
@ -596,8 +594,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest {
logoutFromRealm(getConsumerRoot(), nbc.consumerRealmName());
assertNoSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertActiveOfflineSessionInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm);
String logoutTokenEncoded = getLogoutTokenEncodedAndSigned(userIdProviderRealm, sessionIdProviderRealm, true);
@ -606,8 +603,7 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest {
assertThat(response, Matchers.statusCodeIsHC(Response.Status.OK));
}
assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm,
sessionIdConsumerRealm);
assertNoOfflineSessionsInClient(nbc.consumerRealmName(), consumerClientId, userIdConsumerRealm);
}
private void subConsumerIdpRequestsOfflineSessions() {
@ -790,25 +786,23 @@ public class BackchannelLogoutTest extends AbstractNestedBrokerTest {
.collect(Collectors.toList());
}
private void assertActiveOfflineSessionInClient(String realmName, String clientId, String userId,
String sessionId) {
List<UserSessionRepresentation> sessions = getOfflineClientSessions(realmName, clientId, userId, sessionId);
private void assertActiveOfflineSessionInClient(String realmName, String clientId, String userId) {
List<UserSessionRepresentation> sessions = getOfflineClientSessions(realmName, clientId, userId);
assertThat(sessions.size(), is(1));
}
private void assertNoOfflineSessionsInClient(String realmName, String clientId, String userId, String sessionId) {
List<UserSessionRepresentation> sessions = getOfflineClientSessions(realmName, clientId, userId, sessionId);
private void assertNoOfflineSessionsInClient(String realmName, String clientId, String userId) {
List<UserSessionRepresentation> sessions = getOfflineClientSessions(realmName, clientId, userId);
assertThat(sessions.size(), is(0));
}
private List<UserSessionRepresentation> getOfflineClientSessions(String realmName, String clientUuid, String userId,
String sessionId) {
private List<UserSessionRepresentation> getOfflineClientSessions(String realmName, String clientUuid, String userId) {
return adminClient.realm(realmName)
.clients()
.get(clientUuid)
.getOfflineUserSessions(0, 5)
.stream()
.filter(s -> s.getUserId().equals(userId) && s.getId().equals(sessionId))
.filter(s -> s.getUserId().equals(userId))
.collect(Collectors.toList());
}

View file

@ -38,6 +38,7 @@ import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
@ -255,17 +256,17 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
setTimeOffset(3000000);
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(newRefreshTokenString, "secret1");
RefreshToken newRefreshToken = oauth.parseRefreshToken(newRefreshTokenString);
Assert.assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(offlineToken.getId(), sessionId)
events.expectRefresh(offlineToken.getId(), newRefreshToken.getSessionState())
.client("offline-client")
.error(Errors.INVALID_TOKEN)
.user(userId)
.clearDetails()
.assertEvent();
setTimeOffset(0);
}
@ -287,7 +288,6 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(200, response.getStatusCode());
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
// Assert new refreshToken in the response
String newRefreshToken = response.getRefreshToken();
@ -393,7 +393,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
.assertEvent();
// Refresh with new refreshToken is successful now
testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, token.getSessionState(), userId);
testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, offlineToken2.getSessionState(), userId);
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
}
@ -591,6 +591,40 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
}
@Test
public void onlineOfflineTokenLogout() throws Exception {
oauth.clientId("offline-client");
// create online session
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
assertEquals(200, response.getStatusCode());
// assert refresh token
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
assertEquals(200, response.getStatusCode());
// create offline session
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
OAuthClient.AccessTokenResponse offlineResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
assertEquals(200, offlineResponse.getStatusCode());
// assert refresh offline token
OAuthClient.AccessTokenResponse offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1");
assertEquals(200, offlineRefresh.getStatusCode());
// logout online session
CloseableHttpResponse logoutResponse = oauth.scope("").doLogout(response.getRefreshToken(), "secret1");
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
// assert the online session is gone
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
assertEquals(400, response.getStatusCode());
// assert the offline token refresh still works
offlineRefresh = oauth.doRefreshTokenRequest(offlineResponse.getRefreshToken(), "secret1");
assertEquals(200, offlineRefresh.getStatusCode());
}
@Test
public void browserOfflineTokenLogoutFollowedByLoginSameSession() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
@ -621,11 +655,14 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
assertEquals(0, offlineToken.getExpiration());
String offlineUserSessionId = testingClient.server().fetch((KeycloakSession session) ->
session.sessions().getOfflineUserSession(session.realms().getRealmByName("test"), offlineToken.getSessionState()).getId(), String.class);
// logout offline session
try (CloseableHttpResponse logoutResponse = oauth.doLogout(offlineTokenString, "secret1")) {
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
}
events.expectLogout(offlineToken.getSessionState())
events.expectLogout(offlineUserSessionId)
.client("offline-client")
.removeDetail(Details.REDIRECT_URI)
.assertEvent();
@ -752,7 +789,6 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
offlineToken = oauth.parseRefreshToken(offlineTokenString);
Assert.assertEquals(200, tokenResponse.getStatusCode());
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
// wait to expire
setTimeOffset(offset);

View file

@ -64,6 +64,14 @@
"provider": "${keycloak.authSession.provider:infinispan}"
},
"userSessions": {
"provider": "${keycloak.userSession.provider:infinispan}"
},
"loginFailure": {
"provider": "${keycloak.loginFailure.provider:infinispan}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {
@ -93,10 +101,6 @@
}
},
"userSessions": {
"provider" : "${keycloak.userSessions.provider:infinispan}"
},
"timer": {
"provider": "basic"
},

View file

@ -3,7 +3,10 @@
"realm": "test",
"enabled": true,
"accessTokenLifespan": 10,
"ssoSessionIdleTimeout": 30,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"offlineSessionIdleTimeout": 2592000,
"offlineSessionMaxLifespan": 5184000,
"sslRequired": "external",
"registrationAllowed": true,
"resetPasswordAllowed": true,

View file

@ -6,6 +6,10 @@
"registrationAllowed": true,
"resetPasswordAllowed": true,
"editUsernameAllowed" : true,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"offlineSessionIdleTimeout": 2592000,
"offlineSessionMaxLifespan": 5184000,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],

View file

@ -0,0 +1,45 @@
/*
* Copyright 2021 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.testsuite.model;
import org.keycloak.common.util.ResteasyProvider;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class ResteasyNullProvider implements ResteasyProvider {
@Override
public <R> R getContextData(Class<R> type) {
return null;
}
@Override
public void pushDefaultContextObject(Class type, Object instance) {
}
@Override
public void pushContext(Class type, Object instance) {
}
@Override
public void clearContextData() {
}
}

View file

@ -0,0 +1,17 @@
#
# Copyright 2021 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.testsuite.model.ResteasyNullProvider

View file

@ -33,8 +33,11 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmSpi;
import org.keycloak.models.RoleSpi;
import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.UserSpi;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
@ -67,6 +70,7 @@ import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.keycloak.timer.TimerSpi;
/**
* Base of testcases that operate on session level. The tests derived from this class
@ -172,6 +176,9 @@ public abstract class KeycloakModelTest {
.add(RealmSpi.class)
.add(RoleSpi.class)
.add(StoreFactorySpi.class)
.add(TimerSpi.class)
.add(UserLoginFailureSpi.class)
.add(UserSessionSpi.class)
.add(UserSpi.class)
.build();
@ -228,15 +235,15 @@ public abstract class KeycloakModelTest {
}
};
res.init();
res.publish(new PostMigrationEvent());
return res;
}
public static void reinitializeKeycloakSessionFactory() {
DefaultKeycloakSessionFactory f = createKeycloakSessionFactory();
if (FACTORY != null) {
FACTORY.close();
}
FACTORY = f;
FACTORY = createKeycloakSessionFactory();
}
@BeforeClass

View file

@ -0,0 +1,217 @@
/*
* 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.testsuite.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
import org.keycloak.services.managers.UserSessionManager;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@RequireProvider(UserSessionPersisterProvider.class)
@RequireProvider(UserSessionProvider.class)
@RequireProvider(UserProvider.class)
@RequireProvider(RealmProvider.class)
public class UserSessionInitializerTest extends KeycloakModelTest {
private String realmId;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
realm.setSsoSessionIdleTimeout(1800);
realm.setSsoSessionMaxLifespan(36000);
this.realmId = realm.getId();
s.users().addUser(realm, "user1").setEmail("user1@localhost");
s.users().addUser(realm, "user2").setEmail("user2@localhost");
UserSessionPersisterProviderTest.createClients(s, realm);
}
@Override
public void cleanEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().getRealm(realmId);
s.sessions().removeUserSessions(realm);
UserModel user1 = s.users().getUserByUsername(realm, "user1");
UserModel user2 = s.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(s);
if (user1 != null) {
um.removeUser(realm, user1);
}
if (user2 != null) {
um.removeUser(realm, user2);
}
s.realms().removeRealm(realmId);
}
@Test
public void testUserSessionInitializer() {
String[] origSessionIds = createSessionsInPersisterOnly();
int started = Time.currentTime();
reinitializeKeycloakSessionFactory();
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Assert sessions are in
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sesions for client 'test-app'", session.sessions().getOfflineSessionsCount(realm, testApp), is((long) 3));
assertThat("Count of offline sesions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1));
List<UserSessionModel> loadedSessions = session.sessions().getOfflineUserSessionsStream(realm, testApp, 0, 10)
.collect(Collectors.toList());
UserSessionPersisterProviderTest.assertSessions(loadedSessions, origSessionIds);
assertSessionLoaded(loadedSessions, origSessionIds[0], session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
assertSessionLoaded(loadedSessions, origSessionIds[1], session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
assertSessionLoaded(loadedSessions, origSessionIds[2], session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
});
}
@Test
public void testUserSessionInitializerWithDeletingClient() {
String[] origSessionIds = createSessionsInPersisterOnly();
int started = Time.currentTime();
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Delete one of the clients now
ClientModel testApp = realm.getClientByClientId("test-app");
realm.removeClient(testApp.getId());
});
reinitializeKeycloakSessionFactory();
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Assert sessions are in
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sesions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 1));
List<UserSessionModel> loadedSessions = session.sessions().getOfflineUserSessionsStream(realm, thirdparty, 0, 10)
.collect(Collectors.toList());
assertThat("Size of loaded Sessions", loadedSessions.size(), is(1));
assertSessionLoaded(loadedSessions, origSessionIds[0], session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "third-party");
// Revert client
realm.addClient("test-app");
});
}
// Create sessions in persister + infinispan, but then delete them from infinispan cache. This is to allow later testing of initializer. Return the list of "origSessions"
private String[] createSessionsInPersisterOnly() {
UserSessionModel[] origSessions = inComittedTransaction(session -> { return UserSessionPersisterProviderTest.createSessions(session, realmId); });
String[] res = new String[origSessions.length];
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionManager sessionManager = new UserSessionManager(session);
int i = 0;
for (UserSessionModel origSession : origSessions) {
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
}
String cs = userSession.getNote(UserSessionModel.CORRESPONDING_SESSION_ID);
res[i] = cs == null ? userSession.getId() : cs;
i++;
}
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Delete local user cache (persisted sessions are still kept)
UserSessionProvider provider = session.getProvider(UserSessionProvider.class);
if (provider instanceof InfinispanUserSessionProvider) {
// Remove in-memory representation of the offline sessions
((InfinispanUserSessionProvider) provider).removeLocalUserSessions(realm.getId(), true);
// Clear ispn cache to ensure initializerState is removed as well
InfinispanConnectionProvider infinispan = session.getProvider(InfinispanConnectionProvider.class);
if (infinispan != null) {
infinispan.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME).clear();
}
}
});
inComittedTransaction(session -> {
// This is only valid in infinispan provider where the offline session is loaded upon start and never reloaded
UserSessionProvider provider = session.getProvider(UserSessionProvider.class);
if (provider instanceof InfinispanUserSessionProvider) {
RealmModel realm = session.realms().getRealm(realmId);
ClientModel testApp = realm.getClientByClientId("test-app");
ClientModel thirdparty = realm.getClientByClientId("third-party");
assertThat("Count of offline sessions for client 'test-app'", session.sessions().getOfflineSessionsCount(realm, testApp), is((long) 0));
assertThat("Count of offline sessions for client 'third-party'", session.sessions().getOfflineSessionsCount(realm, thirdparty), is((long) 0));
}
});
return res;
}
private void assertSessionLoaded(List<UserSessionModel> sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
for (UserSessionModel session : sessions) {
if (session.getId().equals(id)) {
UserSessionPersisterProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients);
return;
}
}
Assert.fail("Session with ID " + id + " not found in the list");
}
}

View file

@ -0,0 +1,625 @@
/*
* 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.testsuite.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import org.keycloak.models.Constants;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.hamcrest.Matchers;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@RequireProvider(UserSessionPersisterProvider.class)
@RequireProvider(value = UserSessionProvider.class, only = InfinispanUserSessionProviderFactory.PROVIDER_ID)
@RequireProvider(UserProvider.class)
@RequireProvider(RealmProvider.class)
public class UserSessionPersisterProviderTest extends KeycloakModelTest {
private String realmId;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
s.users().addUser(realm, "user1").setEmail("user1@localhost");
s.users().addUser(realm, "user2").setEmail("user2@localhost");
createClients(s, realm);
}
protected static void createClients(KeycloakSession s, RealmModel realm) {
ClientModel clientModel = s.clients().addClient(realm, "test-app");
clientModel.setEnabled(true);
clientModel.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
Set<String> redirects = new HashSet<>(Arrays.asList("http://localhost:8180/auth/realms/master/app/auth/*",
"https://localhost:8543/auth/realms/master/app/auth/*",
"http://localhost:8180/auth/realms/test/app/auth/*",
"https://localhost:8543/auth/realms/test/app/auth/*"));
clientModel.setRedirectUris(redirects);
clientModel.setSecret("password");
clientModel = s.clients().addClient(realm, "third-party");
clientModel.setEnabled(true);
clientModel.setConsentRequired(true);
clientModel.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
clientModel.setRedirectUris(redirects);
clientModel.setSecret("password");
}
@Override
public void cleanEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().getRealm(realmId);
s.sessions().removeUserSessions(realm);
UserModel user1 = s.users().getUserByUsername(realm, "user1");
UserModel user2 = s.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(s);
if (user1 != null) {
um.removeUser(realm, user1);
}
if (user2 != null) {
um.removeUser(realm, user2);
}
s.realms().removeRealm(realmId);
}
@Test
public void testPersistenceWithLoad() {
int started = Time.currentTime();
final UserSessionModel[] userSession = new UserSessionModel[1];
UserSessionModel[] origSessions = inComittedTransaction(session -> {
// Create some sessions in infinispan
return createSessions(session, realmId);
});
inComittedTransaction(session -> {
// Persist 3 created userSessions and clientSessions as offline
RealmModel realm = session.realms().getRealm(realmId);
ClientModel testApp = realm.getClientByClientId("test-app");
session.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList())
.forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true));
});
inComittedTransaction(session -> {
// Persist 1 online session
RealmModel realm = session.realms().getRealm(realmId);
userSession[0] = session.sessions().getUserSession(realm, origSessions[0].getId());
persistUserSession(session, userSession[0], false);
});
inComittedTransaction(session -> { // Assert online session
RealmModel realm = session.realms().getRealm(realmId);
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, false, 1, 1, 1);
assertSession(loadedSessions.get(0), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
});
inComittedTransaction(session -> {
// Assert offline sessions
RealmModel realm = session.realms().getRealm(realmId);
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 2, 2, 3);
assertSessions(loadedSessions, new String[] { origSessions[0].getId(), origSessions[1].getId(), origSessions[2].getId() });
assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername(realm, "user1"), "127.0.0.1", started, started, "test-app", "third-party");
assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
});
}
@Test
public void testUpdateAndRemove() {
int started = Time.currentTime();
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
AtomicReference<List<UserSessionModel>> loadedSessionsAt = new AtomicReference<>();
AtomicReference<UserSessionModel> userSessionAt = new AtomicReference<>();
AtomicReference<UserSessionModel> persistedSessionAt = new AtomicReference<>();
inComittedTransaction(session -> {
// Create some sessions in infinispan
UserSessionModel[] origSessions = createSessions(session, realmId);
origSessionsAt.set(origSessions);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel[] origSessions = origSessionsAt.get();
// Persist 1 offline session
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[1].getId());
userSessionAt.set(userSession);
persistUserSession(session, userSession, true);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
// Load offline session
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1);
loadedSessionsAt.set(loadedSessions);
UserSessionModel persistedSession = loadedSessions.get(0);
persistedSessionAt.set(persistedSession);
assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
// create new clientSession
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()),
"http://redirect", "state");
persister.createClientSession(clientSession, true);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
UserSessionModel userSession = userSessionAt.get();
// Remove clientSession
persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
// Assert clientSession removed
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1);
UserSessionModel persistedSession = loadedSessions.get(0);
assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app");
// Remove userSession
persister.removeUserSession(persistedSession.getId(), true);
});
inComittedTransaction(session -> {
// Assert nothing found
loadPersistedSessionsPaginated(session, true, 10, 0, 0);
});
}
@Test
public void testOnRealmRemoved() {
AtomicReference<String> userSessionID = new AtomicReference<>();
inComittedTransaction(session -> {
RealmModel fooRealm = session.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(session.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + fooRealm.getName()));
fooRealm.addClient("foo-app");
session.users().addUser(fooRealm, "user3");
UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null);
userSessionID.set(userSession.getId());
createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
});
inComittedTransaction(session -> {
// Persist offline session
RealmModel fooRealm = session.realms().getRealm("foo");
UserSessionModel userSession = session.sessions().getUserSession(fooRealm, userSessionID.get());
persistUserSession(session, userSession, true);
});
inComittedTransaction(session -> {
// Assert session was persisted
loadPersistedSessionsPaginated(session, true, 10, 1, 1);
// Remove realm
RealmManager realmMgr = new RealmManager(session);
realmMgr.removeRealm(realmMgr.getRealm("foo"));
});
inComittedTransaction(session -> {
// Assert nothing loaded
loadPersistedSessionsPaginated(session, true, 10, 0, 0);
});
}
@Test
public void testOnClientRemoved() {
int started = Time.currentTime();
AtomicReference<String> userSessionID = new AtomicReference<>();
inComittedTransaction(session -> {
RealmModel fooRealm = session.realms().createRealm("foo", "foo");
fooRealm.setDefaultRole(session.roles().addRealmRole(fooRealm, Constants.DEFAULT_ROLES_ROLE_PREFIX));
fooRealm.addClient("foo-app");
fooRealm.addClient("bar-app");
session.users().addUser(fooRealm, "user3");
UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername(fooRealm, "user3"), "user3", "127.0.0.1", "form", true, null, null);
userSessionID.set(userSession.getId());
createClientSession(session, realmId, fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state");
createClientSession(session, realmId, fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state");
});
inComittedTransaction(session -> {
RealmModel fooRealm = session.realms().getRealm("foo");
// Persist offline session
UserSessionModel userSession = session.sessions().getUserSession(fooRealm, userSessionID.get());
persistUserSession(session, userSession, true);
});
inComittedTransaction(session -> {
RealmManager realmMgr = new RealmManager(session);
ClientManager clientMgr = new ClientManager(realmMgr);
RealmModel fooRealm = realmMgr.getRealm("foo");
// Assert session was persisted with both clientSessions
UserSessionModel persistedSession = loadPersistedSessionsPaginated(session, true, 10, 1, 1).get(0);
assertSession(persistedSession, session.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "foo-app", "bar-app");
// Remove foo-app client
ClientModel client = fooRealm.getClientByClientId("foo-app");
clientMgr.removeClient(fooRealm, client);
});
inComittedTransaction(session -> {
RealmManager realmMgr = new RealmManager(session);
ClientManager clientMgr = new ClientManager(realmMgr);
RealmModel fooRealm = realmMgr.getRealm("foo");
// Assert just one bar-app clientSession persisted now
UserSessionModel persistedSession = loadPersistedSessionsPaginated(session, true, 10, 1, 1).get(0);
assertSession(persistedSession, session.users().getUserByUsername(fooRealm, "user3"), "127.0.0.1", started, started, "bar-app");
// Remove bar-app client
ClientModel client = fooRealm.getClientByClientId("bar-app");
clientMgr.removeClient(fooRealm, client);
});
inComittedTransaction(session -> {
// Assert loading still works - last userSession is still there, but no clientSession on it
loadPersistedSessionsPaginated(session, true, 10, 1, 1);
// Cleanup
RealmManager realmMgr = new RealmManager(session);
realmMgr.removeRealm(realmMgr.getRealm("foo"));
});
}
@Test
public void testOnUserRemoved() {
int started = Time.currentTime();
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
inComittedTransaction(session -> {
// Create some sessions in infinispan
UserSessionModel[] origSessions = createSessions(session, realmId);
origSessionsAt.set(origSessions);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel[] origSessions = origSessionsAt.get();
// Persist 2 offline sessions of 2 users
UserSessionModel userSession1 = session.sessions().getUserSession(realm, origSessions[1].getId());
UserSessionModel userSession2 = session.sessions().getUserSession(realm, origSessions[2].getId());
persistUserSession(session, userSession1, true);
persistUserSession(session, userSession2, true);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Load offline sessions
loadPersistedSessionsPaginated(session, true, 10, 1, 2);
// Properly delete user and assert his offlineSession removed
UserModel user1 = session.users().getUserByUsername(realm, "user1");
new UserManager(session).removeUser(realm, user1);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
Assert.assertEquals(1, persister.getUserSessionsCount(true));
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1);
UserSessionModel persistedSession = loadedSessions.get(0);
assertSession(persistedSession, session.users().getUserByUsername(realm, "user2"), "127.0.0.3", started, started, "test-app");
// KEYCLOAK-2431 Assert that userSessionPersister is resistent even to situation, when users are deleted "directly".
// No exception will happen. However session will be still there
UserModel user2 = session.users().getUserByUsername(realm, "user2");
session.users().removeUser(realm, user2);
loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1);
// Cleanup
UserSessionModel userSession = loadedSessions.get(0);
session.sessions().removeUserSession(realm, userSession);
persister.removeUserSession(userSession.getId(), userSession.isOffline());
});
}
// KEYCLOAK-1999
@Test
public void testNoSessions() {
inComittedTransaction(session -> {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
Stream<UserSessionModel> sessions = persister.loadUserSessionsStream(0, 1, true, 0, "abc");
Assert.assertEquals(0, sessions.count());
});
}
@Test
public void testMoreSessions() {
AtomicReference<List<UserSessionModel>> userSessionsAt = new AtomicReference<>();
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// Create 10 userSessions - each having 1 clientSession
List<UserSessionModel> userSessions = new ArrayList<>();
UserModel user = session.users().getUserByUsername(realm, "user1");
for (int i = 0; i < 20; i++) {
// Having different offsets for each session (to ensure that lastSessionRefresh is also different)
Time.setOffset(i);
UserSessionModel userSession = session.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null);
createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
userSessions.add(userSession);
}
userSessionsAt.set(userSessions);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
List<UserSessionModel> userSessions = userSessionsAt.get();
for (UserSessionModel userSession : userSessions) {
UserSessionModel userSession2 = session.sessions().getUserSession(realm, userSession.getId());
persistUserSession(session, userSession2, true);
}
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 2, 10, 20);
UserModel user = session.users().getUserByUsername(realm, "user1");
ClientModel testApp = realm.getClientByClientId("test-app");
for (UserSessionModel loadedSession : loadedSessions) {
assertEquals(user.getId(), loadedSession.getUser().getId());
assertEquals("127.0.0.1", loadedSession.getIpAddress());
assertEquals(user.getUsername(), loadedSession.getLoginUsername());
assertEquals(1, loadedSession.getAuthenticatedClientSessions().size());
assertTrue(loadedSession.getAuthenticatedClientSessions().containsKey(testApp.getId()));
}
});
}
@Test
public void testExpiredSessions() {
int started = Time.currentTime();
final UserSessionModel[] userSession1 = {null};
final UserSessionModel[] userSession2 = {null};
UserSessionModel[] origSessions = inComittedTransaction(session -> {
// Create some sessions in infinispan
return createSessions(session, realmId);
});
inComittedTransaction(session -> {
// Persist 2 offline sessions of 2 users
RealmModel realm = session.realms().getRealm(realmId);
userSession1[0] = session.sessions().getUserSession(realm, origSessions[1].getId());
userSession2[0] = session.sessions().getUserSession(realm, origSessions[2].getId());
persistUserSession(session, userSession1[0], true);
persistUserSession(session, userSession2[0], true);
});
inComittedTransaction(session -> {
// Update one of the sessions with lastSessionRefresh of 20 days ahead
int lastSessionRefresh = Time.currentTime() + 1728000;
RealmModel realm = session.realms().getRealm(realmId);
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
persister.updateLastSessionRefreshes(realm, lastSessionRefresh, Collections.singleton(userSession1[0].getId()), true);
// Increase time offset - 40 days
Time.setOffset(3456000);
try {
// Run expiration thread
persister.removeExpired(realm);
// Test the updated session is still in persister. Not updated session is not there anymore
List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1);
UserSessionModel persistedSession = loadedSessions.get(0);
assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, lastSessionRefresh, "test-app");
} finally {
// Cleanup
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
});
}
protected static AuthenticatedClientSessionModel createClientSession(KeycloakSession session, String realmId, ClientModel client, UserSessionModel userSession, String redirect, String state) {
RealmModel realm = session.realms().getRealm(realmId);
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
return clientSession;
}
protected static UserSessionModel[] createSessions(KeycloakSession session, String realmId) {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state");
createClientSession(session, realmId, realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state");
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state");
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user2"), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(session, realmId, realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state");
return sessions;
}
private void persistUserSession(KeycloakSession session, UserSessionModel userSession, boolean offline) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
persister.createUserSession(userSession, offline);
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
persister.createClientSession(clientSession, offline);
}
}
public static void assertSessionLoaded(List<UserSessionModel> sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
for (UserSessionModel session : sessions) {
if (session.getId().equals(id)) {
assertSession(session, user, ipAddress, started, lastRefresh, clients);
return;
}
}
Assert.fail("Session with ID " + id + " not found in the list");
}
private List<UserSessionModel> loadPersistedSessionsPaginated(KeycloakSession session, boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int count = persister.getUserSessionsCount(offline);
int pageCount = 0;
boolean next = true;
List<UserSessionModel> result = new ArrayList<>();
int lastCreatedOn = 0;
String lastSessionId = "abc";
while (next) {
List<UserSessionModel> sess = persister
.loadUserSessionsStream(0, sessionsPerPage, offline, lastCreatedOn, lastSessionId)
.collect(Collectors.toList());
if (sess.size() < sessionsPerPage) {
next = false;
// We had at least some session
if (sess.size() > 0) {
pageCount++;
}
} else {
pageCount++;
UserSessionModel lastSession = sess.get(sess.size() - 1);
lastCreatedOn = lastSession.getStarted();
lastSessionId = lastSession.getId();
}
result.addAll(sess);
}
Assert.assertEquals(expectedPageCount, pageCount);
Assert.assertEquals(expectedSessionsCount, result.size());
return result;
}
public static void assertSession(UserSessionModel session, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
assertEquals(user.getId(), session.getUser().getId());
assertEquals(ipAddress, session.getIpAddress());
assertEquals(user.getUsername(), session.getLoginUsername());
assertEquals("form", session.getAuthMethod());
assertTrue(session.isRememberMe());
assertTrue(session.getStarted() >= started - 1 && session.getStarted() <= started + 1);
assertTrue(session.getLastSessionRefresh() >= lastRefresh - 1 && session.getLastSessionRefresh() <= lastRefresh + 1);
String[] actualClients = new String[session.getAuthenticatedClientSessions().size()];
int i = 0;
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : session.getAuthenticatedClientSessions().entrySet()) {
String clientUUID = entry.getKey();
AuthenticatedClientSessionModel clientSession = entry.getValue();
Assert.assertEquals(clientUUID, clientSession.getClient().getId());
actualClients[i] = clientSession.getClient().getClientId();
i++;
}
assertThat(actualClients, Matchers.arrayContainingInAnyOrder(clients));
}
public static void assertSessions(List<UserSessionModel> actualSessions, String[] expectedSessionIds) {
String[] actual = new String[actualSessions.size()];
for (int i = 0; i < actual.length; i++) {
actual[i] = actualSessions.get(i).getId();
}
assertThat(actual, Matchers.arrayContainingInAnyOrder(expectedSessionIds));
}
}

View file

@ -0,0 +1,197 @@
/*
* Copyright 2021 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.testsuite.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
import org.keycloak.timer.TimerProvider;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import static org.keycloak.testsuite.model.UserSessionPersisterProviderTest.createClients;
import static org.keycloak.testsuite.model.UserSessionPersisterProviderTest.createSessions;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@RequireProvider(UserSessionPersisterProvider.class)
@RequireProvider(UserSessionProvider.class)
@RequireProvider(UserProvider.class)
@RequireProvider(RealmProvider.class)
public class UserSessionProviderModelTest extends KeycloakModelTest {
private String realmId;
private KeycloakSession kcSession;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
realm.setSsoSessionIdleTimeout(1800);
realm.setSsoSessionMaxLifespan(36000);
this.realmId = realm.getId();
this.kcSession = s;
s.users().addUser(realm, "user1").setEmail("user1@localhost");
s.users().addUser(realm, "user2").setEmail("user2@localhost");
createClients(s, realm);
}
@Override
public void cleanEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().getRealm(realmId);
s.sessions().removeUserSessions(realm);
UserModel user1 = s.users().getUserByUsername(realm, "user1");
UserModel user2 = s.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(s);
if (user1 != null) {
um.removeUser(realm, user1);
}
if (user2 != null) {
um.removeUser(realm, user2);
}
s.realms().removeRealm(realmId);
}
@Test
public void testMultipleSessionsRemovalInOneTransaction() {
UserSessionModel[] origSessions = inComittedTransaction(session -> { return createSessions(session, realmId); });
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
Assert.assertEquals(origSessions[0], userSession);
userSession = session.sessions().getUserSession(realm, origSessions[1].getId());
Assert.assertEquals(origSessions[1], userSession);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
session.sessions().removeUserSession(realm, origSessions[0]);
session.sessions().removeUserSession(realm, origSessions[1]);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
Assert.assertNull(userSession);
userSession = session.sessions().getUserSession(realm, origSessions[1].getId());
Assert.assertNull(userSession);
});
}
@Test
public void testExpiredClientSessions() {
// Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider
TimerProvider timer = kcSession.getProvider(TimerProvider.class);
TimerProvider.TimerTaskContext timerTaskCtx = null;
if (timer != null) {
timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
InfinispanTestUtil.setTestingTimeService(kcSession);
}
AtomicReference<List<String>> clientSessionIds = new AtomicReference<>();
try {
UserSessionModel[] origSessions = inComittedTransaction(session -> {
// create some user and client sessions
return createSessions(session, realmId);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
Assert.assertEquals(origSessions[0], userSession);
AuthenticatedClientSessionModel clientSession = session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"),
UUID.fromString(origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId()),
false);
Assert.assertEquals(origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId(), clientSession.getId());
userSession = session.sessions().getUserSession(realm, origSessions[1].getId());
Assert.assertEquals(origSessions[1], userSession);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
Collection<AuthenticatedClientSessionModel> values = userSession.getAuthenticatedClientSessions().values();
List<String> clientSessions = new LinkedList<>();
values.stream().forEach(clientSession -> {
// expire client sessions
clientSession.setTimestamp(1);
clientSessions.add(clientSession.getId());
});
clientSessionIds.set(clientSessions);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
// assert the user session is still there
UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
Assert.assertEquals(origSessions[0], userSession);
// assert the client sessions are expired
clientSessionIds.get().forEach(clientSessionId ->
Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), UUID.fromString(clientSessionId), false)));
});
} finally {
Time.setOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
if (timer != null && timerTaskCtx != null) {
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
InfinispanTestUtil.revertTimeService(kcSession);
}
}
}
}

View file

@ -0,0 +1,234 @@
/*
* Copyright 2021 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.testsuite.model;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
import org.keycloak.timer.TimerProvider;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@RequireProvider(UserSessionPersisterProvider.class)
@RequireProvider(value=UserSessionProvider.class, only={"infinispan"})
@RequireProvider(UserProvider.class)
@RequireProvider(RealmProvider.class)
public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
private String realmId;
private KeycloakSession kcSession;
private UserSessionManager sessionManager;
private UserSessionPersisterProvider persister;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
this.kcSession = s;
s.users().addUser(realm, "user1").setEmail("user1@localhost");
s.users().addUser(realm, "user2").setEmail("user2@localhost");
UserSessionPersisterProviderTest.createClients(s, realm);
}
@Override
public void cleanEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().getRealm(realmId);
s.sessions().removeUserSessions(realm);
UserModel user1 = s.users().getUserByUsername(realm, "user1");
UserModel user2 = s.users().getUserByUsername(realm, "user2");
UserManager um = new UserManager(s);
if (user1 != null) {
um.removeUser(realm, user1);
}
if (user2 != null) {
um.removeUser(realm, user2);
}
s.realms().removeRealm(realmId);
}
@Test
public void testExpired() {
// Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider
TimerProvider timer = kcSession.getProvider(TimerProvider.class);
TimerProvider.TimerTaskContext timerTaskCtx = null;
if (timer != null) {
timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
}
InfinispanTestUtil.setTestingTimeService(kcSession);
try {
// Key is userSessionId, value is set of client UUIDS
Map<String, Set<String>> offlineSessions = new HashMap<>();
ClientModel[] testApp = new ClientModel[1];
UserSessionModel[] origSessions = inComittedTransaction(session -> {
// Create some online sessions in infinispan
return UserSessionPersisterProviderTest.createSessions(session, realmId);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
sessionManager = new UserSessionManager(session);
persister = session.getProvider(UserSessionPersisterProvider.class);
// Persist 3 created userSessions and clientSessions as offline
testApp[0] = realm.getClientByClientId("test-app");
session.sessions().getUserSessionsStream(realm, testApp[0]).collect(Collectors.toList())
.forEach(userSession -> offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(session, userSession)));
// Assert all previously saved offline sessions found
for (Map.Entry<String, Set<String>> entry : offlineSessions.entrySet()) {
UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey());
Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue());
}
});
log.info("Persisted 3 sessions to UserSessionPersisterProvider");
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
persister = session.getProvider(UserSessionPersisterProvider.class);
UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId());
Assert.assertNotNull(session0);
// sessions are in persister too
Assert.assertEquals(3, persister.getUserSessionsCount(true));
Time.setOffset(300);
log.infof("Set time offset to 300. Time is: %d", Time.currentTime());
// Set lastSessionRefresh to currentSession[0] to 0
session0.setLastSessionRefresh(Time.currentTime());
});
// Increase timeOffset and update LSR of the session two times - first to 20 days and then to 21 days. At least one of updates
// will propagate to PersisterLastSessionRefreshStore and update DB (Single update is not 100% sure as there is still a
// chance of delayed periodic task to be run in the meantime and causing race-condition, which would mean LSR not updated in the DB)
IntStream.range(0, 2).sequential().forEach(index -> inComittedTransaction(index, (session, i) -> {
int timeOffset = 1728000 + (i * 86400);
RealmModel realm = session.realms().getRealm(realmId);
Time.setOffset(timeOffset);
log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime());
UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId());
session0.setLastSessionRefresh(Time.currentTime());
return null;
}));
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
persister = session.getProvider(UserSessionPersisterProvider.class);
// Increase timeOffset - 40 days
Time.setOffset(3456000);
log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime());
// Expire and ensure that all sessions despite session0 were removed
persister.removeExpired(realm);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
persister = session.getProvider(UserSessionPersisterProvider.class);
// assert session0 is the only session found
Assert.assertNotNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId()));
Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[1].getId()));
Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[2].getId()));
Assert.assertEquals(1, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found
Time.setOffset(7000000);
persister.removeExpired(realm);
});
inComittedTransaction(session -> {
RealmModel realm = session.realms().getRealm(realmId);
sessionManager = new UserSessionManager(session);
persister = session.getProvider(UserSessionPersisterProvider.class);
for (String userSessionId : offlineSessions.keySet()) {
Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId));
}
Assert.assertEquals(0, persister.getUserSessionsCount(true));
});
} finally {
Time.setOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
if (timer != null) {
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
}
InfinispanTestUtil.revertTimeService(kcSession);
}
}
private static Set<String> createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel
userSession) {
Set<String> offlineSessions = new HashSet<>();
UserSessionManager localManager = new UserSessionManager(session);
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
localManager.createOrUpdateOfflineSession(clientSession, userSession);
offlineSessions.add(clientSession.getClient().getId());
}
return offlineSessions;
}
}

View file

@ -19,6 +19,11 @@ package org.keycloak.testsuite.model.parameters;
import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
import org.keycloak.models.session.UserSessionPersisterSpi;
import org.keycloak.models.sessions.infinispan.InfinispanUserLoginFailureProviderFactory;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
import org.keycloak.sessions.StickySessionEncoderSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.cache.CacheRealmProviderSpi;
import org.keycloak.models.cache.CacheUserProviderSpi;
@ -28,6 +33,8 @@ import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.testsuite.model.Config;
import com.google.common.collect.ImmutableSet;
import org.keycloak.timer.TimerProviderFactory;
import java.util.Set;
/**
@ -40,6 +47,8 @@ public class Infinispan extends KeycloakModelParameters {
.add(CacheRealmProviderSpi.class)
.add(CacheUserProviderSpi.class)
.add(InfinispanConnectionSpi.class)
.add(StickySessionEncoderSpi.class)
.add(UserSessionPersisterSpi.class)
.build();
@ -48,6 +57,10 @@ public class Infinispan extends KeycloakModelParameters {
.add(InfinispanClusterProviderFactory.class)
.add(InfinispanConnectionProviderFactory.class)
.add(InfinispanUserCacheProviderFactory.class)
.add(InfinispanUserSessionProviderFactory.class)
.add(InfinispanUserLoginFailureProviderFactory.class)
.add(StickySessionEncoderProviderFactory.class)
.add(TimerProviderFactory.class)
.build();
@Override

View file

@ -25,6 +25,8 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory;
import org.keycloak.models.session.UserSessionPersisterSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.dblock.DBLockSpi;
import org.keycloak.models.jpa.JpaClientProviderFactory;
@ -51,6 +53,7 @@ public class Jpa extends KeycloakModelParameters {
.add(JpaConnectionSpi.class)
.add(JpaUpdaterSpi.class)
.add(LiquibaseConnectionSpi.class)
.add(UserSessionPersisterSpi.class)
.build();
@ -68,6 +71,7 @@ public class Jpa extends KeycloakModelParameters {
.add(JpaUserProviderFactory.class)
.add(LiquibaseConnectionProviderFactory.class)
.add(LiquibaseDBLockProviderFactory.class)
.add(JpaUserSessionPersisterProviderFactory.class)
.build();
public Jpa() {

View file

@ -16,10 +16,15 @@
*/
package org.keycloak.testsuite.model.parameters;
import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
import org.keycloak.models.map.group.MapGroupProviderFactory;
import org.keycloak.models.map.realm.MapRealmProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageSpi;
@ -45,9 +50,12 @@ public class Map extends KeycloakModelParameters {
.add(MapClientProviderFactory.class)
.add(MapClientScopeProviderFactory.class)
.add(MapGroupProviderFactory.class)
.add(MapRealmProviderFactory.class)
.add(MapRoleProviderFactory.class)
.add(MapUserProviderFactory.class)
.add(MapStorageProvider.class)
.add(MapUserSessionProviderFactory.class)
.add(MapUserLoginFailureProviderFactory.class)
.build();
public Map() {
@ -59,8 +67,11 @@ public class Map extends KeycloakModelParameters {
cf.spi("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID)
.spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID)
.spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID)
.spi("realm").defaultProvider(MapRealmProviderFactory.PROVIDER_ID)
.spi("role").defaultProvider(MapRoleProviderFactory.PROVIDER_ID)
.spi("user").defaultProvider(MapUserProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).defaultProvider(MapUserSessionProviderFactory.PROVIDER_ID)
.spi(UserLoginFailureSpi.NAME).defaultProvider(MapUserLoginFailureProviderFactory.PROVIDER_ID)
;
}
}

View file

@ -19,7 +19,7 @@ log4j.rootLogger=info, keycloak
log4j.appender.keycloak=org.apache.log4j.ConsoleAppender
log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout
keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n
keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] (%t) %m%n
log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern}
# Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug )
@ -42,4 +42,7 @@ log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase
log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug
# Enable to log short stack traces for log entries enabled with StackUtil.getShortStackTrace() calls
# log4j.logger.org.keycloak.STACK_TRACE=trace
#log4j.logger.org.keycloak.models.map=trace
#log4j.logger.org.keycloak.models.map.transaction=debug
#
#log4j.logger.org.keycloak.STACK_TRACE=trace

View file

@ -38,6 +38,14 @@
"provider": "${keycloak.authSession.provider:infinispan}"
},
"userSessions": {
"provider": "${keycloak.userSession.provider:infinispan}"
},
"loginFailure": {
"provider": "${keycloak.loginFailure.provider:infinispan}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {