diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java index 3e20e5ffd8..6cd5bf32a1 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java @@ -28,8 +28,11 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel; +import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER; +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; /** * NOTE: Calling setter doesn't automatically enlist for update @@ -282,12 +285,49 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel @Override public UserModel getAuthenticatedUser() { - return entity.getAuthUserId() == null ? null : session.users().getUserById(getRealm(), entity.getAuthUserId()); } + if (entity.getAuthUserId() == null) { + return null; + } + + if (getUserSessionNotes().containsKey(SESSION_NOTE_LIGHTWEIGHT_USER)) { + LightweightUserAdapter cachedUser = session.getAttribute("authSession.user." + parent.getId(), LightweightUserAdapter.class); + + if (cachedUser != null) { + return cachedUser; + } + + LightweightUserAdapter lua = LightweightUserAdapter.fromString(session, parent.getRealm(), getUserSessionNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER)); + session.setAttribute("authSession.user." + parent.getId(), lua); + lua.setUpdateHandler(lua1 -> { + if (lua == lua1) { // Ensure there is no conflicting user model, only the latest lightweight user can be used + setUserSessionNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua1.serialize()); + } + }); + + return lua; + } else { + return session.users().getUserById(getRealm(), entity.getAuthUserId()); + } + } @Override public void setAuthenticatedUser(UserModel user) { - if (user == null) entity.setAuthUserId(null); - else entity.setAuthUserId(user.getId()); + if (user == null) { + entity.setAuthUserId(null); + setUserSessionNote(SESSION_NOTE_LIGHTWEIGHT_USER, null); + } else { + entity.setAuthUserId(user.getId()); + + if (isLightweightUser(user)) { + LightweightUserAdapter lua = (LightweightUserAdapter) user; + setUserSessionNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua.serialize()); + lua.setUpdateHandler(lua1 -> { + if (lua == lua1) { // Ensure there is no conflicting user model, only the latest lightweight user can be used + setUserSessionNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua1.serialize()); + } + }); + } + } update(); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index d35a19b3f6..f1417da47d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -53,13 +53,13 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent; import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; -import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; import org.keycloak.models.sessions.infinispan.util.FuturesHelper; import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; import org.keycloak.connections.infinispan.InfinispanUtil; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; import java.io.Serializable; @@ -83,6 +83,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER; import static org.keycloak.utils.StreamsUtil.paginatedStream; /** @@ -231,7 +232,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { SessionUpdateTask createSessionTask = Tasks.addIfAbsentSync(); sessionTx.addTask(id, createSessionTask, entity, persistenceState); - UserSessionAdapter adapter = wrap(realm, entity, false); + UserSessionAdapter adapter = user instanceof LightweightUserAdapter + ? wrap(realm, entity, false, user) + : wrap(realm, entity, false); adapter.setPersistenceState(persistenceState); return adapter; } @@ -736,7 +739,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); } - UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline, UserModel user) { InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); @@ -744,12 +747,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return null; } - UserModel user = session.users().getUserById(realm, entity.getUser()); + return new UserSessionAdapter(session, user, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline); + } + + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { + UserModel user = null; + if (entity.getNotes().containsKey(SESSION_NOTE_LIGHTWEIGHT_USER)) { + LightweightUserAdapter lua = LightweightUserAdapter.fromString(session, realm, entity.getNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER)); + final UserSessionAdapter us = wrap(realm, entity, offline, lua); + lua.setUpdateHandler(lua1 -> { + if (lua == lua1) { // Ensure there is no conflicting user model, only the latest lightweight user can be used + us.setNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua1.serialize()); + } + }); + return us; + } + + user = session.users().getUserById(realm, entity.getUser()); + if (user == null) { return null; } - return new UserSessionAdapter(session, user, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline); + return wrap(realm, entity, offline, user); } AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index d0714f1435..c54646115e 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -152,4 +152,6 @@ public final class Constants { public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE; public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE = "keycloak.username-search.case-sensitive"; + public static final String SESSION_NOTE_LIGHTWEIGHT_USER = "keycloak.userModel"; + } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 8ffd6674c5..e0c5acecd5 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -41,6 +41,7 @@ import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AuthDetails; import org.keycloak.models.*; import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.account.CredentialMetadataRepresentation; import org.keycloak.representations.idm.*; @@ -63,6 +64,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; /** * @author Bill Burke @@ -275,7 +277,7 @@ public class ModelToRepresentation { rep.setDisableableCredentialTypes(user.credentialManager() .getDisableableCredentialTypesStream().collect(Collectors.toSet())); rep.setFederationLink(user.getFederationLink()); - rep.setNotBefore(session.users().getNotBeforeOfUser(realm, user)); + rep.setNotBefore(isLightweightUser(user) ? ((LightweightUserAdapter) user).getCreatedTimestamp().intValue() : session.users().getNotBeforeOfUser(realm, user)); rep.setRequiredActions(user.getRequiredActionsStream().collect(Collectors.toList())); Map> attributes = user.getAttributes(); diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java index 0dad953f41..648652321d 100755 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -39,6 +39,7 @@ public class IdentityProviderModel implements Serializable { public static final String FILTERED_BY_CLAIMS = "filteredByClaim"; public static final String CLAIM_FILTER_NAME = "claimFilterName"; public static final String CLAIM_FILTER_VALUE = "claimFilterValue"; + public static final String DO_NOT_STORE_USERS = "doNotStoreUsers"; private String internalId; @@ -259,6 +260,23 @@ public class IdentityProviderModel implements Serializable { getConfig().put(HIDE_ON_LOGIN, String.valueOf(hideOnLogin)); } + /** + * Returns flag whether the users withing this IdP should be transient, ie. not stored in Keycloak database. + * Default value: {@code false}. + * @return + */ + public boolean isTransientUsers() { + return Boolean.valueOf(getConfig().get(DO_NOT_STORE_USERS)); + } + + /** + * Configures the IdP to not store users in Keycloak database. Default value: {@code false}. + * @return + */ + public void setTransientUsers(boolean transientUsers) { + getConfig().put(DO_NOT_STORE_USERS, String.valueOf(transientUsers)); + } + public boolean isFilteredByClaims() { return Boolean.valueOf(getConfig().getOrDefault(FILTERED_BY_CLAIMS, Boolean.toString(false))); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index dc37029e53..022e65d3b8 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -71,6 +71,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification; /** @@ -504,6 +505,9 @@ public class AuthenticationProcessor { @Override public void attachUserSession(UserSessionModel userSession) { AuthenticationProcessor.this.userSession = userSession; + if (isLightweightUser(userSession.getUser())) { + AuthenticationProcessor.this.authenticationSession.setAuthenticatedUser(userSession.getUser()); + } } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java index bd8dce9dcd..9a41641edd 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java @@ -28,12 +28,14 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.services.ServicesLogger; import org.keycloak.services.messages.Messages; import jakarta.ws.rs.core.Response; import java.util.List; import java.util.Map; +import java.util.UUID; /** * @author Marek Posolda @@ -66,13 +68,23 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator return; } - ExistingUserInfo duplication = checkExistingUser(context, username, serializedCtx, brokerContext); + ExistingUserInfo duplication = brokerContext.getIdpConfig().isTransientUsers() ? null : checkExistingUser(context, username, serializedCtx, brokerContext); - if (duplication == null) { + UserModel federatedUser = null; + if (brokerContext.getIdpConfig().isTransientUsers()) { + logger.debugf("Transient brokering requested. Recording user details for account '%s' and from identity provider '%s' .", + username, brokerContext.getIdpConfig().getAlias()); + + federatedUser = new LightweightUserAdapter(session, brokerContext.getBrokerSessionId()); + federatedUser.setUsername(username); + } else if (duplication == null) { logger.debugf("No duplication detected. Creating account for user '%s' and linking with identity provider '%s' .", username, brokerContext.getIdpConfig().getAlias()); - UserModel federatedUser = session.users().addUser(realm, username); + federatedUser = session.users().addUser(realm, username); + } + + if (federatedUser != null) { federatedUser.setEnabled(true); for (Map.Entry> attr : serializedCtx.getAttributes().entrySet()) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java index 5d439d2699..54d4459e5b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java @@ -45,6 +45,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint.TokenExchangeSamlProtocol; @@ -85,6 +86,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import java.util.UUID; /** * Default token exchange implementation @@ -535,12 +537,15 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { target.preprocessFederatedIdentity(session, realm, mapper, context); } - FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(), - context.getUsername(), context.getToken()); + UserModel user = null; + if (! context.getIdpConfig().isTransientUsers()) { + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(), + context.getUsername(), context.getToken()); - UserModel user = this.session.users().getUserByFederatedIdentity(realm, federatedIdentityModel); + user = this.session.users().getUserByFederatedIdentity(realm, federatedIdentityModel); + } - if (user == null) { + if (user == null || context.getIdpConfig().isTransientUsers()) { logger.debugf("Federated user not found for provider '%s' and broker username '%s'.", providerId, context.getUsername()); @@ -570,17 +575,22 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST); } - - user = session.users().addUser(realm, username); + if (context.getIdpConfig().isTransientUsers()) { + user = new LightweightUserAdapter(session, UUID.randomUUID().toString()); + } else { + user = session.users().addUser(realm, username); + } user.setEnabled(true); user.setEmail(context.getEmail()); user.setFirstName(context.getFirstName()); user.setLastName(context.getLastName()); - federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(), - context.getUsername(), context.getToken()); - session.users().addFederatedIdentity(realm, user, federatedIdentityModel); + if (! context.getIdpConfig().isTransientUsers()) { + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(), + context.getUsername(), context.getToken()); + session.users().addFederatedIdentity(realm, user, federatedIdentityModel); + } context.getIdp().importNewUser(session, realm, user, context); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index f9b1ae72c9..781c07b3a5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -56,6 +56,7 @@ import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.SessionExpirationUtils; import org.keycloak.models.utils.RoleUtils; @@ -112,6 +113,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; import static org.keycloak.representations.IDToken.NONCE; import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification; @@ -1234,8 +1236,11 @@ public class TokenManager { int notBefore = realm.getNotBefore(); if (client.getNotBefore() > notBefore) notBefore = client.getNotBefore(); - int userNotBefore = session.users().getNotBeforeOfUser(realm, userSession.getUser()); - if (userNotBefore > notBefore) notBefore = userNotBefore; + final UserModel user = userSession.getUser(); + if (! isLightweightUser(user)) { + int userNotBefore = session.users().getNotBeforeOfUser(realm, user); + if (userNotBefore > notBefore) notBefore = userNotBefore; + } res.setNotBeforePolicy(notBefore); res = transformAccessTokenResponse(session, res, userSession, clientSessionCtx); @@ -1307,7 +1312,9 @@ public class TokenManager { } public static NotBeforeCheck forModel(KeycloakSession session, RealmModel realmModel, UserModel userModel) { - return new NotBeforeCheck(session.users().getNotBeforeOfUser(realmModel, userModel)); + return isLightweightUser(userModel) + ? new NotBeforeCheck(((LightweightUserAdapter) userModel).getCreatedTimestamp().intValue()) + : new NotBeforeCheck(session.users().getNotBeforeOfUser(realmModel, userModel)); } } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 91a7569c98..c4da75fd8b 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -111,6 +111,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue; +import static org.keycloak.models.light.LightweightUserAdapter.isLightweightUser; import static org.keycloak.models.UserSessionModel.CORRESPONDING_SESSION_ID; import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow; import static org.keycloak.services.util.CookieHelper.getCookie; @@ -1542,10 +1543,12 @@ public class AuthenticationManager { return false; } - int userNotBefore = session.users().getNotBeforeOfUser(realm, user); - if (token.getIssuedAt() < userNotBefore) { - logger.debug("User notBefore newer than token"); - return false; + if (! isLightweightUser(user)) { + int userNotBefore = session.users().getNotBeforeOfUser(realm, user); + if (token.getIssuedAt() < userNotBefore) { + logger.debug("User notBefore newer than token"); + return false; + } } return true; diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index bd58cdf800..9bd8d334de 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -60,6 +60,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.light.LightweightUserAdapter; import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; @@ -686,9 +687,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } // Add federated identity link here - FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(), - context.getUsername(), context.getToken()); - session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel); + if (! (federatedUser instanceof LightweightUserAdapter)) { + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(), + context.getUsername(), context.getToken()); + session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel); + } String isRegisteredNewUser = authSession.getAuthNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);