mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
Session cache affinity
Closes #42776 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net> Co-authored-by: Steven Hawkins <shawkins@redhat.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
@@ -1,13 +1,27 @@
|
||||
package org.keycloak.common.util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SecretGenerator {
|
||||
|
||||
public static final int SECRET_LENGTH_256_BITS = 32;
|
||||
public static final int SECRET_LENGTH_384_BITS = 48;
|
||||
public static final int SECRET_LENGTH_512_BITS = 64;
|
||||
/**
|
||||
* Session ID length in bytes.
|
||||
* <p />
|
||||
* Both NIST and ANSSI ask for at least 128 bits of entropy, see <a href="https://github.com/keycloak/keycloak/issues/38663">#38663</a>.
|
||||
* As we are about to filter those session IDs on each node to find a key of the local segment using Infinispan's org.infinispan.affinity.KeyAffinityServiceFactory,
|
||||
* we add some more entropy so that the filtering then leaves enough entropy for those IDs.
|
||||
* Usually there are 256 segments in a cache. Just in case someone increases it, we add 16 bits.
|
||||
* This should handle the case when a caller connects to one node and generates codes (as it is the case with a keep-alive HTTP connection),
|
||||
* instead of a caller connecting to a random node on each request.
|
||||
*/
|
||||
private static final int SESSION_ID_BYTES = 18;
|
||||
public static final Supplier<String> SECURE_ID_GENERATOR = () -> getInstance().generateBase64SecureId(SESSION_ID_BYTES);
|
||||
|
||||
public static final char[] UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
|
||||
|
||||
@@ -30,6 +44,21 @@ public class SecretGenerator {
|
||||
return generateSecureUUID().toString();
|
||||
}
|
||||
|
||||
public String generateBase64SecureId(int nBytes) {
|
||||
assert nBytes > 0;
|
||||
byte[] data = new byte[nBytes];
|
||||
SECURE_RANDOM.nextBytes(data);
|
||||
String id = Base64.getUrlEncoder().encodeToString(data);
|
||||
|
||||
// disallow a dot, as a dot is used as a separator in AuthenticationSessionManager.decodeBase64AndValidateSignature
|
||||
assert !id.contains(".");
|
||||
|
||||
// disallow a space, as session_state must not contain a space (see https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions)
|
||||
assert !id.contains(" ");
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public String randomString() {
|
||||
return randomString(SECRET_LENGTH_256_BITS, ALPHANUM);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ To revert to the previoius behavior and to accept non-normalized URLs, set the o
|
||||
|
||||
Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
|
||||
|
||||
=== `session_state` and `sid` are no longer UUIDs
|
||||
|
||||
In OpenID connect, there are several places where the protocol shares a `session_state` and a `sid`.
|
||||
The specifications define it as an opaque string.
|
||||
Previous versions of {project_name} used a UUID for it, while the current version now uses a random base64-encoded string.
|
||||
The length of the string was reduced from 36 characters to 24 characters, although it might increase in the future if additional randomness is required.
|
||||
|
||||
=== `log-console-color` will automatically enable if supported by the terminal
|
||||
|
||||
The `log-console-color` previously defaulted to `false`, but it will now instead check if the terminal supports color.
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.commons.util.concurrent.CompletionStages;
|
||||
import org.infinispan.factories.ComponentRegistry;
|
||||
@@ -37,13 +40,11 @@ import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessio
|
||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
||||
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -51,15 +52,13 @@ import java.util.Map;
|
||||
public class InfinispanAuthenticationSessionProvider implements AuthenticationSessionProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final InfinispanKeyGenerator keyGenerator;
|
||||
private final int authSessionsLimit;
|
||||
protected final InfinispanChangelogBasedTransaction<String, RootAuthenticationSessionEntity> sessionTx;
|
||||
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
||||
|
||||
public InfinispanAuthenticationSessionProvider(KeycloakSession session, InfinispanKeyGenerator keyGenerator,
|
||||
public InfinispanAuthenticationSessionProvider(KeycloakSession session,
|
||||
InfinispanChangelogBasedTransaction<String, RootAuthenticationSessionEntity> sessionTx, int authSessionsLimit) {
|
||||
this.session = session;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.authSessionsLimit = authSessionsLimit;
|
||||
this.sessionTx = sessionTx;
|
||||
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
||||
@@ -69,8 +68,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||
|
||||
@Override
|
||||
public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
|
||||
String id = keyGenerator.generateKeyString(session, sessionTx.getCache());
|
||||
return createRootAuthenticationSession(realm, id);
|
||||
return createRootAuthenticationSession(realm, sessionTx.generateKey());
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +183,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||
public void migrate(String modelVersion) {
|
||||
if ("26.1.0".equals(modelVersion)) {
|
||||
InfinispanConnectionProvider infinispanConnectionProvider = session.getProvider(InfinispanConnectionProvider.class);
|
||||
Cache<String, RootAuthenticationSessionEntity> authSessionsCache = infinispanConnectionProvider.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
|
||||
Cache<String, RootAuthenticationSessionEntity> authSessionsCache = infinispanConnectionProvider.getCache(AUTHENTICATION_SESSIONS_CACHE_NAME);
|
||||
CompletionStages.join(ComponentRegistry.componentOf(authSessionsCache, PersistenceManager.class).clearAllStores(PersistenceManager.AccessMode.BOTH));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.cluster.ClusterEvent;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -41,7 +42,6 @@ import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessio
|
||||
import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
|
||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||
import org.keycloak.models.sessions.infinispan.transaction.InfinispanTransactionProvider;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.PostMigrationEvent;
|
||||
@@ -62,7 +62,6 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
|
||||
|
||||
private final InfinispanKeyGenerator keyGenerator = new InfinispanKeyGenerator();
|
||||
private CacheHolder<String, RootAuthenticationSessionEntity> cacheHolder;
|
||||
|
||||
private int authSessionsLimit;
|
||||
@@ -90,7 +89,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(this);
|
||||
try (var session = factory.create()) {
|
||||
cacheHolder = InfinispanChangesUtils.createWithCache(session, AUTHENTICATION_SESSIONS_CACHE_NAME, SessionTimeouts::getAuthSessionLifespanMS, SessionTimeouts::getAuthSessionMaxIdleMS);
|
||||
cacheHolder = InfinispanChangesUtils.createWithCache(session, AUTHENTICATION_SESSIONS_CACHE_NAME, SessionTimeouts::getAuthSessionLifespanMS, SessionTimeouts::getAuthSessionMaxIdleMS, SecretGenerator.SECURE_ID_GENERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +131,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||
|
||||
@Override
|
||||
public InfinispanAuthenticationSessionProvider create(KeycloakSession session) {
|
||||
return new InfinispanAuthenticationSessionProvider(session, keyGenerator, createTransaction(session), authSessionsLimit);
|
||||
return new InfinispanAuthenticationSessionProvider(session, createTransaction(session), authSessionsLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -75,7 +75,6 @@ import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.utils.StreamsUtil;
|
||||
|
||||
@@ -103,15 +102,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
protected final PersisterLastSessionRefreshStore persisterLastSessionRefreshStore;
|
||||
|
||||
protected final InfinispanKeyGenerator keyGenerator;
|
||||
|
||||
protected final SessionFunction<UserSessionEntity> offlineSessionCacheEntryLifespanAdjuster;
|
||||
|
||||
protected final SessionFunction<AuthenticatedClientSessionEntity> offlineClientSessionCacheEntryLifespanAdjuster;
|
||||
|
||||
public InfinispanUserSessionProvider(KeycloakSession session,
|
||||
PersisterLastSessionRefreshStore persisterLastSessionRefreshStore,
|
||||
InfinispanKeyGenerator keyGenerator,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx,
|
||||
InfinispanChangelogBasedTransaction<EmbeddedClientSessionKey, AuthenticatedClientSessionEntity> clientSessionTx,
|
||||
@@ -128,7 +124,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
||||
|
||||
this.persisterLastSessionRefreshStore = persisterLastSessionRefreshStore;
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.offlineSessionCacheEntryLifespanAdjuster = offlineSessionCacheEntryLifespanAdjuster;
|
||||
this.offlineClientSessionCacheEntryLifespanAdjuster = offlineClientSessionCacheEntryLifespanAdjuster;
|
||||
|
||||
@@ -182,7 +177,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
|
||||
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
|
||||
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
if (id == null) {
|
||||
id = keyGenerator.generateKeyString(session, sessionTx.getCache());
|
||||
id = sessionTx.generateKey();
|
||||
}
|
||||
|
||||
UserSessionEntity entity = UserSessionEntity.create(id, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
|
||||
|
||||
@@ -24,12 +24,11 @@ import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.affinity.KeyGenerator;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.MultiSiteUtils;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||
import org.keycloak.models.ClientModel;
|
||||
@@ -55,7 +54,6 @@ import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionCluster
|
||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
|
||||
import org.keycloak.models.sessions.infinispan.transaction.InfinispanTransactionProvider;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.PostMigrationEvent;
|
||||
@@ -97,7 +95,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
private long offlineClientSessionCacheEntryLifespanOverride;
|
||||
|
||||
private PersisterLastSessionRefreshStore persisterLastSessionRefreshStore;
|
||||
private InfinispanKeyGenerator keyGenerator;
|
||||
ArrayBlockingQueue<PersistentUpdate> asyncQueuePersistentUpdate;
|
||||
private PersistentSessionsWorker persistentSessionsWorker;
|
||||
private int maxBatchSize;
|
||||
@@ -110,7 +107,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
var tx = createPersistentTransaction(session);
|
||||
return new PersistentUserSessionProvider(
|
||||
session,
|
||||
keyGenerator,
|
||||
tx.userTx,
|
||||
tx.clientTx
|
||||
);
|
||||
@@ -119,7 +115,6 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
return new InfinispanUserSessionProvider(
|
||||
session,
|
||||
persisterLastSessionRefreshStore,
|
||||
keyGenerator,
|
||||
tx.sessionTx,
|
||||
tx.offlineSessionTx,
|
||||
tx.clientSessionTx,
|
||||
@@ -154,17 +149,9 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
public void postInit(final KeycloakSessionFactory factory) {
|
||||
factory.register(event -> {
|
||||
if (event instanceof PostMigrationEvent) {
|
||||
if (!useCaches) {
|
||||
keyGenerator = new InfinispanKeyGenerator() {
|
||||
@Override
|
||||
protected <K> K generateKey(KeycloakSession session, Cache<K, ?> cache, KeyGenerator<K> keyGenerator) {
|
||||
return keyGenerator.getKey();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (useCaches) {
|
||||
KeycloakModelUtils.runJobInTransaction(factory, (KeycloakSession session) -> {
|
||||
|
||||
keyGenerator = new InfinispanKeyGenerator();
|
||||
if (!MultiSiteUtils.isPersistentSessionsEnabled()) {
|
||||
initializePersisterLastSessionRefreshStore(factory);
|
||||
}
|
||||
@@ -199,20 +186,20 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
|
||||
if (MultiSiteUtils.isPersistentSessionsEnabled()) {
|
||||
if (useCaches) {
|
||||
try (var session = factory.create()) {
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithCache(session, USER_SESSION_CACHE_NAME, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithCache(session, USER_SESSION_CACHE_NAME, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs, SecretGenerator.SECURE_ID_GENERATOR);
|
||||
offlineSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_USER_SESSION_CACHE_NAME, SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
clientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, CLIENT_SESSION_CACHE_NAME, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||
offlineClientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_CLIENT_SESSION_CACHE_NAME, SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||
}
|
||||
} else {
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithoutCache(SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithoutCache(SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs, SecretGenerator.SECURE_ID_GENERATOR);
|
||||
offlineSessionCacheHolder = InfinispanChangesUtils.createWithoutCache(SessionTimeouts::getOfflineSessionLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
clientSessionCacheHolder = InfinispanChangesUtils.createWithoutCache(SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||
offlineClientSessionCacheHolder = InfinispanChangesUtils.createWithoutCache(SessionTimeouts::getOfflineClientSessionLifespanMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||
}
|
||||
} else {
|
||||
try (var session = factory.create()) {
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithCache(session, USER_SESSION_CACHE_NAME, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs);
|
||||
sessionCacheHolder = InfinispanChangesUtils.createWithCache(session, USER_SESSION_CACHE_NAME, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs, SecretGenerator.SECURE_ID_GENERATOR);
|
||||
offlineSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_USER_SESSION_CACHE_NAME, this::deriveOfflineSessionCacheEntryLifespanMs, SessionTimeouts::getOfflineSessionMaxIdleMs);
|
||||
clientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, CLIENT_SESSION_CACHE_NAME, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs);
|
||||
offlineClientSessionCacheHolder = InfinispanChangesUtils.createWithCache(session, OFFLINE_CLIENT_SESSION_CACHE_NAME, this::deriveOfflineClientSessionCacheEntryLifespanOverrideMs, SessionTimeouts::getOfflineClientSessionMaxIdleMs);
|
||||
|
||||
@@ -82,11 +82,14 @@ import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||
import org.keycloak.models.sessions.infinispan.util.SessionTimeouts;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
|
||||
import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER;
|
||||
import static org.keycloak.models.sessions.infinispan.changes.ClientSessionPersistentChangelogBasedTransaction.createAuthenticatedClientSessionInstance;
|
||||
import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
||||
@@ -105,10 +108,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
||||
|
||||
protected final InfinispanKeyGenerator keyGenerator;
|
||||
|
||||
public PersistentUserSessionProvider(KeycloakSession session,
|
||||
InfinispanKeyGenerator keyGenerator,
|
||||
UserSessionPersistentChangelogBasedTransaction sessionTx,
|
||||
ClientSessionPersistentChangelogBasedTransaction clientSessionTx) {
|
||||
if (!MultiSiteUtils.isPersistentSessionsEnabled()) {
|
||||
@@ -119,7 +119,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
this.sessionTx = sessionTx;
|
||||
this.clientSessionTx = clientSessionTx;
|
||||
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
|
||||
this.keyGenerator = keyGenerator;
|
||||
|
||||
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
|
||||
}
|
||||
@@ -183,7 +182,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
|
||||
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
if (id == null) {
|
||||
id = keyGenerator.generateKeyString(session, sessionTx.getCache(false));
|
||||
id = sessionTx.generateKey();
|
||||
}
|
||||
|
||||
UserSessionEntity entity = new UserSessionEntity(id);
|
||||
@@ -847,7 +846,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
// This is a best-effort approach: Even if due to a rolling update some entries are left there, the checking of sessions and tokens does not depend on them.
|
||||
// Refreshing of tokens will still work even if the user session does not contain the list of client sessions.
|
||||
var stage = CompletionStages.aggregateCompletionStage();
|
||||
Stream.of(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME)
|
||||
Stream.of(USER_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)
|
||||
.map(s -> {
|
||||
InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
|
||||
if (provider != null) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.changes;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.util.concurrent.ActionSequencer;
|
||||
import org.keycloak.models.sessions.infinispan.SessionFunction;
|
||||
@@ -29,5 +31,6 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
public record CacheHolder<K, V extends SessionEntity>(Cache<K, SessionEntityWrapper<V>> cache,
|
||||
ActionSequencer sequencer,
|
||||
SessionFunction<V> lifespanFunction,
|
||||
SessionFunction<V> maxIdleFunction) {
|
||||
SessionFunction<V> maxIdleFunction,
|
||||
Supplier<K> keyGenerator) {
|
||||
}
|
||||
|
||||
@@ -187,6 +187,11 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> imp
|
||||
return cacheHolder.cache();
|
||||
}
|
||||
|
||||
public K generateKey() {
|
||||
assert cacheHolder.keyGenerator() != null;
|
||||
return cacheHolder.keyGenerator().get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a session from an external source into the {@link Cache}.
|
||||
* <p>
|
||||
|
||||
@@ -20,8 +20,10 @@ package org.keycloak.models.sessions.infinispan.changes;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.affinity.KeyAffinityServiceFactory;
|
||||
import org.infinispan.commons.util.concurrent.AggregateCompletionStage;
|
||||
import org.infinispan.commons.util.concurrent.CompletableFutures;
|
||||
import org.infinispan.commons.util.concurrent.CompletionStages;
|
||||
@@ -39,6 +41,9 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
*/
|
||||
public class InfinispanChangesUtils {
|
||||
|
||||
// by default, keep 128 keys ready to use
|
||||
private static final int DEFAULT_KEY_BUFFER = 128;
|
||||
|
||||
private InfinispanChangesUtils() {
|
||||
}
|
||||
|
||||
@@ -46,15 +51,38 @@ public class InfinispanChangesUtils {
|
||||
String cacheName,
|
||||
SessionFunction<V> lifespanFunction,
|
||||
SessionFunction<V> maxIdleFunction) {
|
||||
return createWithCache(session, cacheName, lifespanFunction, maxIdleFunction, null);
|
||||
}
|
||||
|
||||
public static <K, V extends SessionEntity> CacheHolder<K, V> createWithCache(KeycloakSession session,
|
||||
String cacheName,
|
||||
SessionFunction<V> lifespanFunction,
|
||||
SessionFunction<V> maxIdleFunction,
|
||||
Supplier<K> keyGenerator) {
|
||||
var connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||
var cache = connections.<K, SessionEntityWrapper<V>>getCache(cacheName);
|
||||
var sequencer = new ActionSequencer(connections.getExecutor(cacheName + "Replace"), false, null);
|
||||
return new CacheHolder<>(cache, sequencer, lifespanFunction, maxIdleFunction);
|
||||
if (!cache.getCacheConfiguration().clustering().cacheMode().isClustered() || keyGenerator == null) {
|
||||
return new CacheHolder<>(cache, sequencer, lifespanFunction, maxIdleFunction, keyGenerator);
|
||||
}
|
||||
var local = cache.getAdvancedCache().getRpcManager().getAddress();
|
||||
var affinity = KeyAffinityServiceFactory.newLocalKeyAffinityService(
|
||||
cache,
|
||||
keyGenerator::get,
|
||||
connections.getExecutor(cacheName + "KeyGenerator"),
|
||||
DEFAULT_KEY_BUFFER);
|
||||
return new CacheHolder<>(cache, sequencer, lifespanFunction, maxIdleFunction, () -> affinity.getKeyForAddress(local));
|
||||
}
|
||||
|
||||
public static <K, V extends SessionEntity> CacheHolder<K, V> createWithoutCache(SessionFunction<V> lifespanFunction,
|
||||
SessionFunction<V> maxIdleFunction) {
|
||||
return new CacheHolder<>(null, null, lifespanFunction, maxIdleFunction);
|
||||
return new CacheHolder<>(null, null, lifespanFunction, maxIdleFunction, null);
|
||||
}
|
||||
|
||||
public static <K, V extends SessionEntity> CacheHolder<K, V> createWithoutCache(SessionFunction<V> lifespanFunction,
|
||||
SessionFunction<V> maxIdleFunction,
|
||||
Supplier<K> keyGenerator) {
|
||||
return new CacheHolder<>(null, null, lifespanFunction, maxIdleFunction, keyGenerator);
|
||||
}
|
||||
|
||||
public static <K, V extends SessionEntity> void runOperationInCluster(
|
||||
|
||||
@@ -77,6 +77,11 @@ abstract public class PersistentSessionsChangelogBasedTransaction<K, V extends S
|
||||
return offline ? offlineUpdates : updates;
|
||||
}
|
||||
|
||||
public K generateKey() {
|
||||
assert cacheHolder.keyGenerator() != null;
|
||||
return cacheHolder.keyGenerator().get();
|
||||
}
|
||||
|
||||
public SessionEntityWrapper<V> get(K key, boolean offline) {
|
||||
SessionUpdatesList<V> myUpdates = getUpdates(offline).get(key);
|
||||
if (myUpdates == null) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -29,7 +30,6 @@ import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNote
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.remote.transaction.AuthenticationSessionChangeLogTransaction;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
@@ -53,7 +53,7 @@ public class RemoteInfinispanAuthenticationSessionProvider implements Authentica
|
||||
|
||||
@Override
|
||||
public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
|
||||
return createRootAuthenticationSession(realm, KeycloakModelUtils.generateId());
|
||||
return createRootAuthenticationSession(realm, SecretGenerator.SECURE_ID_GENERATOR.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.infinispan.commons.util.concurrent.CompletionStages;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@@ -111,7 +112,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider {
|
||||
@Override
|
||||
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
if (id == null) {
|
||||
id = KeycloakModelUtils.generateId();
|
||||
id = SecretGenerator.SECURE_ID_GENERATOR.get();
|
||||
}
|
||||
|
||||
var entity = RemoteUserSessionEntity.create(id, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
|
||||
|
||||
@@ -29,11 +29,14 @@ import org.infinispan.affinity.KeyGenerator;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.sessions.infinispan.changes.CacheHolder;
|
||||
import org.keycloak.sessions.StickySessionEncoderProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
* @deprecated not supported and to be removed. Check {@link CacheHolder#keyGenerator()}
|
||||
*/
|
||||
@Deprecated(since = "26.4", forRemoval = true)
|
||||
public class InfinispanKeyGenerator {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanKeyGenerator.class);
|
||||
|
||||
@@ -482,8 +482,14 @@ public final class CacheConfigurator {
|
||||
switch (cacheName) {
|
||||
// Distributed Caches
|
||||
case CLIENT_SESSION_CACHE_NAME:
|
||||
case USER_SESSION_CACHE_NAME:
|
||||
case OFFLINE_CLIENT_SESSION_CACHE_NAME:
|
||||
// Groups keys by user session ID.
|
||||
if (clustered) {
|
||||
builder.clustering().hash().groups()
|
||||
.enabled()
|
||||
.addGrouper(ClientSessionKeyGrouper.INSTANCE);
|
||||
}
|
||||
case USER_SESSION_CACHE_NAME:
|
||||
case OFFLINE_USER_SESSION_CACHE_NAME:
|
||||
if (clustered) {
|
||||
builder.clustering().cacheMode(CacheMode.DIST_SYNC).hash().numOwners(1);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 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.spi.infinispan.impl.embedded;
|
||||
|
||||
import org.infinispan.distribution.group.Grouper;
|
||||
import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey;
|
||||
|
||||
/**
|
||||
* A {@link Grouper} implementation that uses the User Session ID to assign the Client Session to the cache segment. It
|
||||
* groups all the Client Sessions belonging to the same User Session in the same node where the User Session lives.
|
||||
*/
|
||||
public enum ClientSessionKeyGrouper implements Grouper<EmbeddedClientSessionKey> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
// The Infinispan parser expects a constructor or a static "getInstance" method; fixes ClusterConfigKeepAliveDistTest.
|
||||
public static ClientSessionKeyGrouper getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object computeGroup(EmbeddedClientSessionKey key, Object group) {
|
||||
return key.userSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<EmbeddedClientSessionKey> getKeyType() {
|
||||
return EmbeddedClientSessionKey.class;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package org.keycloak.testframework.events;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EventMatchers {
|
||||
|
||||
@@ -12,6 +14,32 @@ public class EventMatchers {
|
||||
return new UUIDMatcher();
|
||||
}
|
||||
|
||||
public static Matcher<String> isCodeId() {
|
||||
// Make the tests pass with the old and the new encoding of code IDs
|
||||
return Matchers.anyOf(isBase64WithAtLeast128Bits(), isUUID());
|
||||
}
|
||||
|
||||
public static Matcher<String> isSessionId() {
|
||||
// Make the tests pass with the old and the new encoding of sessions
|
||||
return Matchers.anyOf(isBase64WithAtLeast128Bits(), isUUID());
|
||||
}
|
||||
|
||||
public static Matcher<String> isBase64WithAtLeast128Bits() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
private static final Pattern BASE64 = Pattern.compile("[-A-Za-z0-9+/_]*");
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(String item) {
|
||||
return item.length() >= 24 && item.matches(BASE64.pattern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("not an base64 ID with at least 128bits");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private EventMatchers() {
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.tests.admin;
|
||||
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -41,6 +42,7 @@ import org.keycloak.testframework.annotations.InjectEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.events.EventMatchers;
|
||||
import org.keycloak.testframework.events.Events;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
@@ -302,8 +304,7 @@ public class ConsentsTest {
|
||||
Assertions.assertEquals(EventType.LOGIN.toString(), loginEvent.getType());
|
||||
loginEvent.getDetails().forEach((key, value) -> {
|
||||
switch (key) {
|
||||
case Details.CODE_ID ->
|
||||
Assertions.assertTrue(isUUID(value));
|
||||
case Details.CODE_ID -> MatcherAssert.assertThat(value, EventMatchers.isCodeId());
|
||||
case Details.USERNAME -> Assertions.assertEquals(userFromUserRealm.getUsername(), value);
|
||||
case Details.CONSENT -> Assertions.assertEquals(Details.CONSENT_VALUE_NO_CONSENT_REQUIRED, value);
|
||||
case Details.REDIRECT_URI -> Assertions.assertEquals("http://127.0.0.1:8500/callback/oauth", value);
|
||||
|
||||
@@ -305,7 +305,7 @@ public class ImpersonationTest {
|
||||
|
||||
EventRepresentation event = events.poll();
|
||||
Assertions.assertEquals(event.getType(), EventType.IMPERSONATE.toString());
|
||||
MatcherAssert.assertThat(event.getSessionId(), EventMatchers.isUUID());
|
||||
MatcherAssert.assertThat(event.getSessionId(), EventMatchers.isSessionId());
|
||||
Assertions.assertEquals(event.getUserId(), managedUser.getId());
|
||||
Assertions.assertTrue(event.getDetails().values().stream().anyMatch(f -> f.equals(admin)));
|
||||
Assertions.assertTrue(event.getDetails().values().stream().anyMatch(f -> f.equals(adminRealm)));
|
||||
|
||||
@@ -47,6 +47,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
@@ -118,7 +119,7 @@ public class AssertEvents implements TestRule {
|
||||
//.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE)
|
||||
.detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI))
|
||||
.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
|
||||
.session(isUUID());
|
||||
.session(isSessionId());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectClientLogin() {
|
||||
@@ -127,7 +128,7 @@ public class AssertEvents implements TestRule {
|
||||
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||
.detail(Details.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)
|
||||
.removeDetail(Details.CODE_ID)
|
||||
.session(isUUID());
|
||||
.session(isSessionId());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectSocialLogin() {
|
||||
@@ -136,14 +137,14 @@ public class AssertEvents implements TestRule {
|
||||
.detail(Details.USERNAME, DEFAULT_USERNAME)
|
||||
.detail(Details.AUTH_METHOD, "form")
|
||||
.detail(Details.REDIRECT_URI, Matchers.equalTo(DEFAULT_REDIRECT_URI))
|
||||
.session(isUUID());
|
||||
.session(isSessionId());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectCodeToToken(String codeId, String sessionId) {
|
||||
return expect(EventType.CODE_TO_TOKEN)
|
||||
.detail(Details.CODE_ID, codeId)
|
||||
.detail(Details.TOKEN_ID, isAccessTokenId(AuthorizationCodeGrantTypeFactory.GRANT_SHORTCUT))
|
||||
.detail(Details.REFRESH_TOKEN_ID, isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||
.session(sessionId);
|
||||
@@ -153,7 +154,7 @@ public class AssertEvents implements TestRule {
|
||||
return expect(EventType.OAUTH2_DEVICE_VERIFY_USER_CODE)
|
||||
.user((String) null)
|
||||
.client(clientId)
|
||||
.detail(Details.CODE_ID, isUUID());
|
||||
.detail(Details.CODE_ID, isCodeId());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectDeviceLogin(String clientId, String codeId, String userId) {
|
||||
@@ -171,7 +172,7 @@ public class AssertEvents implements TestRule {
|
||||
.user(userId)
|
||||
.detail(Details.CODE_ID, codeId)
|
||||
.detail(Details.TOKEN_ID, isAccessTokenId(DeviceGrantTypeFactory.GRANT_SHORTCUT))
|
||||
.detail(Details.REFRESH_TOKEN_ID, isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||
.session(codeId);
|
||||
@@ -182,7 +183,7 @@ public class AssertEvents implements TestRule {
|
||||
.detail(Details.TOKEN_ID, isAccessTokenId(RefreshTokenGrantTypeFactory.GRANT_SHORTCUT))
|
||||
.detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, isTokenId())
|
||||
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||
.session(sessionId);
|
||||
}
|
||||
@@ -242,10 +243,10 @@ public class AssertEvents implements TestRule {
|
||||
return expect(EventType.AUTHREQID_TO_TOKEN)
|
||||
.detail(Details.CODE_ID, codeId)
|
||||
.detail(Details.TOKEN_ID, isAccessTokenId(CibaGrantTypeFactory.GRANT_SHORTCUT))
|
||||
.detail(Details.REFRESH_TOKEN_ID, isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
|
||||
.session(isUUID());
|
||||
.session(isSessionId());
|
||||
}
|
||||
|
||||
public ExpectedEvent expectClientPolicyError(EventType eventType, String error, String reason, String clientPolicyError, String clientPolicyErrorDetail) {
|
||||
@@ -466,7 +467,34 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public static Matcher<String> isCodeId() {
|
||||
return isUUID();
|
||||
// Make the tests pass with the old and the new encoding of code IDs
|
||||
return Matchers.anyOf(isBase64WithAtLeast128Bits(), isUUID());
|
||||
}
|
||||
|
||||
public static Matcher<String> isSessionId() {
|
||||
// Make the tests pass with the old and the new encoding of sessions
|
||||
return Matchers.anyOf(isBase64WithAtLeast128Bits(), isUUID());
|
||||
}
|
||||
|
||||
public static Matcher<String> isTokenId() {
|
||||
// Make the tests pass with the old and the new encoding of token IDs
|
||||
return Matchers.anyOf(isBase64WithAtLeast128Bits(), isUUID());
|
||||
}
|
||||
|
||||
public static Matcher<String> isBase64WithAtLeast128Bits() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
private static final Pattern BASE64 = Pattern.compile("[-A-Za-z0-9+/_]*");
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(String item) {
|
||||
return item.length() >= 24 && item.matches(BASE64.pattern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("not an base64 ID with at least 128bits");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<String> isUUID() {
|
||||
@@ -491,7 +519,7 @@ public class AssertEvents implements TestRule {
|
||||
if (items.length != 2) return false;
|
||||
// Grant type shortcut starts at character 4th char and is 2-chars long
|
||||
if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false;
|
||||
return isUUID().matches(items[1]);
|
||||
return isTokenId().matches(items[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -587,7 +587,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
|
||||
|
||||
tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
|
||||
oauth.logoutForm().idTokenHint(tokenResponse.getIdToken()).withRedirect().open();
|
||||
events.expectLogout(null).session(AssertEvents.isUUID()).assertEvent();
|
||||
events.expectLogout(null).session(AssertEvents.isSessionId()).assertEvent();
|
||||
|
||||
// test lookAheadWindow
|
||||
realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
||||
@@ -674,7 +674,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
tokenResponse = sendTokenRequestAndGetResponse(loginEvent);
|
||||
oauth.logoutForm().idTokenHint(tokenResponse.getIdToken()).withRedirect().open();
|
||||
events.expectLogout(null).session(AssertEvents.isUUID()).assertEvent();
|
||||
events.expectLogout(null).session(AssertEvents.isSessionId()).assertEvent();
|
||||
|
||||
// test lookAheadWindow
|
||||
realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
||||
@@ -2933,7 +2933,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||
else assertThat(tokenRes.getErrorDescription(), is(equalTo("Session not active")));
|
||||
|
||||
RefreshToken rt = oauth.parseRefreshToken(refreshToken);
|
||||
return events.expectLogout(sessionId).client(TEST_CLIENT_NAME).user(rt.getSubject()).session(AssertEvents.isUUID()).clearDetails().assertEvent();
|
||||
return events.expectLogout(sessionId).client(TEST_CLIENT_NAME).user(rt.getSubject()).session(AssertEvents.isSessionId()).clearDetails().assertEvent();
|
||||
}
|
||||
|
||||
private EventRepresentation doTokenRevokeByRefreshToken(String refreshToken, String sessionId, String userId, boolean isOfflineAccess) throws IOException {
|
||||
|
||||
@@ -26,6 +26,7 @@ import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.Form;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
@@ -47,7 +48,6 @@ import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
@@ -1100,7 +1100,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest {
|
||||
String authSessionId = decodedAuthSessionId.substring(0, decodedAuthSessionId.indexOf("."));
|
||||
String signature = decodedAuthSessionId.substring(decodedAuthSessionId.indexOf(".") + 1);
|
||||
Assert.assertNotNull(authSessionId);
|
||||
Assert.assertTrue(KeycloakModelUtils.isValidUUID(authSessionId));
|
||||
MatcherAssert.assertThat(authSessionId, AssertEvents.isSessionId());
|
||||
Assert.assertNotNull(signature);
|
||||
|
||||
testingClient.server().run(session-> {
|
||||
|
||||
@@ -296,7 +296,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||
|
||||
events.expect(EventType.LOGIN)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.USERNAME, "test-user@localhost")
|
||||
.detail(OIDCLoginProtocol.RESPONSE_MODE_PARAM, OIDCResponseMode.FORM_POST.name().toLowerCase())
|
||||
.detail(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||
|
||||
@@ -21,7 +21,7 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.testsuite.AssertEvents.isUUID;
|
||||
import static org.keycloak.testsuite.AssertEvents.isTokenId;
|
||||
import static org.keycloak.testsuite.AbstractAdminTest.loadJson;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -318,7 +318,7 @@ public class TokenRevocationTest extends AbstractKeycloakTest {
|
||||
|
||||
events.expect(EventType.REVOKE_GRANT)
|
||||
.session(tokenResponse.getSessionState())
|
||||
.detail(Details.REFRESH_TOKEN_ID, isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, expectedTokenType)
|
||||
.client("test-app")
|
||||
.assertEvent(true);
|
||||
|
||||
@@ -103,7 +103,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
|
||||
//.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE)
|
||||
.detail(Details.REDIRECT_URI, defaultRedirectUri)
|
||||
.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
|
||||
.session(isUUID());
|
||||
.session(isSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "requested_token_type unsupported")
|
||||
.detail(Details.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
|
||||
.detail(Details.SUBJECT_TOKEN_CLIENT_ID, "subject-client")
|
||||
@@ -252,7 +252,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
events.expect(EventType.TOKEN_EXCHANGE)
|
||||
.client("requester-client")
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REQUESTED_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE)
|
||||
.detail(Details.SUBJECT_TOKEN_CLIENT_ID, "subject-client")
|
||||
.assertEvent();
|
||||
@@ -265,7 +265,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "requested_token_type unsupported")
|
||||
.detail(Details.REQUESTED_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
|
||||
.detail(Details.SUBJECT_TOKEN_CLIENT_ID, "subject-client")
|
||||
@@ -279,7 +279,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "requested_token_type unsupported")
|
||||
.detail(Details.REQUESTED_TOKEN_TYPE, OAuth2Constants.SAML2_TOKEN_TYPE)
|
||||
.detail(Details.SUBJECT_TOKEN_CLIENT_ID, "subject-client")
|
||||
@@ -293,7 +293,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "requested_token_type unsupported")
|
||||
.detail(Details.REQUESTED_TOKEN_TYPE, "WRONG_TOKEN_TYPE")
|
||||
.detail(Details.SUBJECT_TOKEN_CLIENT_ID, "subject-client")
|
||||
@@ -329,7 +329,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("invalid-requester-client")
|
||||
.error(Errors.NOT_ALLOWED)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "client is not within the token audience")
|
||||
.assertEvent();
|
||||
}
|
||||
@@ -742,7 +742,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.INVALID_REQUEST)
|
||||
.user(john.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "Requested audience not available: target-client2")
|
||||
.assertEvent();
|
||||
|
||||
@@ -788,9 +788,9 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
AccessToken exchangedToken = assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));
|
||||
events.expect(EventType.REFRESH_TOKEN)
|
||||
.detail(Details.TOKEN_ID, exchangedToken.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, AssertEvents.isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, AssertEvents.isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, AssertEvents.isUUID())
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, AssertEvents.isTokenId())
|
||||
.session(exchangedToken.getSessionId());
|
||||
|
||||
oauth.client("requester-client", "secret");
|
||||
@@ -798,9 +798,9 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
exchangedToken = assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));
|
||||
events.expect(EventType.REFRESH_TOKEN)
|
||||
.detail(Details.TOKEN_ID, exchangedToken.getId())
|
||||
.detail(Details.REFRESH_TOKEN_ID, AssertEvents.isUUID())
|
||||
.detail(Details.REFRESH_TOKEN_ID, AssertEvents.isTokenId())
|
||||
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_REFRESH)
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, AssertEvents.isUUID())
|
||||
.detail(Details.UPDATED_REFRESH_TOKEN_ID, AssertEvents.isTokenId())
|
||||
.session(exchangedToken.getSessionId());
|
||||
}
|
||||
}
|
||||
@@ -844,7 +844,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.CONSENT_DENIED)
|
||||
.user(mike.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "Missing consents for Token Exchange in client requester-client")
|
||||
.assertEvent();
|
||||
|
||||
@@ -866,7 +866,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
.client("requester-client")
|
||||
.error(Errors.CONSENT_DENIED)
|
||||
.user(mike.getId())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.detail(Details.REASON, "Missing consents for Token Exchange in client requester-client")
|
||||
.assertEvent();
|
||||
|
||||
@@ -1221,7 +1221,7 @@ public class StandardTokenExchangeV2Test extends AbstractClientPoliciesTest {
|
||||
assertTrue(rep.isActive());
|
||||
events.expect(EventType.INTROSPECT_TOKEN)
|
||||
.user(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isUUID())
|
||||
.session(AssertEvents.isSessionId())
|
||||
.client(clientId)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
@@ -826,7 +826,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
int pageCount = 0;
|
||||
boolean next = true;
|
||||
List<UserSessionModel> result = new ArrayList<>();
|
||||
String lastSessionId = "00000000-0000-0000-0000-000000000000";
|
||||
String lastSessionId = "";
|
||||
|
||||
while (next) {
|
||||
List<UserSessionModel> sess = persister
|
||||
|
||||
Reference in New Issue
Block a user