Cache the client session if it is missing from the cache (#40156)

Closes #39785

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Alexander Schwartz 2025-06-03 10:35:35 +02:00 committed by GitHub
parent bb46b34e61
commit 202e9e8479
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 8 deletions

View File

@ -811,7 +811,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
return sessionsById.entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
}
private <T extends SessionEntity, K> Map<K, SessionEntityWrapper<T>> importSessionsWithExpiration(Map<K, SessionEntityWrapper<T>> sessionsById,
public <T extends SessionEntity, K> Map<K, SessionEntityWrapper<T>> importSessionsWithExpiration(Map<K, SessionEntityWrapper<T>> sessionsById,
BasicCache<K, SessionEntityWrapper<T>> cache, SessionFunction<T> lifespanMsCalculator,
SessionFunction<T> maxIdleTimeMsCalculator) {
return sessionsById.entrySet().stream().map(entry -> {

View File

@ -24,6 +24,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider;
import org.keycloak.models.sessions.infinispan.SessionFunction;
@ -32,8 +33,8 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
@ -158,13 +159,17 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
entity.setTimestamp(userSession.getLastSessionRefresh());
}
if (getMaxIdleMsLoader(offline).apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG
|| getLifespanMsLoader(offline).apply(realm, client, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) {
final UUID clientSessionId = entity.getId();
SessionEntityWrapper<AuthenticatedClientSessionEntity> wrapper = new SessionEntityWrapper<>(entity);
Map<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> imported = ((PersistentUserSessionProvider) kcSession.getProvider(UserSessionProvider.class)).importSessionsWithExpiration(Map.of(clientSessionId, wrapper), getCache(offline),
getLifespanMsLoader(offline),
getMaxIdleMsLoader(offline));
if (imported.isEmpty()) {
return null;
}
final UUID clientSessionId = entity.getId();
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
this.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
@ -176,10 +181,10 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions();
clientSessions.put(client.getId(), clientSessionId);
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId, offline);
SessionUpdateTask<UserSessionEntity> registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId, offline);
userSessionTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
return new SessionEntityWrapper<>(entity);
return wrapper;
}
public static class RegisterClientSessionTask implements PersistentSessionUpdateTask<UserSessionEntity> {

View File

@ -345,6 +345,52 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
}
}
@Test
public void refreshingTokenLoadsSessionIntoCache() {
ProfileAssume.assumeFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse response = oauth.doAccessTokenRequest(code);
String refreshTokenString = response.getRefreshToken();
// Test when neither client nor user session is in the cache
testingClient.server().run(session -> {
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).clear();
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).clear();
});
response = oauth.doRefreshTokenRequest(refreshTokenString);
Assert.assertEquals(200, response.getStatusCode());
testingClient.server().run(session -> {
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(),
greaterThan(0));
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).size(),
greaterThan(0));
});
// Test is only the client session is missing
testingClient.server().run(session -> {
session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).clear();
});
response = oauth.doRefreshTokenRequest(refreshTokenString);
Assert.assertEquals(200, response.getStatusCode());
testingClient.server().run(session -> {
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size(),
greaterThan(0));
assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME).size(),
greaterThan(0));
});
}
@Test
public void refreshTokenWithAccessToken() {
oauth.doLogin("test-user@localhost", "password");