Deprecate UserSessionCrossDCManager

Fixes #31878

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
This commit is contained in:
Pedro Ruivo 2024-08-02 21:36:09 +01:00 committed by Alexander Schwartz
parent 33776ad8ed
commit 4675a4eda9
11 changed files with 136 additions and 115 deletions

View file

@ -125,6 +125,11 @@ It used to be difficult to regain access to a {project_name} instance when all a
Consequently, the environment variables `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` have been deprecated. You should use `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` instead. These are also general options, so they may be specified via the cli or other config sources, for example `--bootstrap-admin-username=admin`. For more information, see the new https://www.keycloak.org/server/bootstrap-admin-recovery[Bootstrap admin and recovery] guide. Consequently, the environment variables `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` have been deprecated. You should use `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` instead. These are also general options, so they may be specified via the cli or other config sources, for example `--bootstrap-admin-username=admin`. For more information, see the new https://www.keycloak.org/server/bootstrap-admin-recovery[Bootstrap admin and recovery] guide.
= Deprecations in `keycloak-services` module
The class `UserSessionCrossDCManager` is deprecated and planned to be removed in a future version of {project_name}.
Read the `UserSessionCrossDCManager` Javadoc for the alternative methods to use.
= Identity Providers no longer available from the realm representation = Identity Providers no longer available from the realm representation
As part of the improvements around the scalability of realms and organizations when they have many identity providers, the realm representation As part of the improvements around the scalability of realms and organizations when they have many identity providers, the realm representation

View file

@ -17,15 +17,14 @@
package org.keycloak.models; package org.keycloak.models;
import org.keycloak.migration.MigrationModel;
import org.keycloak.provider.Provider;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.keycloak.provider.Provider;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -212,4 +211,21 @@ public interface UserSessionProvider extends Provider {
default void migrate(String modelVersion) { default void migrate(String modelVersion) {
} }
/**
* Returns the {@link UserSessionModel} if the user session with ID {@code userSessionId} exist, and it has an
* {@link AuthenticatedClientSessionModel} from a {@link ClientModel} with ID {@code clientUUID}.
* <p>
* If the {@link AuthenticatedClientSessionModel} from the client or the {@link UserSessionModel} does not exist,
* this method returns {@code null}.
*
* @param realm The {@link RealmModel} where the session belongs to.
* @param userSessionId The ID of the {@link UserSessionModel}.
* @param offline If {@code true}, it fetches an offline session and, if {@code false}, an online session.
* @param clientUUID The {@link ClientModel#getId()}.
* @return The {@link UserSessionModel} if it has a session from the {@code clientUUID}.
*/
default UserSessionModel getUserSessionIfClientExists(RealmModel realm, String userSessionId, boolean offline, String clientUUID) {
return getUserSessionWithPredicate(realm, userSessionId, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null);
}
} }

View file

@ -38,7 +38,6 @@ import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.ErrorPageException; import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
@ -114,9 +113,7 @@ public abstract class AuthorizationEndpointBase {
// We cancel login if any authentication action or required action is required // We cancel login if any authentication action or required action is required
try { try {
Response challenge = processor.authenticateOnly(); Response challenge = processor.authenticateOnly();
if (challenge == null) { if (challenge != null) {
// nothing to do - user is already authenticated;
} else {
// KEYCLOAK-8043: forward the request with prompt=none to the default provider. // KEYCLOAK-8043: forward the request with prompt=none to the default provider.
if ("true".equals(authSession.getAuthNote(AuthenticationProcessor.FORWARDED_PASSIVE_LOGIN))) { if ("true".equals(authSession.getAuthNote(AuthenticationProcessor.FORWARDED_PASSIVE_LOGIN))) {
RestartLoginCookie.setRestartCookie(session, authSession); RestartLoginCookie.setRestartCookie(session, authSession);
@ -127,16 +124,14 @@ public abstract class AuthorizationEndpointBase {
return challenge; return challenge;
} }
else { else {
Response response = protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED); return protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED);
return response;
} }
} }
AuthenticationManager.setClientScopesInSession(session, authSession); AuthenticationManager.setClientScopesInSession(session, authSession);
if (processor.nextRequiredAction() != null) { if (processor.nextRequiredAction() != null) {
Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED); return protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED);
return response;
} }
} catch (Exception e) { } catch (Exception e) {
@ -186,8 +181,7 @@ public abstract class AuthorizationEndpointBase {
logger.debugf("Sent request to authz endpoint. Root authentication session with ID '%s' exists. Client is '%s' . Created new authentication session with tab ID: %s", logger.debugf("Sent request to authz endpoint. Root authentication session with ID '%s' exists. Client is '%s' . Created new authentication session with tab ID: %s",
rootAuthSession.getId(), client.getClientId(), authSession.getTabId()); rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
} else { } else {
UserSessionCrossDCManager userSessionCrossDCManager = new UserSessionCrossDCManager(session); UserSessionModel userSession = manager.getUserSessionFromAuthenticationCookie(realm);
UserSessionModel userSession = userSessionCrossDCManager.getUserSessionIfExistsRemotely(manager, realm);
if (userSession != null) { if (userSession != null) {
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();

View file

@ -86,7 +86,6 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.util.AuthorizationContextUtil; import org.keycloak.services.util.AuthorizationContextUtil;
@ -193,7 +192,7 @@ public class TokenManager {
// Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache // Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache
if (clientSession == null) { if (clientSession == null) {
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId()); userSession = session.sessions().getUserSessionIfClientExists(realm, userSession.getId(), offline, client.getId());
if (userSession != null) { if (userSession != null) {
clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId()); clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
} else { } else {

View file

@ -28,8 +28,6 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.keycloak.http.HttpRequest;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
@ -38,6 +36,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.headers.SecurityHeadersProvider; import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -53,7 +52,6 @@ import org.keycloak.services.clientpolicy.context.TokenRevokeContext;
import org.keycloak.services.clientpolicy.context.TokenRevokeResponseContext; import org.keycloak.services.clientpolicy.context.TokenRevokeResponseContext;
import org.keycloak.services.cors.Cors; import org.keycloak.services.cors.Cors;
import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
@ -212,12 +210,11 @@ public class TokenRevocationEndpoint {
if (token.getSessionState() == null) { if (token.getSessionState() == null) {
user = TokenManager.lookupUserFromStatelessToken(session, realm, token); user = TokenManager.lookupUserFromStatelessToken(session, realm, token);
} else { } else {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, var userSessionProvider = session.sessions();
token.getSessionState(), false, client.getId()); UserSessionModel userSession = userSessionProvider.getUserSessionIfClientExists(realm, token.getSessionId(), false, client.getId());
if (userSession == null) { if (userSession == null) {
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, userSession = userSessionProvider.getUserSessionIfClientExists(realm, token.getSessionId(), true, client.getId());
client.getId());
if (userSession == null) { if (userSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND); event.error(Errors.USER_SESSION_NOT_FOUND);

View file

@ -17,15 +17,18 @@
package org.keycloak.protocol.oidc.grants.device; package org.keycloak.protocol.oidc.grants.device;
import static org.keycloak.protocol.oidc.OIDCLoginProtocolService.tokenServiceBaseUrl; import java.net.URI;
import java.util.Map;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
@ -38,13 +41,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.oidc.grants.OAuth2GrantType;
import org.keycloak.protocol.oidc.grants.OAuth2GrantTypeBase; import org.keycloak.protocol.oidc.grants.OAuth2GrantTypeBase;
import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenRequestContext; import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenRequestContext;
import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenResponseContext; import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenResponseContext;
@ -54,17 +55,11 @@ import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientpolicy.ClientPolicyException; import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import jakarta.ws.rs.core.Response; import static org.keycloak.protocol.oidc.OIDCLoginProtocolService.tokenServiceBaseUrl;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Map;
/** /**
* OAuth 2.0 Device Authorization Grant * OAuth 2.0 Device Authorization Grant
@ -279,11 +274,11 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
event.session(userSessionId); event.session(userSessionId);
// Retrieve UserSession // Retrieve UserSession
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, var userSessionProvider = session.sessions();
client.getId()); UserSessionModel userSession = userSessionProvider.getUserSessionIfClientExists(realm, userSessionId, false, client.getId());
if (userSession == null) { if (userSession == null) {
userSession = session.sessions().getUserSession(realm, userSessionId); userSession = userSessionProvider.getUserSession(realm, userSessionId);
if (userSession == null) { if (userSession == null) {
throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING, throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING,
"The authorization request is verified but can not lookup the user session yet", "The authorization request is verified but can not lookup the user session yet",

View file

@ -29,7 +29,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.UserSessionCrossDCManager;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -99,10 +98,11 @@ public class OAuth2CodeParser {
} }
// Retrieve UserSession // Retrieve UserSession
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, clientUUID); var userSessionProvider = session.sessions();
UserSessionModel userSession = userSessionProvider.getUserSessionIfClientExists(realm, userSessionId, false, clientUUID);
if (userSession == null) { if (userSession == null) {
// Needed to track if code is invalid or was already used. // Needed to track if code is invalid or was already used.
userSession = session.sessions().getUserSession(realm, userSessionId); userSession = userSessionProvider.getUserSession(realm, userSessionId);
if (userSession == null) { if (userSession == null) {
return result.illegalCode(); return result.illegalCode();
} }
@ -150,16 +150,6 @@ public class OAuth2CodeParser {
private boolean isExpiredCode = false; private boolean isExpiredCode = false;
private ParseResult(String code, OAuth2Code codeData, AuthenticatedClientSessionModel clientSession) {
this.code = code;
this.codeData = codeData;
this.clientSession = clientSession;
this.isIllegalCode = false;
this.isExpiredCode = false;
}
private ParseResult(String code) { private ParseResult(String code) {
this.code = code; this.code = code;
} }

View file

@ -27,7 +27,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor; import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.services.managers.UserSessionCrossDCManager;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -60,14 +59,14 @@ public class SamlSessionUtils {
String userSessionId = parts[0]; String userSessionId = parts[0];
String clientUUID = parts[1]; String clientUUID = parts[1];
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId, false, clientUUID); UserSessionModel userSession = session.sessions().getUserSessionIfClientExists(realm, userSessionId, false, clientUUID);
if (userSession == null) { if (userSession == null) {
return null; return null;
} }
return userSession.getAuthenticatedClientSessionByClient(clientUUID); return userSession.getAuthenticatedClientSessionByClient(clientUUID);
} }
public static Iterator<SamlAuthenticationPreprocessor> getSamlAuthenticationPreprocessorIterator(KeycloakSession session) { public static Iterator<SamlAuthenticationPreprocessor> getSamlAuthenticationPreprocessorIterator(KeycloakSession session) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(SamlAuthenticationPreprocessor.class) return session.getKeycloakSessionFactory().getProviderFactoriesStream(SamlAuthenticationPreprocessor.class)
.filter(Objects::nonNull) .filter(Objects::nonNull)

View file

@ -17,6 +17,8 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import java.util.Objects;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieProvider;
@ -26,12 +28,15 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.SessionExpiration; import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.sessions.StickySessionEncoderProvider; import org.keycloak.sessions.StickySessionEncoderProvider;
import static org.keycloak.services.managers.AuthenticationManager.authenticateIdentityCookie;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -233,7 +238,7 @@ public class AuthenticationSessionManager {
// Check to see if we already have authenticationSession with same ID // Check to see if we already have authenticationSession with same ID
public UserSessionModel getUserSession(AuthenticationSessionModel authSession) { public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId()); return getUserSessionProvider().getUserSession(authSession.getRealm(), authSession.getParentSession().getId());
} }
@ -242,4 +247,43 @@ public class AuthenticationSessionManager {
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId); return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId);
} }
public UserSessionModel getUserSessionFromAuthenticationCookie(RealmModel realm) {
String oldEncodedId = getAuthSessionCookies(realm);
if (oldEncodedId == null) {
// ideally, we should not rely on auth session id to retrieve user sessions
// in case the auth session was removed, we fall back to the identity cookie
// we are here doing the user session lookup twice, however the second lookup is going to make sure the
// session exists in remote caches
AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(session, realm, true);
if (authResult != null && authResult.getSession() != null) {
oldEncodedId = authResult.getSession().getId();
} else {
return null;
}
}
AuthSessionId authSessionId = decodeAuthSessionId(oldEncodedId);
String sessionId = authSessionId.getDecodedId();
// TODO: remove this code once InfinispanUserSessionProvider is removed or no longer using any remote caches, as other implementations don't need this call.
// This will remove userSession "locally" if it doesn't exist on remoteCache
var userSessionProvider = getUserSessionProvider();
userSessionProvider.getUserSessionWithPredicate(realm, sessionId, false, Objects::isNull);
UserSessionModel userSession = userSessionProvider.getUserSession(realm, sessionId);
if (userSession != null) {
reencodeAuthSessionCookie(oldEncodedId, authSessionId, realm);
return userSession;
} else {
return null;
}
}
private UserSessionProvider getUserSessionProvider() {
return session.sessions();
}
} }

View file

@ -17,22 +17,20 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.ImpersonationSessionNote; import org.keycloak.services.util.UserSessionUtil;
import static org.keycloak.services.managers.AuthenticationManager.authenticateIdentityCookie;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*
* @deprecated To be removed without replacement. Check the methods documentation for alternatives.
*/ */
@Deprecated(since = "26", forRemoval = true)
public class UserSessionCrossDCManager { public class UserSessionCrossDCManager {
private static final Logger logger = Logger.getLogger(UserSessionCrossDCManager.class);
private final KeycloakSession kcSession; private final KeycloakSession kcSession;
public UserSessionCrossDCManager(KeycloakSession session) { public UserSessionCrossDCManager(KeycloakSession session) {
@ -41,59 +39,43 @@ public class UserSessionCrossDCManager {
// get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
/**
* @deprecated To be removed in Keycloak 27+. Use
* {@link UserSessionProvider#getUserSessionIfClientExists(RealmModel, String, boolean, String)}
*/
@Deprecated(since = "26", forRemoval = true)
public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) { public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) {
return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null); return kcSession.sessions().getUserSessionIfClientExists(realm, id, offline, clientUUID);
} }
/**
* @deprecated To be removed in Keycloak 27+. Use
* {@link UserSessionUtil#getUserSessionWithImpersonatorClient(KeycloakSession, RealmModel, String, boolean, String)}
*/
@Deprecated(since = "26", forRemoval = true)
public UserSessionModel getUserSessionWithImpersonatorClient(RealmModel realm, String id, boolean offline, String clientUUID) { public UserSessionModel getUserSessionWithImpersonatorClient(RealmModel realm, String id, boolean offline, String clientUUID) {
return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> clientUUID.equals(userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_CLIENT.toString()))); return UserSessionUtil.getUserSessionWithImpersonatorClient(kcSession, realm, id, offline, clientUUID);
} }
// get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
// TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead // TODO Probably remove this method once AuthenticatedClientSession.getAction is removed and information is moved to OAuth code JWT instead
/**
* @deprecated To be removed in Keycloak 27+. Use
* {@link UserSessionProvider#getUserSessionIfClientExists(RealmModel, String, boolean, String)}
*/
@Deprecated(since = "26", forRemoval = true)
public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, String clientUUID) { public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, String clientUUID) {
return getUserSessionWithClient(realm, id, false, clientUUID);
return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> {
AuthenticatedClientSessionModel authSessions = userSession.getAuthenticatedClientSessionByClient(clientUUID);
return authSessions != null;
});
} }
// Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified // Just check if userSession also exists on remoteCache. It can happen that logout happened on 2nd DC and userSession is already removed on remoteCache and this DC wasn't yet notified
/**
* @deprecated To be removed in Keycloak 27+. Use
* {@link AuthenticationSessionManager#getUserSessionFromAuthenticationCookie(RealmModel)}
*/
@Deprecated(since = "26", forRemoval = true)
public UserSessionModel getUserSessionIfExistsRemotely(AuthenticationSessionManager asm, RealmModel realm) { public UserSessionModel getUserSessionIfExistsRemotely(AuthenticationSessionManager asm, RealmModel realm) {
String oldEncodedId = asm.getAuthSessionCookies(realm); return asm.getUserSessionFromAuthenticationCookie(realm);
if (oldEncodedId == null) {
// ideally, we should not rely on auth session id to retrieve user sessions
// in case the auth session was removed, we fall back to the identity cookie
// we are here doing the user session lookup twice, however the second lookup is going to make sure the
// session exists in remote caches
AuthenticationManager.AuthResult authResult = authenticateIdentityCookie(kcSession, realm, true);
if (authResult != null && authResult.getSession() != null) {
oldEncodedId = authResult.getSession().getId();
} else {
return null;
}
}
AuthSessionId authSessionId = asm.decodeAuthSessionId(oldEncodedId);
String sessionId = authSessionId.getDecodedId();
// TODO: remove this code once InfinispanUserSessionProvider is removed or no longer using any remote caches, as other implementations don't need this call.
// This will remove userSession "locally" if it doesn't exist on remoteCache
kcSession.sessions().getUserSessionWithPredicate(realm, sessionId, false, (UserSessionModel userSession2) -> userSession2 == null);
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
asm.reencodeAuthSessionCookie(oldEncodedId, authSessionId, realm);
return userSession;
} else {
return null;
}
} }
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.services.util; package org.keycloak.services.util;
import java.util.Objects;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
@ -7,6 +9,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -17,7 +20,6 @@ import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -27,9 +29,6 @@ public class UserSessionUtil {
private static final Logger logger = Logger.getLogger(UserSessionUtil.class); private static final Logger logger = Logger.getLogger(UserSessionUtil.class);
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) { public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) {
OAuth2Error error = new OAuth2Error().json(false).realm(realm); OAuth2Error error = new OAuth2Error().json(false).realm(realm);
return findValidSession(session, realm, token, event, client, error); return findValidSession(session, realm, token, event, client, error);
@ -41,19 +40,20 @@ public class UserSessionUtil {
return createTransientSessionForClient(session, realm, token, client, event); return createTransientSessionForClient(session, realm, token, client, event);
} }
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), false, client.getId()); var userSessionProvider = session.sessions();
UserSessionModel userSession = userSessionProvider.getUserSessionIfClientExists(realm, token.getSessionId(), false, client.getId());
if (userSession == null) { if (userSession == null) {
// also try to resolve sessions created during token exchange when the user is impersonated // also try to resolve sessions created during token exchange when the user is impersonated
userSession = new UserSessionCrossDCManager(session).getUserSessionWithImpersonatorClient(realm, token.getSessionId(), false, client.getId()); userSession = getUserSessionWithImpersonatorClient(session, realm, token.getSessionId(), false, client.getId());
} }
UserSessionModel offlineUserSession = null; UserSessionModel offlineUserSession;
if (AuthenticationManager.isSessionValid(realm, userSession)) { if (AuthenticationManager.isSessionValid(realm, userSession)) {
checkTokenIssuedAt(realm, token, userSession, event, client); checkTokenIssuedAt(realm, token, userSession, event, client);
event.session(userSession); event.session(userSession);
return userSession; return userSession;
} else { } else {
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionId(), true, client.getId()); offlineUserSession = userSessionProvider.getUserSessionIfClientExists(realm, token.getSessionId(), true, client.getId());
if (AuthenticationManager.isSessionValid(realm, offlineUserSession)) { if (AuthenticationManager.isSessionValid(realm, offlineUserSession)) {
checkTokenIssuedAt(realm, token, offlineUserSession, event, client); checkTokenIssuedAt(realm, token, offlineUserSession, event, client);
event.session(offlineUserSession); event.session(offlineUserSession);
@ -67,11 +67,7 @@ public class UserSessionUtil {
throw error.invalidToken("User session not found or doesn't have client attached on it"); throw error.invalidToken("User session not found or doesn't have client attached on it");
} }
if (userSession != null) { event.session(Objects.requireNonNullElse(userSession, offlineUserSession));
event.session(userSession);
} else {
event.session(offlineUserSession);
}
logger.debug("Session expired"); logger.debug("Session expired");
event.error(Errors.SESSION_EXPIRED); event.error(Errors.SESSION_EXPIRED);
@ -116,4 +112,8 @@ public class UserSessionUtil {
throw error.invalidToken("Stale token"); throw error.invalidToken("Stale token");
} }
} }
public static UserSessionModel getUserSessionWithImpersonatorClient(KeycloakSession session, RealmModel realm, String userSessionId, boolean offline, String clientUUID) {
return session.sessions().getUserSessionWithPredicate(realm, userSessionId, offline, userSession -> Objects.equals(clientUUID, userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_CLIENT.toString())));
}
} }