Support for transient sessions via lightweight users
Part-of: Add support for not importing brokered user into Keycloak database Closes: #11334
This commit is contained in:
parent
1ec2a97f92
commit
26328a7c1e
11 changed files with 152 additions and 31 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<UserSessionEntity> 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<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> 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) {
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -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<String, List<String>> attributes = user.getAttributes();
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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<String, List<String>> attr : serializedCtx.getAttributes().entrySet()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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(),
|
||||
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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,11 +1543,13 @@ public class AuthenticationManager {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue