Fix and correlate session timeout calculations in legacy and new map implementations
Closes https://github.com/keycloak/keycloak/issues/14854 Closes https://github.com/keycloak/keycloak/issues/11990
This commit is contained in:
parent
8eee3f434b
commit
81aa588ddc
27 changed files with 1531 additions and 327 deletions
|
@ -1166,6 +1166,9 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
StorageId storageId = new StorageId(cached.getId());
|
||||
if (!storageId.isLocal()) {
|
||||
ComponentModel component = realm.getComponent(storageId.getProviderId());
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
ClientStorageProviderModel model = new ClientStorageProviderModel(component);
|
||||
|
||||
// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
|
||||
|
|
|
@ -316,6 +316,9 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
|||
|
||||
if (!storageId.isLocal()) {
|
||||
ComponentModel component = realm.getComponent(storageId.getProviderId());
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
CacheableStorageProviderModel model = new CacheableStorageProviderModel(component);
|
||||
|
||||
// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
|
||||
|
|
|
@ -24,6 +24,7 @@ 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.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakSessionTask;
|
||||
|
@ -49,7 +50,6 @@ import org.keycloak.models.utils.PostMigrationEvent;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
|
||||
|
||||
/**
|
||||
|
@ -142,7 +142,7 @@ public class InfinispanUserLoginFailureProviderFactory implements UserLoginFailu
|
|||
}
|
||||
|
||||
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) {
|
||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
||||
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
|
||||
|
||||
if (remoteStores.isEmpty()) {
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.Retry;
|
||||
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;
|
||||
|
@ -76,7 +75,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
@ -193,8 +191,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache);
|
||||
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
|
||||
entity.setRealmId(realm.getId());
|
||||
entity.setClientId(client.getId());
|
||||
entity.setTimestamp(Time.currentTime());
|
||||
entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp()));
|
||||
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted()));
|
||||
if (userSession.isRememberMe()) {
|
||||
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
|
||||
}
|
||||
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
|
||||
|
@ -287,7 +290,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
return importUserSession(realm, offline, persistentUserSession);
|
||||
UserSessionEntity sessionEntity = importUserSession(realm, offline, persistentUserSession);
|
||||
if (sessionEntity == null) {
|
||||
persister.removeUserSession(sessionId, offline);
|
||||
}
|
||||
|
||||
return sessionEntity;
|
||||
}
|
||||
|
||||
private UserSessionEntity getUserSessionEntityFromCacheOrImportIfNecessary(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) {
|
||||
|
@ -414,7 +422,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
return importClientSession((UserSessionAdapter) userSession, clientSession, getTransaction(offline), getClientSessionTransaction(offline), offline);
|
||||
AuthenticatedClientSessionAdapter clientAdapter = importClientSession((UserSessionAdapter) userSession, clientSession, getTransaction(offline),
|
||||
getClientSessionTransaction(offline), offline, true);
|
||||
|
||||
if (clientAdapter == null) {
|
||||
persister.removeClientSession(userSession.getId(), client.getId(), offline);
|
||||
}
|
||||
return clientAdapter;
|
||||
}
|
||||
|
||||
private AuthenticatedClientSessionEntity getClientSessionEntity(UUID id, boolean offline) {
|
||||
|
@ -815,11 +829,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(true);
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(true);
|
||||
AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true);
|
||||
AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true, false);
|
||||
|
||||
// update timestamp to current time
|
||||
offlineClientSession.setTimestamp(Time.currentTime());
|
||||
offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp()));
|
||||
offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted()));
|
||||
|
||||
session.getProvider(UserSessionPersisterProvider.class).createClientSession(clientSession, true);
|
||||
|
||||
|
@ -863,7 +878,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
for (Map.Entry<String, AuthenticatedClientSessionModel> entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
|
||||
String clientUUID = entry.getKey();
|
||||
AuthenticatedClientSessionModel clientSession = entry.getValue();
|
||||
AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(clientSession, userSessionEntityToImport.getRealmId(), offline);
|
||||
AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(clientSession,
|
||||
userSessionEntityToImport.getRealmId(), clientUUID, offline);
|
||||
|
||||
// Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value
|
||||
clientSessionToImport.setTimestamp(userSessionEntityToImport.getLastSessionRefresh());
|
||||
|
@ -970,14 +986,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
}
|
||||
|
||||
private <T extends SessionEntity> void importSessionsWithExpiration(Map<? extends Object, SessionEntityWrapper<T>> sessionsById,
|
||||
BasicCache cache, BiFunction<RealmModel, T, Long> lifespanMsCalculator,
|
||||
BiFunction<RealmModel, T, Long> maxIdleTimeMsCalculator) {
|
||||
BasicCache cache, SessionFunction<T> lifespanMsCalculator,
|
||||
SessionFunction<T> maxIdleTimeMsCalculator) {
|
||||
sessionsById.forEach((id, sessionEntityWrapper) -> {
|
||||
|
||||
T sessionEntity = sessionEntityWrapper.getEntity();
|
||||
RealmModel currentRealm = session.realms().getRealm(sessionEntity.getRealmId());
|
||||
long lifespan = lifespanMsCalculator.apply(currentRealm, sessionEntity);
|
||||
long maxIdle = maxIdleTimeMsCalculator.apply(currentRealm, sessionEntity);
|
||||
ClientModel client = sessionEntityWrapper.getClientIfNeeded(currentRealm);
|
||||
long lifespan = lifespanMsCalculator.apply(currentRealm, client, sessionEntity);
|
||||
long maxIdle = maxIdleTimeMsCalculator.apply(currentRealm, client, sessionEntity);
|
||||
|
||||
if(lifespan != SessionTimeouts.ENTRY_EXPIRED_FLAG
|
||||
&& maxIdle != SessionTimeouts.ENTRY_EXPIRED_FLAG ) {
|
||||
|
@ -1055,8 +1072,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
|
||||
boolean offline) {
|
||||
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(clientSession, sessionToImportInto.getRealm().getId(), offline);
|
||||
boolean offline, boolean checkExpiration) {
|
||||
AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(clientSession,
|
||||
sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), offline);
|
||||
|
||||
if (checkExpiration) {
|
||||
SessionFunction<AuthenticatedClientSessionEntity> lifespanChecker = offline
|
||||
? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs;
|
||||
SessionFunction<AuthenticatedClientSessionEntity> idleTimeoutChecker = offline
|
||||
? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs;
|
||||
if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG
|
||||
|| lifespanChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final UUID clientSessionId = entity.getId();
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
|
||||
|
@ -1072,10 +1102,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
}
|
||||
|
||||
|
||||
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession, String realmId, boolean offline) {
|
||||
private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession,
|
||||
String realmId, String clientId, boolean offline) {
|
||||
final UUID clientSessionId = keyGenerator.generateKeyUUID(session, getClientSessionCache(offline));
|
||||
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId);
|
||||
entity.setRealmId(realmId);
|
||||
entity.setClientId(clientId);
|
||||
|
||||
entity.setAction(clientSession.getAction());
|
||||
entity.setAuthMethod(clientSession.getProtocol());
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.common.Profile;
|
|||
import org.keycloak.common.util.Environment;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakSessionTask;
|
||||
|
@ -66,7 +67,6 @@ import org.keycloak.provider.ProviderEventListener;
|
|||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiFunction;
|
||||
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
|
||||
|
||||
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory, EnvironmentDependentProviderFactory {
|
||||
|
@ -292,7 +292,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
|||
}
|
||||
|
||||
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) {
|
||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
||||
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
|
||||
|
||||
if (remoteStores.isEmpty()) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2023 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.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
/**
|
||||
* <p>Function definition used for the lifespan and idle calculations for the infinispan
|
||||
* session entities. The method receives the realm, client if needed (it's optional)
|
||||
* and the entity. It returns the timestamp for the entity (lifespan, idle
|
||||
* timeout,...) in milliseconds.</p>
|
||||
*
|
||||
* @param <V> The session entity to apply the function
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SessionFunction<V extends SessionEntity> {
|
||||
|
||||
Long apply(RealmModel realm, ClientModel client, V entity);
|
||||
}
|
|
@ -20,16 +20,17 @@ package org.keycloak.models.sessions.infinispan.changes;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.context.Flag;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.AbstractKeycloakTransaction;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.CacheDecorators;
|
||||
import org.keycloak.models.sessions.infinispan.SessionFunction;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
|
||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||
|
@ -48,11 +49,11 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
|
||||
private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
|
||||
|
||||
private final BiFunction<RealmModel, V, Long> lifespanMsLoader;
|
||||
private final BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
|
||||
private final SessionFunction<V> lifespanMsLoader;
|
||||
private final SessionFunction<V> maxIdleTimeMsLoader;
|
||||
|
||||
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker,
|
||||
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
||||
this.kcSession = kcSession;
|
||||
this.cacheName = cache.getName();
|
||||
this.cache = cache;
|
||||
|
@ -162,8 +163,8 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
|
||||
RealmModel realm = sessionUpdates.getRealm();
|
||||
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionUpdates.getClient(), sessionWrapper.getEntity());
|
||||
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper, lifespanMs, maxIdleTimeMs);
|
||||
|
||||
|
|
|
@ -28,6 +28,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
import org.infinispan.commons.marshall.Externalizer;
|
||||
import org.infinispan.commons.marshall.MarshallUtil;
|
||||
import org.infinispan.commons.marshall.SerializeWith;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import java.util.HashMap;
|
||||
import org.jboss.logging.Logger;
|
||||
|
@ -96,6 +99,16 @@ public class SessionEntityWrapper<S extends SessionEntity> {
|
|||
return entity;
|
||||
}
|
||||
|
||||
public ClientModel getClientIfNeeded(RealmModel realm) {
|
||||
if (entity instanceof AuthenticatedClientSessionEntity) {
|
||||
String clientId = ((AuthenticatedClientSessionEntity) entity).getClientId();
|
||||
if (clientId != null) {
|
||||
return realm.getClientById(clientId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getLocalMetadataNote(String key) {
|
||||
if (isForTransport()) {
|
||||
throw new IllegalStateException("This entity is only intended for transport");
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.changes;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
@ -33,6 +34,8 @@ class SessionUpdatesList<S extends SessionEntity> {
|
|||
|
||||
private final RealmModel realm;
|
||||
|
||||
private final ClientModel client;
|
||||
|
||||
private final SessionEntityWrapper<S> entityWrapper;
|
||||
|
||||
private List<SessionUpdateTask<S>> updateTasks = new LinkedList<>();
|
||||
|
@ -47,12 +50,17 @@ class SessionUpdatesList<S extends SessionEntity> {
|
|||
this.realm = realm;
|
||||
this.entityWrapper = entityWrapper;
|
||||
this.persistenceState = persistenceState;
|
||||
this.client = entityWrapper.getClientIfNeeded(realm);
|
||||
}
|
||||
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public SessionEntityWrapper<S> getEntityWrapper() {
|
||||
return entityWrapper;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.infinispan.commons.marshall.Externalizer;
|
|||
import org.infinispan.commons.marshall.MarshallUtil;
|
||||
import org.infinispan.commons.marshall.SerializeWith;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
|
||||
import java.util.UUID;
|
||||
|
@ -42,6 +43,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
|
|||
|
||||
// Metadata attribute, which contains the last timestamp available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
|
||||
public static final String LAST_TIMESTAMP_REMOTE = "lstr";
|
||||
public static final String CLIENT_ID_NOTE = "clientId";
|
||||
|
||||
private String authMethod;
|
||||
private String redirectUri;
|
||||
|
@ -83,6 +85,28 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
|
|||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getUserSessionStarted() {
|
||||
String started = getNotes().get(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE);
|
||||
return started == null ? timestamp : Integer.parseInt(started);
|
||||
}
|
||||
|
||||
public int getStarted() {
|
||||
String started = getNotes().get(AuthenticatedClientSessionModel.STARTED_AT_NOTE);
|
||||
return started == null ? timestamp : Integer.parseInt(started);
|
||||
}
|
||||
|
||||
public boolean isUserSessionRememberMe() {
|
||||
return Boolean.parseBoolean(getNotes().get(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE));
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return getNotes().get(CLIENT_ID_NOTE);
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
getNotes().put(CLIENT_ID_NOTE, clientId);
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
|
|
@ -31,20 +31,21 @@ import org.infinispan.context.Flag;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.infinispan.TopologyInfo;
|
||||
import org.keycloak.executors.ExecutorsProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.SessionFunction;
|
||||
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.infinispan.client.hotrod.VersionedValue;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -60,8 +61,8 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
private RemoteCache<K, SessionEntityWrapper<V>> remoteCache;
|
||||
private TopologyInfo topologyInfo;
|
||||
private ClientListenerExecutorDecorator<K> executor;
|
||||
private BiFunction<RealmModel, V, Long> lifespanMsLoader;
|
||||
private BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader;
|
||||
private SessionFunction<V> lifespanMsLoader;
|
||||
private SessionFunction<V> maxIdleTimeMsLoader;
|
||||
private KeycloakSessionFactory sessionFactory;
|
||||
|
||||
|
||||
|
@ -70,7 +71,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
|
||||
|
||||
protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
|
||||
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
||||
this.cache = cache;
|
||||
this.remoteCache = remoteCache;
|
||||
|
||||
|
@ -135,8 +136,9 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||
|
||||
RealmModel realm = session.realms().getRealm(newWrapper.getEntity().getRealmId());
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, newWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, newWrapper.getEntity());
|
||||
ClientModel client = newWrapper.getClientIfNeeded(realm);
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, client, newWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, client, newWrapper.getEntity());
|
||||
|
||||
logger.tracef("Calling putIfAbsent for entity '%s' in the cache '%s' . lifespan: %d ms, maxIdleTime: %d ms", key, remoteCache.getName(), lifespanMs, maxIdleTimeMs);
|
||||
|
||||
|
@ -187,8 +189,9 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
KeycloakModelUtils.runJobInTransaction(sessionFactory, (session -> {
|
||||
|
||||
RealmModel realm = session.realms().getRealm(sessionWrapper.getEntity().getRealmId());
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, sessionWrapper.getEntity());
|
||||
ClientModel client = sessionWrapper.getClientIfNeeded(realm);
|
||||
long lifespanMs = lifespanMsLoader.apply(realm, client, sessionWrapper.getEntity());
|
||||
long maxIdleTimeMs = maxIdleTimeMsLoader.apply(realm, client, sessionWrapper.getEntity());
|
||||
|
||||
// We received event from remoteCache, so we won't update it back
|
||||
replaced.set(cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
|
||||
|
@ -253,7 +256,7 @@ public class RemoteCacheSessionListener<K, V extends SessionEntity> {
|
|||
|
||||
|
||||
public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, SessionEntityWrapper<V>> remoteCache,
|
||||
BiFunction<RealmModel, V, Long> lifespanMsLoader, BiFunction<RealmModel, V, Long> maxIdleTimeMsLoader) {
|
||||
SessionFunction<V> lifespanMsLoader, SessionFunction<V> maxIdleTimeMsLoader) {
|
||||
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
|
||||
|
||||
// Just cluster coordinator will fetch userSessions from remote cache.
|
||||
|
|
|
@ -18,12 +18,14 @@
|
|||
|
||||
package org.keycloak.models.sessions.infinispan.util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.models.utils.SessionExpirationUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -45,52 +47,37 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param userSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getUserSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
|
||||
|
||||
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||
if (userSessionEntity.isRememberMe()) {
|
||||
sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespanRememberMe(), sessionMaxLifespan);
|
||||
}
|
||||
|
||||
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (timeToExpire <=0) {
|
||||
public static long getUserSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
|
||||
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(false, userSessionEntity.isRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(userSessionEntity.getStarted()), realm);
|
||||
lifespan = lifespan - Time.currentTimeMillis();
|
||||
if (lifespan <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(timeToExpire);
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the maximum idle time for this userSession.
|
||||
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param userSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getUserSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
|
||||
|
||||
int sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||
if (userSessionEntity.isRememberMe()) {
|
||||
sessionIdleMs = Math.max(realm.getSsoSessionIdleTimeoutRememberMe(), sessionIdleMs);
|
||||
}
|
||||
|
||||
long maxIdleTime = sessionIdleMs - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (maxIdleTime <=0) {
|
||||
public static long getUserSessionMaxIdleMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
|
||||
long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(false, userSessionEntity.isRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(userSessionEntity.getLastSessionRefresh()), realm);
|
||||
idle = idle - Time.currentTimeMillis();
|
||||
if (idle <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(maxIdleTime);
|
||||
return idle;
|
||||
}
|
||||
|
||||
|
||||
|
@ -99,29 +86,19 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param clientSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
|
||||
|
||||
int sessionMaxLifespan = Math.max(realm.getSsoSessionMaxLifespan(), realm.getSsoSessionMaxLifespanRememberMe());
|
||||
|
||||
// clientSession max lifespan has preference if set
|
||||
if (realm.getClientSessionMaxLifespan() > 0) {
|
||||
sessionMaxLifespan = realm.getClientSessionMaxLifespan();
|
||||
}
|
||||
|
||||
sessionMaxLifespan = Math.max(sessionMaxLifespan, MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
long timeToExpire = sessionMaxLifespan - timeSinceTimestampUpdate;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (timeToExpire <=0) {
|
||||
public static long getClientSessionLifespanMs(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||
long lifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, clientSessionEntity.isUserSessionRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(clientSessionEntity.getStarted()), TimeUnit.SECONDS.toMillis(clientSessionEntity.getUserSessionStarted()),
|
||||
realm, client);
|
||||
lifespan = lifespan - Time.currentTimeMillis();
|
||||
if (lifespan <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(timeToExpire);
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
|
||||
|
@ -130,29 +107,18 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param clientSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||
int timeSinceTimestampUpdate = Time.currentTime() - clientSessionEntity.getTimestamp();
|
||||
|
||||
int sessionIdleTimeout = Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe());
|
||||
|
||||
// clientSession idle timeout has preference if set
|
||||
if (realm.getClientSessionIdleTimeout() > 0) {
|
||||
sessionIdleTimeout = realm.getClientSessionIdleTimeout();
|
||||
}
|
||||
|
||||
sessionIdleTimeout = Math.max(sessionIdleTimeout, MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
long timeToExpire = sessionIdleTimeout - timeSinceTimestampUpdate + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (timeToExpire <=0) {
|
||||
public static long getClientSessionMaxIdleMs(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity clientSessionEntity) {
|
||||
long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, clientSessionEntity.isUserSessionRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(clientSessionEntity.getTimestamp()), realm, client);
|
||||
idle = idle - Time.currentTimeMillis();
|
||||
if (idle <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(timeToExpire);
|
||||
return idle;
|
||||
}
|
||||
|
||||
|
||||
|
@ -161,25 +127,21 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param userSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getOfflineSessionLifespanMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||
// By default, this is disabled, so offlineSessions have just "maxIdle"
|
||||
if (!realm.isOfflineSessionMaxLifespanEnabled()) return -1l;
|
||||
|
||||
int timeSinceSessionStart = Time.currentTime() - userSessionEntity.getStarted();
|
||||
|
||||
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
long timeToExpire = sessionMaxLifespan - timeSinceSessionStart;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (timeToExpire <=0) {
|
||||
public static long getOfflineSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
|
||||
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(true, userSessionEntity.isRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(userSessionEntity.getStarted()), realm);
|
||||
if (lifespan == -1L) {
|
||||
return lifespan;
|
||||
}
|
||||
lifespan = lifespan - Time.currentTimeMillis();
|
||||
if (lifespan <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(timeToExpire);
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
|
||||
|
@ -188,22 +150,18 @@ public class SessionTimeouts {
|
|||
* Returned value will be used when as "maxIdleTime" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param userSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getOfflineSessionMaxIdleMs(RealmModel realm, UserSessionEntity userSessionEntity) {
|
||||
int timeSinceLastRefresh = Time.currentTime() - userSessionEntity.getLastSessionRefresh();
|
||||
|
||||
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (maxIdleTime <=0) {
|
||||
public static long getOfflineSessionMaxIdleMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
|
||||
long idle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(true, userSessionEntity.isRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(userSessionEntity.getLastSessionRefresh()), realm);
|
||||
idle = idle - Time.currentTimeMillis();
|
||||
if (idle <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(maxIdleTime);
|
||||
return idle;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,30 +169,22 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "lifespan" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param authenticatedClientSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getOfflineClientSessionLifespanMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||
// By default, this is disabled, so offlineSessions have just "maxIdle"
|
||||
if (!realm.isOfflineSessionMaxLifespanEnabled() && realm.getClientOfflineSessionMaxLifespan() <= 0) return -1l;
|
||||
|
||||
int timeSinceTimestamp = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
|
||||
|
||||
int sessionMaxLifespan = Math.max(realm.getOfflineSessionMaxLifespan(), MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
// clientSession max lifespan has preference if set
|
||||
if (realm.getClientOfflineSessionMaxLifespan() > 0) {
|
||||
sessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
|
||||
public static long getOfflineClientSessionLifespanMs(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||
long lifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, authenticatedClientSessionEntity.isUserSessionRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(authenticatedClientSessionEntity.getStarted()), TimeUnit.SECONDS.toMillis(authenticatedClientSessionEntity.getUserSessionStarted()),
|
||||
realm, client);
|
||||
if (lifespan == -1L) {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
long timeToExpire = sessionMaxLifespan - timeSinceTimestamp;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (timeToExpire <=0) {
|
||||
lifespan = lifespan - Time.currentTimeMillis();
|
||||
if (lifespan <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(timeToExpire);
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -242,27 +192,18 @@ public class SessionTimeouts {
|
|||
* Returned value will be used as "maxIdle" when calling put/replace operation in the infinispan cache for this entity
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param authenticatedClientSessionEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getOfflineClientSessionMaxIdleMs(RealmModel realm, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||
int timeSinceLastRefresh = Time.currentTime() - authenticatedClientSessionEntity.getTimestamp();
|
||||
|
||||
int sessionIdle = Math.max(realm.getOfflineSessionIdleTimeout(), MINIMAL_EXPIRATION_SEC);
|
||||
|
||||
// clientSession idle timeout has preference if set
|
||||
if (realm.getClientOfflineSessionIdleTimeout() > 0) {
|
||||
sessionIdle = realm.getClientOfflineSessionIdleTimeout();
|
||||
}
|
||||
|
||||
long maxIdleTime = sessionIdle - timeSinceLastRefresh + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
|
||||
// Indication that entry should be expired
|
||||
if (maxIdleTime <=0) {
|
||||
public static long getOfflineClientSessionMaxIdleMs(RealmModel realm, ClientModel client, AuthenticatedClientSessionEntity authenticatedClientSessionEntity) {
|
||||
long idle = SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, authenticatedClientSessionEntity.isUserSessionRememberMe(),
|
||||
TimeUnit.SECONDS.toMillis(authenticatedClientSessionEntity.getTimestamp()), realm, client);
|
||||
idle = idle - Time.currentTimeMillis();
|
||||
if (idle <= 0) {
|
||||
return ENTRY_EXPIRED_FLAG;
|
||||
}
|
||||
|
||||
return Time.toMillis(maxIdleTime);
|
||||
return idle;
|
||||
}
|
||||
|
||||
|
||||
|
@ -270,10 +211,11 @@ public class SessionTimeouts {
|
|||
* Not using lifespan for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param loginFailureEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getLoginFailuresLifespanMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
|
||||
public static long getLoginFailuresLifespanMs(RealmModel realm, ClientModel client, LoginFailureEntity loginFailureEntity) {
|
||||
return -1l;
|
||||
}
|
||||
|
||||
|
@ -282,12 +224,11 @@ public class SessionTimeouts {
|
|||
* Not using maxIdle for detached login failure (backwards compatibility with the background cleaner threads, which were used for cleanup of detached login failures)
|
||||
*
|
||||
* @param realm
|
||||
* @param client
|
||||
* @param loginFailureEntity
|
||||
* @return
|
||||
*/
|
||||
public static long getLoginFailuresMaxIdleMs(RealmModel realm, LoginFailureEntity loginFailureEntity) {
|
||||
public static long getLoginFailuresMaxIdleMs(RealmModel realm, ClientModel client, LoginFailureEntity loginFailureEntity) {
|
||||
return -1l;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -234,7 +234,9 @@ public class HotRodCrudOperations<K, E extends AbstractHotRodEntity, V extends A
|
|||
|
||||
// workaround if the query contains us.clientId field, in which case don't read by id => read without optimistic locking.
|
||||
// See https://issues.redhat.com/browse/ISPN-14537
|
||||
if (!dmc.isEmpty() && dmc.partiallyEvaluate((field, op, arg) -> field == UserSessionModel.SearchableFields.CLIENT_ID).toString().contains("__TRUE__")) {
|
||||
if (!dmc.isEmpty() && dmc.partiallyEvaluate((field, op, arg) ->
|
||||
field == UserSessionModel.SearchableFields.CLIENT_ID || field == UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID
|
||||
).toString().contains("__TRUE__")) {
|
||||
Query<E> query = prepareQueryWithPrefixAndParameters(null, queryParameters);
|
||||
CloseableIterator<E> iterator = paginateQuery(query, queryParameters.getOffset(),
|
||||
queryParameters.getLimit()).iterator();
|
||||
|
|
|
@ -124,11 +124,14 @@ public class MapUserSessionProvider implements UserSessionProvider {
|
|||
}
|
||||
|
||||
MapAuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionEntityInstance(null, userSession.getId(),
|
||||
realm.getId(), client.getId(), false);
|
||||
realm.getId(), client.getId(), userSession.isOffline());
|
||||
String started = entity.getTimestamp() != null ? String.valueOf(TimeAdapter.fromMilliSecondsToSeconds(entity.getTimestamp())) : String.valueOf(0);
|
||||
entity.setNote(AuthenticatedClientSessionModel.STARTED_AT_NOTE, started);
|
||||
entity.setNote(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted()));
|
||||
if (userSession.isRememberMe()) {
|
||||
entity.setNote(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
|
||||
}
|
||||
setClientSessionExpiration(entity, realm, client);
|
||||
|
||||
userSessionEntity.addAuthenticatedClientSession(entity);
|
||||
|
||||
// We need to load the clientSession through userModel so we return an entity that is included within the
|
||||
|
@ -427,6 +430,7 @@ public class MapUserSessionProvider implements UserSessionProvider {
|
|||
MapAuthenticatedClientSessionEntity clientSessionEntity = createAuthenticatedClientSessionInstance(clientSession, offlineUserSession, true);
|
||||
int currentTime = Time.currentTime();
|
||||
clientSessionEntity.setNote(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(currentTime));
|
||||
clientSessionEntity.setNote(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted()));
|
||||
clientSessionEntity.setTimestamp(Time.currentTimeMillis());
|
||||
RealmModel realm = clientSession.getRealm();
|
||||
setClientSessionExpiration(clientSessionEntity, realm, clientSession.getClient());
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
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.RealmModel;
|
||||
import org.keycloak.models.map.common.TimeAdapter;
|
||||
import org.keycloak.models.utils.SessionExpirationUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
||||
/**
|
||||
|
@ -27,131 +29,46 @@ import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
|||
*/
|
||||
public class SessionExpiration {
|
||||
|
||||
private static long getTimestampNote(MapAuthenticatedClientSessionEntity entity, String name) {
|
||||
String value = entity.getNote(name);
|
||||
if (value == null) {
|
||||
// return timestamp if not found
|
||||
return entity.getTimestamp();
|
||||
}
|
||||
return TimeAdapter.fromSecondsToMilliseconds(Integer.parseInt(value));
|
||||
}
|
||||
|
||||
public static void setClientSessionExpiration(MapAuthenticatedClientSessionEntity entity, RealmModel realm, ClientModel client) {
|
||||
long timestampMillis = entity.getTimestamp() != null ? entity.getTimestamp() : 0L;
|
||||
if (Boolean.TRUE.equals(entity.isOffline())) {
|
||||
long sessionExpires = timestampMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionIdleTimeout());
|
||||
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
||||
sessionExpires = timestampMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionMaxLifespan());
|
||||
long clientSessionStartedAtMillis = getTimestampNote(entity, AuthenticatedClientSessionModel.STARTED_AT_NOTE);
|
||||
long userSessionStartedAtMillis = getTimestampNote(entity, AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE);
|
||||
boolean isRememberMe = Boolean.parseBoolean(entity.getNote(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE));
|
||||
boolean isOffline = Boolean.TRUE.equals(entity.isOffline());
|
||||
|
||||
long clientOfflineSessionMaxLifespan;
|
||||
String clientOfflineSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||
if (clientOfflineSessionMaxLifespanPerClient != null && !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientOfflineSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(Long.parseLong(clientOfflineSessionMaxLifespanPerClient));
|
||||
} else {
|
||||
clientOfflineSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(realm.getClientOfflineSessionMaxLifespan());
|
||||
}
|
||||
long expiresbyLifespan = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(isOffline, isRememberMe,
|
||||
clientSessionStartedAtMillis, userSessionStartedAtMillis, realm, client);
|
||||
long expiresByIdle =SessionExpirationUtils.calculateClientSessionIdleTimestamp(isOffline, isRememberMe, timestampMillis, realm, client);
|
||||
|
||||
if (clientOfflineSessionMaxLifespan > 0) {
|
||||
long clientOfflineSessionMaxExpiration = timestampMillis + clientOfflineSessionMaxLifespan;
|
||||
sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration);
|
||||
}
|
||||
}
|
||||
|
||||
long expiration = timestampMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionIdleTimeout());
|
||||
|
||||
long clientOfflineSessionIdleTimeout;
|
||||
String clientOfflineSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
if (clientOfflineSessionIdleTimeoutPerClient != null && !clientOfflineSessionIdleTimeoutPerClient.trim().isEmpty()) {
|
||||
clientOfflineSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(Long.parseLong(clientOfflineSessionIdleTimeoutPerClient));
|
||||
} else {
|
||||
clientOfflineSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(realm.getClientOfflineSessionIdleTimeout());
|
||||
}
|
||||
|
||||
if (clientOfflineSessionIdleTimeout > 0) {
|
||||
long clientOfflineSessionIdleExpiration = timestampMillis + clientOfflineSessionIdleTimeout;
|
||||
expiration = Math.min(expiration, clientOfflineSessionIdleExpiration);
|
||||
}
|
||||
|
||||
entity.setExpiration(Math.min(expiration, sessionExpires));
|
||||
if (expiresbyLifespan > 0) {
|
||||
entity.setExpiration(Math.min(expiresbyLifespan, expiresByIdle));
|
||||
} else {
|
||||
long sessionExpires = timestampMillis + (realm.getSsoSessionMaxLifespanRememberMe() > 0
|
||||
? TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionMaxLifespanRememberMe()) : TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionMaxLifespan()));
|
||||
|
||||
long clientSessionMaxLifespan;
|
||||
String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
|
||||
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(Long.parseLong(clientSessionMaxLifespanPerClient));
|
||||
} else {
|
||||
clientSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(realm.getClientSessionMaxLifespan());
|
||||
}
|
||||
|
||||
if (clientSessionMaxLifespan > 0) {
|
||||
long clientSessionMaxExpiration = timestampMillis + clientSessionMaxLifespan;
|
||||
sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration);
|
||||
}
|
||||
|
||||
long expiration = timestampMillis + (realm.getSsoSessionIdleTimeoutRememberMe() > 0
|
||||
? TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionIdleTimeoutRememberMe()) : TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionIdleTimeout()));
|
||||
|
||||
long clientSessionIdleTimeout;
|
||||
String clientSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT);
|
||||
if (clientSessionIdleTimeoutPerClient != null && !clientSessionIdleTimeoutPerClient.trim().isEmpty()) {
|
||||
clientSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(Long.parseLong(clientSessionIdleTimeoutPerClient));
|
||||
} else {
|
||||
clientSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(realm.getClientSessionIdleTimeout());
|
||||
}
|
||||
|
||||
if (clientSessionIdleTimeout > 0) {
|
||||
long clientSessionIdleExpiration = timestampMillis + clientSessionIdleTimeout;
|
||||
expiration = Math.min(expiration, clientSessionIdleExpiration);
|
||||
}
|
||||
|
||||
entity.setExpiration(Math.min(expiration, sessionExpires));
|
||||
entity.setExpiration(expiresByIdle);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setUserSessionExpiration(MapUserSessionEntity entity, RealmModel realm) {
|
||||
long timestampMillis = entity.getTimestamp() != null ? entity.getTimestamp() : 0L;
|
||||
long lastSessionRefreshMillis = entity.getLastSessionRefresh() != null ? entity.getLastSessionRefresh() : 0L;
|
||||
if (Boolean.TRUE.equals(entity.isOffline())) {
|
||||
long sessionExpires = lastSessionRefreshMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionIdleTimeout());
|
||||
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
||||
sessionExpires = timestampMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionMaxLifespan());
|
||||
boolean isRememberMe = Boolean.TRUE.equals(entity.isRememberMe());
|
||||
boolean isOffline = Boolean.TRUE.equals(entity.isOffline());
|
||||
|
||||
long clientOfflineSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(realm.getClientOfflineSessionMaxLifespan());
|
||||
long expiresByLifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(isOffline, isRememberMe, timestampMillis, realm);
|
||||
long expiresByIdle = SessionExpirationUtils.calculateUserSessionIdleTimestamp(isOffline, isRememberMe, lastSessionRefreshMillis, realm);
|
||||
|
||||
if (clientOfflineSessionMaxLifespan > 0) {
|
||||
long clientOfflineSessionMaxExpiration = timestampMillis + clientOfflineSessionMaxLifespan;
|
||||
sessionExpires = Math.min(sessionExpires, clientOfflineSessionMaxExpiration);
|
||||
}
|
||||
}
|
||||
|
||||
long expiration = lastSessionRefreshMillis + TimeAdapter.fromSecondsToMilliseconds(realm.getOfflineSessionIdleTimeout());
|
||||
|
||||
long clientOfflineSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(realm.getClientOfflineSessionIdleTimeout());
|
||||
|
||||
if (clientOfflineSessionIdleTimeout > 0) {
|
||||
long clientOfflineSessionIdleExpiration = Time.currentTimeMillis() + clientOfflineSessionIdleTimeout;
|
||||
expiration = Math.min(expiration, clientOfflineSessionIdleExpiration);
|
||||
}
|
||||
|
||||
entity.setExpiration(Math.min(expiration, sessionExpires));
|
||||
if (expiresByLifespan > 0) {
|
||||
entity.setExpiration(Math.min(expiresByLifespan, expiresByIdle));
|
||||
} else {
|
||||
long sessionExpires = timestampMillis
|
||||
+ (Boolean.TRUE.equals(entity.isRememberMe()) && realm.getSsoSessionMaxLifespanRememberMe() > 0
|
||||
? TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionMaxLifespanRememberMe())
|
||||
: TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionMaxLifespan()));
|
||||
|
||||
long clientSessionMaxLifespan = TimeAdapter.fromSecondsToMilliseconds(realm.getClientSessionMaxLifespan());
|
||||
|
||||
if (clientSessionMaxLifespan > 0) {
|
||||
long clientSessionMaxExpiration = timestampMillis + clientSessionMaxLifespan;
|
||||
sessionExpires = Math.min(sessionExpires, clientSessionMaxExpiration);
|
||||
}
|
||||
|
||||
long expiration = lastSessionRefreshMillis + (Boolean.TRUE.equals(entity.isRememberMe()) && realm.getSsoSessionIdleTimeoutRememberMe() > 0
|
||||
? TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionIdleTimeoutRememberMe())
|
||||
: TimeAdapter.fromSecondsToMilliseconds(realm.getSsoSessionIdleTimeout()));
|
||||
|
||||
long clientSessionIdleTimeout = TimeAdapter.fromSecondsToMilliseconds(realm.getClientSessionIdleTimeout());
|
||||
|
||||
if (clientSessionIdleTimeout > 0) {
|
||||
long clientSessionIdleExpiration = lastSessionRefreshMillis + clientSessionIdleTimeout;
|
||||
expiration = Math.min(expiration, clientSessionIdleExpiration);
|
||||
}
|
||||
|
||||
entity.setExpiration(Math.min(expiration, sessionExpires));
|
||||
entity.setExpiration(expiresByIdle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,9 @@ public final class Constants {
|
|||
public static final int DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN = 5184000;
|
||||
public static final String DEFAULT_SIGNATURE_ALGORITHM = Algorithm.RS256;
|
||||
|
||||
public static final int DEFAULT_SESSION_IDLE_TIMEOUT = 1800; // 30 minutes
|
||||
public static final int DEFAULT_SESSION_MAX_LIFESPAN = 36000; // 10 hours
|
||||
|
||||
public static final String DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = Algorithm.ES256;
|
||||
public static final String DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME = "keycloak";
|
||||
// it stands for optional parameter not specified in WebAuthn
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright 2023 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.utils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
||||
/**
|
||||
* <p>Shared methods to calculate the session expiration and idle.</p>
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SessionExpirationUtils {
|
||||
|
||||
/**
|
||||
* Calculates the time in which the session is expired via max lifetime
|
||||
* configuration.
|
||||
* @param offline is the session offline?
|
||||
* @param isRememberMe is the session remember me?
|
||||
* @param created timestamp when the session was created
|
||||
* @param realm The realm model
|
||||
* @return The time when the user session is expired or -1 if does not expire
|
||||
*/
|
||||
public static long calculateUserSessionMaxLifespanTimestamp(boolean offline, boolean isRememberMe, long created, RealmModel realm) {
|
||||
long timestamp = -1;
|
||||
if (offline) {
|
||||
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
||||
timestamp = created + TimeUnit.SECONDS.toMillis(getOfflineSessionMaxLifespan(realm));
|
||||
}
|
||||
} else {
|
||||
long userSessionMaxLifespan = TimeUnit.SECONDS.toMillis(getSsoSessionMaxLifespan(realm));
|
||||
if (isRememberMe) {
|
||||
userSessionMaxLifespan = Math.max(userSessionMaxLifespan, TimeUnit.SECONDS.toMillis(realm.getSsoSessionMaxLifespanRememberMe()));
|
||||
}
|
||||
timestamp = created + userSessionMaxLifespan;
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time in which the user session is expired via the idle
|
||||
* configuration.
|
||||
* @param offline is the session offline?
|
||||
* @param isRememberMe is the session remember me?
|
||||
* @param lastRefreshed The last time the session was refreshed
|
||||
* @param realm The realm model
|
||||
* @return The time in which the user session is expired by idle timeout
|
||||
*/
|
||||
public static long calculateUserSessionIdleTimestamp(boolean offline, boolean isRememberMe, long lastRefreshed, RealmModel realm) {
|
||||
long timestamp;
|
||||
if (offline) {
|
||||
timestamp = lastRefreshed + TimeUnit.SECONDS.toMillis(getOfflineSessionIdleTimeout(realm));
|
||||
} else {
|
||||
long userSessionIdleTimeout = TimeUnit.SECONDS.toMillis(getSsoSessionIdleTimeout(realm));
|
||||
if (isRememberMe) {
|
||||
userSessionIdleTimeout = Math.max(userSessionIdleTimeout, TimeUnit.SECONDS.toMillis(realm.getSsoSessionIdleTimeoutRememberMe()));
|
||||
}
|
||||
timestamp = lastRefreshed + userSessionIdleTimeout;
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time in which the client session is expired via lifespan
|
||||
* configuration in the realm and client.
|
||||
* @param offline is the session offline?
|
||||
* @param isRememberMe is the session remember me?
|
||||
* @param clientSessionCreated timestamp when the client session was created
|
||||
* @param userSessionCreated timestamp when the user session was created
|
||||
* @param realm The realm model
|
||||
* @param client The client model
|
||||
* @return The time when the client session is expired or -1 if does not expire
|
||||
*/
|
||||
public static long calculateClientSessionMaxLifespanTimestamp(boolean offline, boolean isRememberMe,
|
||||
long clientSessionCreated, long userSessionCreated, RealmModel realm, ClientModel client) {
|
||||
long timestamp = -1;
|
||||
if (offline) {
|
||||
if (realm.isOfflineSessionMaxLifespanEnabled()) {
|
||||
long clientOfflineSessionMaxLifespan = TimeUnit.SECONDS.toMillis(getOfflineSessionMaxLifespan(realm));
|
||||
|
||||
String clientOfflineSessionMaxLifespanPerClient = client == null? null : client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||
if (clientOfflineSessionMaxLifespanPerClient != null && !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientOfflineSessionMaxLifespan = TimeUnit.SECONDS.toMillis(Long.parseLong(clientOfflineSessionMaxLifespanPerClient));
|
||||
} else if (realm.getClientOfflineSessionMaxLifespan() > 0) {
|
||||
clientOfflineSessionMaxLifespan = TimeUnit.SECONDS.toMillis(realm.getClientOfflineSessionMaxLifespan());
|
||||
}
|
||||
|
||||
timestamp = clientSessionCreated + clientOfflineSessionMaxLifespan;
|
||||
|
||||
long userSessionExpires = calculateUserSessionMaxLifespanTimestamp(offline, isRememberMe, userSessionCreated, realm);
|
||||
|
||||
timestamp = Math.min(timestamp, userSessionExpires);
|
||||
}
|
||||
} else {
|
||||
long clientSessionMaxLifespan = TimeUnit.SECONDS.toMillis(getSsoSessionMaxLifespan(realm));
|
||||
if (isRememberMe) {
|
||||
clientSessionMaxLifespan = Math.max(clientSessionMaxLifespan, TimeUnit.SECONDS.toMillis(realm.getSsoSessionMaxLifespanRememberMe()));
|
||||
}
|
||||
String clientSessionMaxLifespanPerClient = client == null? null : client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
|
||||
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientSessionMaxLifespan = TimeUnit.SECONDS.toMillis(Long.parseLong(clientSessionMaxLifespanPerClient));
|
||||
} else if (realm.getClientSessionMaxLifespan() > 0) {
|
||||
clientSessionMaxLifespan = TimeUnit.SECONDS.toMillis(realm.getClientSessionMaxLifespan());
|
||||
}
|
||||
|
||||
timestamp = clientSessionCreated + clientSessionMaxLifespan;
|
||||
|
||||
long userSessionExpires = calculateUserSessionMaxLifespanTimestamp(offline, isRememberMe, userSessionCreated, realm);
|
||||
|
||||
timestamp = Math.min(timestamp, userSessionExpires);
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time in which the user session is expired via the idle
|
||||
* configuration in the realm and client.
|
||||
* @param offline is the session offline?
|
||||
* @param isRememberMe is the session remember me?
|
||||
* @param lastRefreshed the last time the client session was refreshed
|
||||
* @param realm the realm model
|
||||
* @param client the client model
|
||||
* @return The time in which the client session is expired by idle timeout
|
||||
*/
|
||||
public static long calculateClientSessionIdleTimestamp(boolean offline, boolean isRememberMe, long lastRefreshed,
|
||||
RealmModel realm, ClientModel client) {
|
||||
long timestamp;
|
||||
if (offline) {
|
||||
long clientOfflineSessionIdleTimeout = TimeUnit.SECONDS.toMillis(getOfflineSessionIdleTimeout(realm));
|
||||
String clientOfflineSessionIdleTimeoutPerClient = client == null? null : client.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
if (clientOfflineSessionIdleTimeoutPerClient != null && !clientOfflineSessionIdleTimeoutPerClient.trim().isEmpty()) {
|
||||
clientOfflineSessionIdleTimeout = TimeUnit.SECONDS.toMillis(Long.parseLong(clientOfflineSessionIdleTimeoutPerClient));
|
||||
} else if (realm.getClientOfflineSessionIdleTimeout() > 0) {
|
||||
clientOfflineSessionIdleTimeout = TimeUnit.SECONDS.toMillis(realm.getClientOfflineSessionIdleTimeout());
|
||||
}
|
||||
|
||||
timestamp = lastRefreshed + clientOfflineSessionIdleTimeout;
|
||||
} else {
|
||||
long clientSessionIdleTimeout = TimeUnit.SECONDS.toMillis(getSsoSessionIdleTimeout(realm));
|
||||
if (isRememberMe) {
|
||||
clientSessionIdleTimeout = Math.max(clientSessionIdleTimeout, TimeUnit.SECONDS.toMillis(realm.getSsoSessionIdleTimeoutRememberMe()));
|
||||
}
|
||||
String clientSessionIdleTimeoutPerClient = client == null? null : client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT);
|
||||
if (clientSessionIdleTimeoutPerClient != null && !clientSessionIdleTimeoutPerClient.trim().isEmpty()) {
|
||||
clientSessionIdleTimeout = TimeUnit.SECONDS.toMillis(Long.parseLong(clientSessionIdleTimeoutPerClient));
|
||||
} else if (realm.getClientSessionIdleTimeout() > 0){
|
||||
clientSessionIdleTimeout = TimeUnit.SECONDS.toMillis(realm.getClientSessionIdleTimeout());
|
||||
}
|
||||
|
||||
timestamp = lastRefreshed + clientSessionIdleTimeout;
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
private static int getSsoSessionMaxLifespan(RealmModel realm) {
|
||||
int lifespan = realm.getSsoSessionMaxLifespan();
|
||||
if (lifespan <= 0) {
|
||||
lifespan = Constants.DEFAULT_SESSION_MAX_LIFESPAN;
|
||||
}
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
private static int getOfflineSessionMaxLifespan(RealmModel realm) {
|
||||
int lifespan = realm.getOfflineSessionMaxLifespan();
|
||||
if (lifespan <= 0) {
|
||||
lifespan = Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN;
|
||||
}
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
private static int getSsoSessionIdleTimeout(RealmModel realm) {
|
||||
int idle = realm.getSsoSessionIdleTimeout();
|
||||
if (idle <= 0) {
|
||||
idle = Constants.DEFAULT_SESSION_IDLE_TIMEOUT;
|
||||
}
|
||||
return idle;
|
||||
}
|
||||
|
||||
private static int getOfflineSessionIdleTimeout(RealmModel realm) {
|
||||
int idle = realm.getOfflineSessionIdleTimeout();
|
||||
if (idle <= 0) {
|
||||
idle = Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT;
|
||||
}
|
||||
return idle;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* Copyright 2023 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.utils;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
||||
/**
|
||||
* <p>Class to perform unit tests of the SessionExpirationUtils class.</p>
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SessionExpirationUtilsTest {
|
||||
|
||||
private static final Map<String, Object> realmMap = new HashMap<>();
|
||||
private static final Map<String, Object> clientMap = new HashMap<>();
|
||||
private static final RealmModel realm = createRealm();
|
||||
private static final ClientModel client = createClient();
|
||||
|
||||
private static RealmModel createRealm() {
|
||||
RealmModel realmModel = (RealmModel) Proxy.newProxyInstance(SessionExpirationUtilsTest.class.getClassLoader(),
|
||||
new Class[]{RealmModel.class}, (proxy, method, args) -> {
|
||||
|
||||
Object result = realmMap.get(method.getName());
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
throw new UnsupportedOperationException("Realm method not in map: " + method.getName());
|
||||
});
|
||||
return realmModel;
|
||||
}
|
||||
|
||||
private static ClientModel createClient() {
|
||||
ClientModel clientModel = (ClientModel) Proxy.newProxyInstance(SessionExpirationUtilsTest.class.getClassLoader(),
|
||||
new Class[]{ClientModel.class}, (proxy, method, args) -> {
|
||||
|
||||
if ("getAttribute".equals(method.getName()) && args.length == 1) {
|
||||
return clientMap.get((String) args[0]);
|
||||
}
|
||||
throw new UnsupportedOperationException("Client method not in map: " + method.getName());
|
||||
});
|
||||
return clientModel;
|
||||
}
|
||||
|
||||
private static void resetRealm() {
|
||||
realmMap.put("isOfflineSessionMaxLifespanEnabled", false);
|
||||
realmMap.put("getOfflineSessionMaxLifespan", 0);
|
||||
realmMap.put("getOfflineSessionIdleTimeout", 0);
|
||||
realmMap.put("getClientOfflineSessionMaxLifespan", 0);
|
||||
realmMap.put("getClientOfflineSessionIdleTimeout", 0);
|
||||
realmMap.put("getSsoSessionMaxLifespan", 0);
|
||||
realmMap.put("getSsoSessionIdleTimeout", 0);
|
||||
realmMap.put("getClientSessionMaxLifespan", 0);
|
||||
realmMap.put("getClientSessionIdleTimeout", 0);
|
||||
realmMap.put("getSsoSessionMaxLifespanRememberMe", 0);
|
||||
realmMap.put("getSsoSessionIdleTimeoutRememberMe", 0);
|
||||
}
|
||||
|
||||
private static void resetClient() {
|
||||
clientMap.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateUserSessionMaxLifespanTimestampOnline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
|
||||
// non valid lifespan 0 or negative is default 36000
|
||||
Assert.assertEquals(Constants.DEFAULT_SESSION_MAX_LIFESPAN * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(false, false, t, realm) - t);
|
||||
// normal lifespan to 1000s
|
||||
realmMap.put("getSsoSessionMaxLifespan", 1000);
|
||||
Assert.assertEquals(1000 * 1000L, SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(false, false, t, realm) - t);
|
||||
// use remember me
|
||||
realmMap.put("getSsoSessionMaxLifespanRememberMe", 2000);
|
||||
Assert.assertEquals(2000 * 1000L, SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(false, true, t, realm) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateUserSessionMaxLifespanTimestampOffline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
|
||||
// not activated expiration for offline
|
||||
Assert.assertEquals(-1L, SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(true, false, t, realm));
|
||||
// activate and 0 should be default
|
||||
realmMap.put("isOfflineSessionMaxLifespanEnabled", true);
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(true, false, t, realm) - t);
|
||||
// normal lifespan 2000
|
||||
realmMap.put("getOfflineSessionMaxLifespan", 2000);
|
||||
Assert.assertEquals(2000 * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(true, false, t, realm) - t);
|
||||
// remember me does not affect offline
|
||||
realmMap.put("getSsoSessionMaxLifespanRememberMe", 4000);
|
||||
Assert.assertEquals(2000 * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(true, true, t, realm) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateUserSessionIdleTimestampOnline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
|
||||
// non valid, default value
|
||||
Assert.assertEquals(Constants.DEFAULT_SESSION_IDLE_TIMEOUT * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionIdleTimestamp(false, false, t, realm) - t);
|
||||
// normal value 2000s
|
||||
realmMap.put("getSsoSessionIdleTimeout", 1000);
|
||||
Assert.assertEquals(1000 * 1000L, SessionExpirationUtils.calculateUserSessionIdleTimestamp(false, false, t, realm) - t);
|
||||
// use bigger remember me
|
||||
realmMap.put("getSsoSessionIdleTimeoutRememberMe", 2000);
|
||||
Assert.assertEquals(2000 * 1000L, SessionExpirationUtils.calculateUserSessionIdleTimestamp(false, true, t, realm) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateUserSessionIdleTimestampOffline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
|
||||
// non valid, default value
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT * 1000L,
|
||||
SessionExpirationUtils.calculateUserSessionIdleTimestamp(true, false, t, realm) - t);
|
||||
// normal value 2000s
|
||||
realmMap.put("getOfflineSessionIdleTimeout", 1000);
|
||||
Assert.assertEquals(1000 * 1000L, SessionExpirationUtils.calculateUserSessionIdleTimestamp(true, false, t, realm) - t);
|
||||
// use bigger remember me does not affect
|
||||
realmMap.put("getSsoSessionIdleTimeoutRememberMe", 2000);
|
||||
Assert.assertEquals(1000 * 1000L, SessionExpirationUtils.calculateUserSessionIdleTimestamp(true, true, t, realm) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateClientSessionMaxLifespanTimestampOnline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
resetClient();
|
||||
|
||||
// default
|
||||
Assert.assertEquals(Constants.DEFAULT_SESSION_MAX_LIFESPAN * 1000L,
|
||||
SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, false, t, t, realm, client) - t);
|
||||
// normal value in realm
|
||||
realmMap.put("getSsoSessionMaxLifespan", 5000);
|
||||
Assert.assertEquals(5000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, false, t, t, realm, client) - t);
|
||||
// use remember me
|
||||
realmMap.put("getSsoSessionMaxLifespanRememberMe", 6000);
|
||||
Assert.assertEquals(6000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, true, t, t, realm, client) - t);
|
||||
// override client value in realm
|
||||
realmMap.put("getClientSessionMaxLifespan", 4000);
|
||||
Assert.assertEquals(4000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, false, t, t, realm, client) - t);
|
||||
// override value in client
|
||||
clientMap.put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "3000");
|
||||
Assert.assertEquals(3000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, false, t, t, realm, client) - t);
|
||||
// client max lifespan cannot be bigger than user lifespan
|
||||
realmMap.put("getSsoSessionMaxLifespan", 2000);
|
||||
Assert.assertEquals(2000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, false, t, t, realm, client) - t);
|
||||
// the same but using remember me
|
||||
realmMap.put("getSsoSessionMaxLifespan", 1000);
|
||||
realmMap.put("getSsoSessionMaxLifespanRememberMe", 2000);
|
||||
Assert.assertEquals(2000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(false, true, t, t, realm, client) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateClientSessionMaxLifespanTimestampOffline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
resetClient();
|
||||
|
||||
// no expiration
|
||||
Assert.assertEquals(-1, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t, realm, client));
|
||||
// default
|
||||
realmMap.put("isOfflineSessionMaxLifespanEnabled", true);
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN * 1000L,
|
||||
SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t, realm, client) - t);
|
||||
// normal value in realm
|
||||
realmMap.put("getOfflineSessionMaxLifespan", 5000);
|
||||
Assert.assertEquals(5000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t, realm, client) - t);
|
||||
// override client value in realm
|
||||
realmMap.put("getClientOfflineSessionMaxLifespan", 4000);
|
||||
Assert.assertEquals(4000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t, realm, client) - t);
|
||||
// override value in client
|
||||
clientMap.put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, "3000");
|
||||
Assert.assertEquals(3000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t, realm, client) - t);
|
||||
// client max lifespan cannot be bigger than user lifespan
|
||||
long t2 = t - 100;
|
||||
realmMap.put("getOfflineSessionMaxLifespan", 2000);
|
||||
Assert.assertEquals(2000 * 1000L, SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp(true, false, t, t2, realm, client) - t2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateClientSessionIdleTimestampOnline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
resetClient();
|
||||
|
||||
// default
|
||||
Assert.assertEquals(Constants.DEFAULT_SESSION_IDLE_TIMEOUT * 1000L,
|
||||
SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, false, t, realm, client) - t);
|
||||
// normal value in realm
|
||||
realmMap.put("getSsoSessionIdleTimeout", 5000);
|
||||
Assert.assertEquals(5000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, false, t, realm, client) - t);
|
||||
// use remember me
|
||||
realmMap.put("getSsoSessionIdleTimeoutRememberMe", 6000);
|
||||
Assert.assertEquals(6000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, true, t, realm, client) - t);
|
||||
// override client value in realm
|
||||
realmMap.put("getClientSessionIdleTimeout", 4000);
|
||||
Assert.assertEquals(4000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, false, t, realm, client) - t);
|
||||
// override value in client
|
||||
clientMap.put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "3000");
|
||||
Assert.assertEquals(3000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(false, false, t, realm, client) - t);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateClientSessionIdleTimestampOffline() {
|
||||
long t = Time.currentTimeMillis();
|
||||
resetRealm();
|
||||
resetClient();
|
||||
|
||||
// default
|
||||
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT * 1000L,
|
||||
SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, false, t, realm, client) - t);
|
||||
// normal value in realm
|
||||
realmMap.put("getOfflineSessionIdleTimeout", 5000);
|
||||
Assert.assertEquals(5000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, false, t, realm, client) - t);
|
||||
// use remember me does not affect
|
||||
realmMap.put("getSsoSessionIdleTimeoutRememberMe", 6000);
|
||||
Assert.assertEquals(5000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, true, t, realm, client) - t);
|
||||
// override client value in realm
|
||||
realmMap.put("getClientOfflineSessionIdleTimeout", 4000);
|
||||
Assert.assertEquals(4000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, false, t, realm, client) - t);
|
||||
// override value in client
|
||||
clientMap.put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT, "3000");
|
||||
Assert.assertEquals(3000 * 1000L, SessionExpirationUtils.calculateClientSessionIdleTimestamp(true, false, t, realm, client) - t);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,9 @@ import org.keycloak.sessions.CommonClientSessionModel;
|
|||
*/
|
||||
public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
|
||||
|
||||
String STARTED_AT_NOTE = "startedAt";
|
||||
final String STARTED_AT_NOTE = "startedAt";
|
||||
final String USER_SESSION_STARTED_AT_NOTE = "userSessionStartedAt";
|
||||
final String USER_SESSION_REMEMBER_ME_NOTE = "userSessionRememberMe";
|
||||
|
||||
String getId();
|
||||
|
||||
|
@ -37,6 +39,15 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
|
|||
return started == null ? 0 : Integer.parseInt(started);
|
||||
}
|
||||
|
||||
default int getUserSessionStarted() {
|
||||
String started = getNote(USER_SESSION_STARTED_AT_NOTE);
|
||||
return started == null ? getUserSession().getStarted() : Integer.parseInt(started);
|
||||
}
|
||||
|
||||
default boolean isUserSessionRememberMe() {
|
||||
return Boolean.parseBoolean(getNote(USER_SESSION_REMEMBER_ME_NOTE));
|
||||
}
|
||||
|
||||
int getTimestamp();
|
||||
void setTimestamp(int timestamp);
|
||||
|
||||
|
|
|
@ -576,7 +576,7 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(authSession).getLevelOfAuthenticationFromCurrentAuthentication()));
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
clientSession.setTimestamp(userSession.getLastSessionRefresh());
|
||||
|
||||
// Remove authentication session now
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(userSession.getRealm(), authSession, true);
|
||||
|
@ -1129,7 +1129,8 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
if (clientSessionMaxLifespan > 0) {
|
||||
int clientSessionMaxExpiration = userSession.getStarted() + clientSessionMaxLifespan;
|
||||
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
|
||||
int clientSessionMaxExpiration = clientSession.getStarted() + clientSessionMaxLifespan;
|
||||
sessionExpires = sessionExpires < clientSessionMaxExpiration ? sessionExpires : clientSessionMaxExpiration;
|
||||
}
|
||||
|
||||
|
@ -1167,7 +1168,8 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
if (clientOfflineSessionMaxLifespan > 0) {
|
||||
int clientOfflineSessionMaxExpiration = userSession.getStarted() + clientOfflineSessionMaxLifespan;
|
||||
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
|
||||
int clientOfflineSessionMaxExpiration = clientSession.getStarted() + clientOfflineSessionMaxLifespan;
|
||||
sessionExpires = sessionExpires < clientOfflineSessionMaxExpiration ? sessionExpires
|
||||
: clientOfflineSessionMaxExpiration;
|
||||
}
|
||||
|
|
|
@ -312,6 +312,13 @@ public class OAuthClient {
|
|||
return new AuthorizationEndpointResponse(this);
|
||||
}
|
||||
|
||||
public AuthorizationEndpointResponse doSilentLogin() {
|
||||
openLoginForm();
|
||||
WaitUtils.waitForPageToLoad();
|
||||
|
||||
return new AuthorizationEndpointResponse(this);
|
||||
}
|
||||
|
||||
public AuthorizationEndpointResponse doLoginSocial(String brokerId, String username, String password) {
|
||||
openLoginForm();
|
||||
WaitUtils.waitForPageToLoad();
|
||||
|
|
|
@ -33,11 +33,16 @@ import org.keycloak.common.constants.ServiceAccountConstants;
|
|||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
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.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
@ -78,10 +83,10 @@ import static org.hamcrest.Matchers.allOf;
|
|||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName;
|
||||
|
@ -710,13 +715,16 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
private int[] changeOfflineSessionSettings(boolean isEnabled, int sessionMax, int sessionIdle) {
|
||||
int prev[] = new int[2];
|
||||
private int[] changeOfflineSessionSettings(boolean isEnabled, int sessionMax, int sessionIdle, int clientSessionMax, int clientSessionIdle) {
|
||||
int prev[] = new int[5];
|
||||
RealmRepresentation rep = adminClient.realm("test").toRepresentation();
|
||||
prev[0] = rep.getOfflineSessionMaxLifespan().intValue();
|
||||
prev[1] = rep.getOfflineSessionIdleTimeout().intValue();
|
||||
prev[0] = rep.getOfflineSessionMaxLifespan();
|
||||
prev[1] = rep.getOfflineSessionIdleTimeout();
|
||||
prev[2] = rep.getClientOfflineSessionMaxLifespan();
|
||||
prev[3] = rep.getClientOfflineSessionIdleTimeout();
|
||||
RealmBuilder realmBuilder = RealmBuilder.create();
|
||||
realmBuilder.offlineSessionMaxLifespanEnabled(isEnabled).offlineSessionMaxLifespan(sessionMax).offlineSessionIdleTimeout(sessionIdle);
|
||||
realmBuilder.offlineSessionMaxLifespanEnabled(isEnabled).offlineSessionMaxLifespan(sessionMax).offlineSessionIdleTimeout(sessionIdle)
|
||||
.clientOfflineSessionMaxLifespan(clientSessionMax).clientOfflineSessionIdleTimeout(clientSessionIdle);
|
||||
adminClient.realm("test").update(realmBuilder.build());
|
||||
return prev;
|
||||
}
|
||||
|
@ -724,8 +732,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
private int[] changeSessionSettings(int ssoSessionIdle, int accessTokenLifespan) {
|
||||
int prev[] = new int[2];
|
||||
RealmRepresentation rep = adminClient.realm("test").toRepresentation();
|
||||
prev[0] = rep.getOfflineSessionMaxLifespan().intValue();
|
||||
prev[1] = rep.getOfflineSessionIdleTimeout().intValue();
|
||||
prev[0] = rep.getOfflineSessionMaxLifespan();
|
||||
prev[1] = rep.getOfflineSessionIdleTimeout();
|
||||
RealmBuilder realmBuilder = RealmBuilder.create();
|
||||
realmBuilder.ssoSessionIdleTimeout(ssoSessionIdle).accessTokenLifespan(accessTokenLifespan);
|
||||
adminClient.realm("test").update(realmBuilder.build());
|
||||
|
@ -737,7 +745,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
// expect that offline session expired by max lifespan
|
||||
final int MAX_LIFESPAN = 3600;
|
||||
final int IDLE_LIFESPAN = 6000;
|
||||
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, MAX_LIFESPAN + 60);
|
||||
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, MAX_LIFESPAN / 2, MAX_LIFESPAN + 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -746,7 +754,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
final int MAX_LIFESPAN = 3000;
|
||||
final int IDLE_LIFESPAN = 600;
|
||||
// Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
|
||||
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, IDLE_LIFESPAN + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 60);
|
||||
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, 0, IDLE_LIFESPAN + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 60);
|
||||
}
|
||||
|
||||
// Issue 13706
|
||||
|
@ -761,7 +769,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
int prev[] = null;
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(adminClient.realm("test")).setSsoSessionIdleTimeout(900).update()) {
|
||||
// Step 1 - offline login with "offline-client"
|
||||
prev = changeOfflineSessionSettings(true, MAX_LIFESPAN, IDLE_LIFESPAN);
|
||||
prev = changeOfflineSessionSettings(true, MAX_LIFESPAN, IDLE_LIFESPAN, 0, 0);
|
||||
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("offline-client");
|
||||
|
@ -794,7 +802,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1]);
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1], 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -829,10 +837,35 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
private void testOfflineSessionExpiration(int idleTime, int maxLifespan, int offset) {
|
||||
private String getOfflineClientSessionUuid(final String userSessionId, final String clientId) {
|
||||
return testingClient.server().fetch(session -> {
|
||||
RealmModel realmModel = session.realms().getRealmByName("test");
|
||||
ClientModel clientModel = realmModel.getClientByClientId(clientId);
|
||||
UserSessionModel userSession = session.sessions().getOfflineUserSession(realmModel, userSessionId);
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
|
||||
return clientSession.getId();
|
||||
}, String.class);
|
||||
}
|
||||
|
||||
private int checkIfUserAndClientSessionExist(final String userSessionId, final String clientId, final String clientSessionId) {
|
||||
return testingClient.server().fetch(session -> {
|
||||
RealmModel realmModel = session.realms().getRealmByName("test");
|
||||
session.getContext().setRealm(realmModel);
|
||||
ClientModel clientModel = realmModel.getClientByClientId(clientId);
|
||||
UserSessionModel userSession = session.sessions().getOfflineUserSession(realmModel, userSessionId);
|
||||
if (userSession != null) {
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
|
||||
return clientSession != null && clientSessionId.equals(clientSession.getId())? 2 : 1;
|
||||
}
|
||||
return 0;
|
||||
}, Integer.class);
|
||||
}
|
||||
|
||||
private void testOfflineSessionExpiration(int idleTime, int maxLifespan, int offsetHalf, int offset) {
|
||||
int prev[] = null;
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try {
|
||||
prev = changeOfflineSessionSettings(true, maxLifespan, idleTime);
|
||||
prev = changeOfflineSessionSettings(true, maxLifespan, idleTime, 0, 0);
|
||||
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("offline-client");
|
||||
|
@ -854,12 +887,20 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
|
||||
|
||||
// obtain the client session ID
|
||||
final String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
// perform a refresh in the half-time
|
||||
setTimeOffset(offsetHalf);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
|
||||
AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
offlineTokenString = tokenResponse.getRefreshToken();
|
||||
offlineToken = oauth.parseRefreshToken(offlineTokenString);
|
||||
|
||||
Assert.assertEquals(200, tokenResponse.getStatusCode());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
// wait to expire
|
||||
setTimeOffset(offset);
|
||||
|
@ -870,6 +911,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
assertEquals("invalid_grant", tokenResponse.getError());
|
||||
|
||||
// Assert userSession expired
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
testingClient.testing().removeExpired("test");
|
||||
try {
|
||||
testingClient.testing().removeUserSession("test", sessionId);
|
||||
|
@ -880,7 +922,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
setTimeOffset(0);
|
||||
|
||||
} finally {
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1]);
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -954,12 +997,106 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("offline-client");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
|
||||
int[] prev = changeOfflineSessionSettings(true, 3600, 7200, 1000, 7200);
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().client("offline-client")
|
||||
.detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000);
|
||||
String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
events.poll();
|
||||
|
||||
setTimeOffset(600);
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
events.expectRefresh(refreshId, sessionId).client("offline-client").detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE).assertEvent();
|
||||
|
||||
setTimeOffset(1100);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).client("offline-client").error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Exception {
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("offline-client");
|
||||
oauth.redirectUri(offlineClientAppUri);
|
||||
|
||||
int[] prev = changeOfflineSessionSettings(true, 3600, 7200, 5000, 7200);
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().client("offline-client")
|
||||
.detail(Details.REDIRECT_URI, offlineClientAppUri).assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600);
|
||||
String clientSessionId = getOfflineClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
events.poll();
|
||||
|
||||
setTimeOffset(1800);
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getType());
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800);
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
events.expectRefresh(refreshId, sessionId).client("offline-client").detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE).assertEvent();
|
||||
|
||||
setTimeOffset(3700);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "secret1");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).client("offline-client").error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
changeOfflineSessionSettings(false, prev[0], prev[1], prev[2], prev[3]);
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOfflineSessionMax() throws Exception {
|
||||
int prevOfflineSession[] = null;
|
||||
int prevSession[] = null;
|
||||
try {
|
||||
prevOfflineSession = changeOfflineSessionSettings(true, 60, 30);
|
||||
prevOfflineSession = changeOfflineSessionSettings(true, 60, 30, 0, 0);
|
||||
prevSession = changeSessionSettings(1800, 300);
|
||||
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
|
@ -989,7 +1126,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
|
|||
allOf(greaterThanOrEqualTo(59), lessThanOrEqualTo(60)));
|
||||
|
||||
} finally {
|
||||
changeOfflineSessionSettings(false, prevOfflineSession[0], prevOfflineSession[1]);
|
||||
changeOfflineSessionSettings(false, prevOfflineSession[0], prevOfflineSession[1], prevOfflineSession[2], prevOfflineSession[3]);
|
||||
changeSessionSettings(prevSession[0], prevSession[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.testsuite.oauth;
|
|||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import java.io.Closeable;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.jboss.arquillian.drone.webdriver.htmlunit.DroneHtmlUnitDriver;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
|
@ -33,12 +34,16 @@ import org.keycloak.admin.client.resource.RealmsResource;
|
|||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
@ -1045,37 +1050,182 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
private String getClientSessionUuid(final String userSessionId, String clientId) {
|
||||
return testingClient.server().fetch(session -> {
|
||||
RealmModel realmModel = session.realms().getRealmByName("test");
|
||||
ClientModel clientModel = realmModel.getClientByClientId(clientId);
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
|
||||
return clientSession.getId();
|
||||
}, String.class);
|
||||
}
|
||||
|
||||
private int checkIfUserAndClientSessionExist(final String userSessionId, final String clientId, final String clientSessionId) {
|
||||
return testingClient.server().fetch(session -> {
|
||||
RealmModel realmModel = session.realms().getRealmByName("test");
|
||||
ClientModel clientModel = realmModel.getClientByClientId(clientId);
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
|
||||
if (userSession != null) {
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
|
||||
return clientSession != null && clientSessionId.equals(clientSession.getId())? 2 : 1;
|
||||
}
|
||||
return 0;
|
||||
}, Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserSessionMaxLifespan() throws Exception {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
events.poll();
|
||||
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
Integer maxLifespan = realmResource.toRepresentation().getSsoSessionMaxLifespan();
|
||||
try {
|
||||
RealmManager.realm(realmResource).ssoSessionMaxLifespan(1);
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
}).update()) {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
setTimeOffset(2);
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600);
|
||||
final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
events.poll();
|
||||
|
||||
setTimeOffset(1800);
|
||||
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800);
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
events.expectRefresh(refreshId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(3700);
|
||||
oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
|
||||
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
|
||||
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
RealmManager.realm(realmResource).ssoSessionMaxLifespan(maxLifespan);
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
r.setClientSessionMaxLifespan(1000);
|
||||
r.setClientSessionIdleTimeout(7200);
|
||||
}).update()) {
|
||||
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000);
|
||||
String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
events.poll();
|
||||
|
||||
setTimeOffset(600);
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
events.expectRefresh(refreshId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(1100);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
setTimeOffset(1600);
|
||||
oauth.doSilentLogin();
|
||||
loginEvent = events.expectLogin().assertEvent();
|
||||
sessionId = loginEvent.getSessionId();
|
||||
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000);
|
||||
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
|
||||
|
||||
clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
setTimeOffset(3700);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Exception {
|
||||
RealmResource realmResource = adminClient.realm("test");
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
try (Closeable realmUpdater = new RealmAttributeUpdater(realmResource)
|
||||
.updateWith(r -> {
|
||||
r.setSsoSessionMaxLifespan(3600);
|
||||
r.setSsoSessionIdleTimeout(7200);
|
||||
r.setClientSessionMaxLifespan(5000);
|
||||
r.setClientSessionIdleTimeout(7200);
|
||||
}).update()) {
|
||||
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600);
|
||||
String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
|
||||
events.poll();
|
||||
|
||||
setTimeOffset(1800);
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800);
|
||||
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
events.expectRefresh(refreshId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(3700);
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
|
||||
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
@ -1132,6 +1282,79 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenClientSessionMaxLifespan() throws Exception {
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
|
||||
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
try {
|
||||
rep.setSsoSessionMaxLifespan(1000);
|
||||
realm.update(rep);
|
||||
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "500");
|
||||
client.update(clientRepresentation);
|
||||
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
String sessionId = loginEvent.getSessionId();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
events.poll();
|
||||
|
||||
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertTrue("Invalid RefreshExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 500);
|
||||
|
||||
setTimeOffset(100);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertTrue("Invalid RefreshExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
|
||||
|
||||
setTimeOffset(600);
|
||||
|
||||
oauth.doSilentLogin();
|
||||
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
assertEquals(200, tokenResponse.getStatusCode());
|
||||
assertTrue("Invalid RefreshExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
|
||||
|
||||
setTimeOffset(700);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertEquals(200, tokenResponse.getStatusCode());
|
||||
assertTrue("Invalid RefreshExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 300);
|
||||
|
||||
setTimeOffset(1100);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
assertEquals(400, tokenResponse.getStatusCode());
|
||||
assertNull(tokenResponse.getAccessToken());
|
||||
assertNull(tokenResponse.getRefreshToken());
|
||||
|
||||
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
|
||||
} finally {
|
||||
rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
|
||||
realm.update(rep);
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, null);
|
||||
client.update(clientRepresentation);
|
||||
|
||||
events.clear();
|
||||
resetTimeOffset();
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSsl() throws Exception {
|
||||
Client client = AdminClientUtil.createResteasyClient();
|
||||
|
|
|
@ -172,4 +172,18 @@ public class RealmManager {
|
|||
realm.update(rep);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmManager clientSessionMaxLifespan(int clientSessionLaxLifespan) {
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
rep.setClientSessionMaxLifespan(clientSessionLaxLifespan);
|
||||
realm.update(rep);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmManager clientSessionIdleTimeout(int clientSessionIdleTimeout) {
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
rep.setClientSessionIdleTimeout(clientSessionIdleTimeout);
|
||||
realm.update(rep);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -19,9 +19,7 @@ package org.keycloak.testsuite.ui.account2;
|
|||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.ui.account2.page.DeviceActivityPage;
|
||||
import org.keycloak.testsuite.ui.account2.page.PersonalInfoPage;
|
||||
|
@ -36,7 +34,7 @@ import static org.keycloak.testsuite.util.WaitUtils.pause;
|
|||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class SessionTest extends AbstractAccountTest {
|
||||
public static final int SSO_SESSION_IDLE_TIMEOUT = 1;
|
||||
public static final int SSO_SESSION_IDLE_TIMEOUT = 10;
|
||||
public static final int ACCESS_TOKEN_LIFESPAN = 10;
|
||||
|
||||
@Page
|
||||
|
@ -51,8 +49,8 @@ public class SessionTest extends AbstractAccountTest {
|
|||
RealmRepresentation realm = testRealms.get(0);
|
||||
|
||||
// in seconds
|
||||
realm.setSsoSessionIdleTimeout(1);
|
||||
realm.setAccessTokenLifespan(10);
|
||||
realm.setSsoSessionIdleTimeout(SSO_SESSION_IDLE_TIMEOUT);
|
||||
realm.setAccessTokenLifespan(ACCESS_TOKEN_LIFESPAN);
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -107,6 +105,6 @@ public class SessionTest extends AbstractAccountTest {
|
|||
private void waitForSessionToExpire() {
|
||||
// +3 to add some toleration
|
||||
log.info("Waiting for SSO session to expire");
|
||||
pause((SSO_SESSION_IDLE_TIMEOUT + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 3) * 1000);
|
||||
pause((SSO_SESSION_IDLE_TIMEOUT + 3) * 1000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* Copyright 2023 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.session;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.junit.Assert;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
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.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||
import org.keycloak.testsuite.model.RequireProvider;
|
||||
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Test that checks the Infinispan user session provider expires the sessions
|
||||
* correctly and does not remain client sessions in memory after user session
|
||||
* expiration.</p>
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
public class SessionTimeoutsTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
||||
@Override
|
||||
public void createEnvironment(KeycloakSession s) {
|
||||
RealmModel realm = createRealm(s, "test");
|
||||
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.clients().addClient(realm, "test-app");
|
||||
InfinispanTestUtil.setTestingTimeService(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanEnvironment(KeycloakSession s) {
|
||||
InfinispanTestUtil.revertTimeService(s);
|
||||
RealmModel realm = s.realms().getRealm(realmId);
|
||||
UserModel user1 = s.users().getUserByUsername(realm, "user1");
|
||||
s.sessions().removeUserSessions(realm);
|
||||
s.sessions().getOfflineUserSessionsStream(realm, user1).forEach(us -> s.sessions().removeOfflineUserSession(realm, us));
|
||||
|
||||
s.realms().removeRealm(realmId);
|
||||
}
|
||||
|
||||
protected static UserSessionModel createUserSession(KeycloakSession session, RealmModel realm, UserModel user, boolean offline) {
|
||||
UserSessionModel userSession = session.sessions().createUserSession(UUID.randomUUID().toString(), realm, user, "user1", "127.0.0.1",
|
||||
"form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
if (offline) {
|
||||
userSession = session.sessions().createOfflineUserSession(userSession);
|
||||
}
|
||||
return userSession;
|
||||
}
|
||||
|
||||
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);
|
||||
if (userSession.isOffline()) {
|
||||
clientSession = session.sessions().createOfflineClientSession(clientSession, userSession);
|
||||
}
|
||||
clientSession.setRedirectUri(redirect);
|
||||
if (state != null) {
|
||||
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
|
||||
}
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
protected static UserSessionModel getUserSession(KeycloakSession session, RealmModel realm, String id, boolean offline) {
|
||||
return offline
|
||||
? session.sessions().getOfflineUserSession(realm, id)
|
||||
: session.sessions().getUserSession(realm, id);
|
||||
}
|
||||
|
||||
protected void configureTimeouts(int realmMaxLifespan, int realmIdleTimeout, boolean overrideInClient, boolean lifespan, int clientValue) {
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
realm.setOfflineSessionMaxLifespanEnabled(true);
|
||||
realm.setOfflineSessionMaxLifespan(realmMaxLifespan);
|
||||
realm.setOfflineSessionIdleTimeout(realmIdleTimeout);
|
||||
realm.setClientOfflineSessionMaxLifespan(realmMaxLifespan);
|
||||
realm.setClientOfflineSessionIdleTimeout(realmIdleTimeout);
|
||||
realm.setSsoSessionMaxLifespan(realmMaxLifespan);
|
||||
realm.setSsoSessionIdleTimeout(realmIdleTimeout);
|
||||
realm.setClientSessionMaxLifespan(realmMaxLifespan);
|
||||
realm.setClientSessionIdleTimeout(realmIdleTimeout);
|
||||
String clientValueString = Integer.toString(clientValue);
|
||||
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
client.removeAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||
client.removeAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT);
|
||||
client.removeAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
|
||||
client.removeAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT);
|
||||
|
||||
if (overrideInClient) {
|
||||
if (lifespan) {
|
||||
client.setAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, clientValueString);
|
||||
client.setAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, clientValueString);
|
||||
} else {
|
||||
client.setAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT, clientValueString);
|
||||
client.setAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, clientValueString);
|
||||
}
|
||||
} else {
|
||||
if (lifespan) {
|
||||
realm.setClientOfflineSessionMaxLifespan(clientValue);
|
||||
realm.setClientSessionMaxLifespan(clientValue);
|
||||
} else {
|
||||
realm.setClientOfflineSessionIdleTimeout(clientValue);
|
||||
realm.setClientSessionIdleTimeout(clientValue);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
protected void testUserClientMaxLifespanSmallerThanSession(boolean offline, boolean overrideInClient) {
|
||||
configureTimeouts(3000, 7200, overrideInClient, true, 2000);
|
||||
|
||||
try {
|
||||
final String[] sessions = inComittedTransaction(session -> {
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "user1");
|
||||
UserSessionModel userSession = createUserSession(session, realm, user, offline);
|
||||
Assert.assertEquals(offline, userSession.isOffline());
|
||||
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
return new String[]{userSession.getId(), clientSession.getId()};
|
||||
});
|
||||
|
||||
setTimeOffset(1000);
|
||||
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// check the sessions are created
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
Assert.assertNotNull(userSession.getAuthenticatedClientSessionByClient(client.getId()));
|
||||
return null;
|
||||
});
|
||||
|
||||
setTimeOffset(2100);
|
||||
|
||||
sessions[1] = withRealm(realmId, (session, realm) -> {
|
||||
// refresh sessions after 2000 => only user session should exist
|
||||
session.getContext().setRealm(realm);
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
Assert.assertNull(userSession.getAuthenticatedClientSessionByClient(client.getId()));
|
||||
// recreate client session
|
||||
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
return clientSession.getId();
|
||||
});
|
||||
|
||||
setTimeOffset(2500);
|
||||
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// check the sessions are created
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
Assert.assertNotNull(userSession.getAuthenticatedClientSessionByClient(client.getId()));
|
||||
return null;
|
||||
});
|
||||
|
||||
setTimeOffset(3100);
|
||||
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// ensure user session is expired after user session expiration
|
||||
Assert.assertNull(getUserSession(session, realm, sessions[0], offline));
|
||||
return null;
|
||||
});
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected void testUserClientMaxLifespanGreaterThanSession(boolean offline, boolean overrideInClient) {
|
||||
configureTimeouts(3000, 7200, overrideInClient, true, 5000);
|
||||
|
||||
try {
|
||||
final String[] sessions = withRealm(realmId, (session, realm) -> {
|
||||
UserModel user = session.users().getUserByUsername(realm, "user1");
|
||||
UserSessionModel userSession = createUserSession(session, realm, user, offline);
|
||||
Assert.assertEquals(offline, userSession.isOffline());
|
||||
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
return new String[]{userSession.getId(), clientSession.getId()};
|
||||
});
|
||||
|
||||
setTimeOffset(2000);
|
||||
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// check the sessions are created
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
Assert.assertNotNull(userSession.getAuthenticatedClientSessionByClient(client.getId()));
|
||||
return null;
|
||||
});
|
||||
|
||||
setTimeOffset(3100);
|
||||
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// ensure user session is expired after user session expiration
|
||||
Assert.assertNull(getUserSession(session, realm, sessions[0], offline));
|
||||
return null;
|
||||
});
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected void testUserClientIdleTimeoutSmallerThanSession(int refreshTimes, boolean offline, boolean overrideInClient) {
|
||||
configureTimeouts(7200, 3000, overrideInClient, false, 2000);
|
||||
|
||||
try {
|
||||
final String[] sessions = withRealm(realmId, (session, realm) -> {
|
||||
UserModel user = session.users().getUserByUsername(realm, "user1");
|
||||
UserSessionModel userSession = createUserSession(session, realm, user, offline);
|
||||
Assert.assertEquals(offline, userSession.isOffline());
|
||||
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
return new String[]{userSession.getId(), clientSession.getId()};
|
||||
});
|
||||
|
||||
int offset = 0;
|
||||
for (int i = 0; i < refreshTimes; i++) {
|
||||
offset += 1500;
|
||||
setTimeOffset(offset);
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// refresh sessions before user session expires => both session should exist
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||
Assert.assertNotNull(clientSession);
|
||||
userSession.setLastSessionRefresh(Time.currentTime());
|
||||
clientSession.setTimestamp(Time.currentTime());
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
offset += 2100;
|
||||
setTimeOffset(offset);
|
||||
sessions[1] = withRealm(realmId, (session, realm) -> {
|
||||
// refresh sessions after 2000 => only user session should exist, client should be expired by idle
|
||||
session.getContext().setRealm(realm);
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
UserSessionModel userSession = getUserSession(session, realm, sessions[0], offline);
|
||||
Assert.assertNotNull(userSession);
|
||||
Assert.assertNull(userSession.getAuthenticatedClientSessionByClient(client.getId()));
|
||||
// recreate client session
|
||||
AuthenticatedClientSessionModel clientSession = createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
|
||||
return clientSession.getId();
|
||||
});
|
||||
|
||||
offset += 3100;
|
||||
setTimeOffset(offset);
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
// ensure user session is expired after user session expiration
|
||||
Assert.assertNull(getUserSession(session, realm, sessions[0], offline));
|
||||
return null;
|
||||
});
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientMaxLifespanGreaterThanSession() {
|
||||
testUserClientMaxLifespanGreaterThanSession(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientMaxLifespanGreaterThanSessionOverrideInClient() {
|
||||
testUserClientMaxLifespanGreaterThanSession(true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientMaxLifespanSmallerThanSession() {
|
||||
testUserClientMaxLifespanSmallerThanSession(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientMaxLifespanSmallerThanSessionOverrideInClient() {
|
||||
testUserClientMaxLifespanSmallerThanSession(true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientIdleTimeoutSmallerThanSessionNoRefresh() {
|
||||
testUserClientIdleTimeoutSmallerThanSession(0, true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOfflineUserClientIdleTimeoutSmallerThanSessionOneRefresh() {
|
||||
testUserClientIdleTimeoutSmallerThanSession(1, true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientMaxLifespanGreaterThanSession() {
|
||||
testUserClientMaxLifespanGreaterThanSession(false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientMaxLifespanGreaterThanSessionOverrideInClient() {
|
||||
testUserClientMaxLifespanGreaterThanSession(false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientMaxLifespanSmallerThanSession() {
|
||||
testUserClientMaxLifespanSmallerThanSession(false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientMaxLifespanSmallerThanSessionOverrideInClient() {
|
||||
testUserClientMaxLifespanSmallerThanSession(false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientIdleTimeoutSmallerThanSessionNoRefresh() {
|
||||
testUserClientIdleTimeoutSmallerThanSession(0, false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineUserClientIdleTimeoutSmallerThanSessionOneRefresh() {
|
||||
testUserClientIdleTimeoutSmallerThanSession(1, false, false);
|
||||
}
|
||||
}
|
|
@ -53,11 +53,11 @@ public class UserSessionExpirationTest extends KeycloakModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testClientSessionIdleTimeout() {
|
||||
public void testSsoSessionIdleTimeout() {
|
||||
|
||||
// Set low ClientSessionIdleTimeout
|
||||
// Set low ssoSessionIdleTimeout
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
realm.setSsoSessionIdleTimeout(1800);
|
||||
realm.setSsoSessionIdleTimeout(5);
|
||||
realm.setSsoSessionMaxLifespan(36000);
|
||||
realm.setClientSessionIdleTimeout(5);
|
||||
return null;
|
||||
|
|
Loading…
Reference in a new issue