How to persist a UserModel on EventListenerProvider (Keycloak 16.1)

HI,
I would like to add some attributes to the UserModel in the my custom EventListenerProvider, but they are not saved.

Example code:

public class MyEventListenerProvider implements EventListenerProvider {

    @Override
    public void onEvent(Event event) {
        UserModel user =  determineUserFromEvent(Event event); // --> for example session.userLocalStorage().getUserByUsername(username, getRealm());

        //add some Attributes
        user.setSingleAttribute("attribute1" , "value1");
        user.setSingleAttribute("attribute2" , "value2");

        //How to persist/save these changes to the Database?
        //user.persist/flush/save();
    }
}

How can I save the attributes to the database?

regards
Daniel

Have a look at this:

package ch.hcuge.listener;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang.StringUtils;
import org.jboss.logging.Logger;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import com.fasterxml.jackson.core.JsonProcessingException;

import ch.hcuge.common.UserUtil;

/**
 * Listener KeyCloak for the HUG account.
 * <ul>
 * <li>Keep a 'small' connections history</li>
 * <li>Set phone number masked on some events</li>
 * </ul>
 */
public class HUGListener implements EventListenerProvider {

	public static final String ID = "hug-listener";

	private static final Logger logger = Logger.getLogger(HUGListener.class);

	public static final Set<EventType> USER_EVENT_TYPES_4_LOGIN_HISTORY = Set.of(EventType.REGISTER, EventType.IDENTITY_PROVIDER_FIRST_LOGIN, EventType.LOGIN, EventType.IDENTITY_PROVIDER_LOGIN, EventType.IDENTITY_PROVIDER_POST_LOGIN, EventType.CLIENT_LOGIN);

	public static final Set<EventType> USER_EVENT_TYPES_4_UPDATE_PROFILE = Set.of(EventType.REGISTER, EventType.UPDATE_PROFILE, EventType.IDENTITY_PROVIDER_FIRST_LOGIN, EventType.IDENTITY_PROVIDER_LOGIN, EventType.IDENTITY_PROVIDER_POST_LOGIN);

	protected static Set<EventType> SUPPORTED_USER_EVENT_TYPES = Set.of();

	static {
		SUPPORTED_USER_EVENT_TYPES = Stream.concat(SUPPORTED_USER_EVENT_TYPES.stream(), USER_EVENT_TYPES_4_LOGIN_HISTORY.stream()).collect(Collectors.toSet());
		SUPPORTED_USER_EVENT_TYPES = Stream.concat(SUPPORTED_USER_EVENT_TYPES.stream(), USER_EVENT_TYPES_4_UPDATE_PROFILE.stream()).collect(Collectors.toSet());
	}
	private KeycloakSession session; // not final for mockito

	public HUGListener(KeycloakSession session) {
		this.session = session;
	}

	@Override
	@SuppressWarnings("deprecation")
	public void onEvent(Event event) {

		if (!SUPPORTED_USER_EVENT_TYPES.contains(event.getType())) {
			logger.debugf("Unsupported user event : %s", event.getType());
			onNotSupportedEvent(event); // just to easy tests with mockito
			return;
		}

		if (session.realms() != null && session.users() != null && StringUtils.isNotBlank(event.getRealmId()) && StringUtils.isNotBlank(event.getUserId())) {
			RealmModel realm = session.realms().getRealm(event.getRealmId());
			if (realm != null) {
				UserModel user = session.users().getUserById(event.getUserId(), realm);
				if (user != null) {
					onAnySupportedEvent(event, user);
					if (USER_EVENT_TYPES_4_LOGIN_HISTORY.contains(event.getType())) {
						onLoginEvent(event, user);
					}
					if (USER_EVENT_TYPES_4_UPDATE_PROFILE.contains(event.getType())) {
						onUpdateProfileEvent(event, user);
					}
					try {
						logger.debugf("%s : %s", event.getType(), UserUtil.toJson(user));
					} catch (JsonProcessingException e) {
						// NOOP
					}
				}
			}
		}
	}

	protected void onAnySupportedEvent(Event event, UserModel user) {
		logger.debugf("Supported user event : %s", event.getType());
		// To be implemented if an action has to be always executed for the supported events
	}

	protected void onLoginEvent(Event event, UserModel user) {
		// Set last login and update login history
		UserUtil.setLastLogin(user, DateTimeFormatter.ISO_DATE_TIME.format(OffsetDateTime.now(ZoneOffset.UTC)));
	}

	protected void onUpdateProfileEvent(Event event, UserModel user) {
		UserUtil.setPhoneNumberMasked(user);
	}

	@Override
	public void onEvent(AdminEvent event, boolean includeRepresentation) {
		ResourceType resourceType = event.getResourceType();
		Boolean supportedAdminEvent = Boolean.FALSE;
		if (ResourceType.USER == resourceType) {
			supportedAdminEvent = Boolean.TRUE;
			onAminEvent4UserResource(event);
		}
		if (Boolean.TRUE.equals(supportedAdminEvent)) {
			logger.debugf("Supported admin event :  ressource type : [%s], ressource path : [%s], operation type : [%s]", event.getResourceTypeAsString(), event.getResourcePath(), event.getOperationType());
		} else {
			logger.debugf("Unsupported admin event :  ressource type : [%s], ressource path : [%s], operation type : [%s]", event.getResourceTypeAsString(), event.getResourcePath(), event.getOperationType());
		}
	}

	@SuppressWarnings("deprecation")
	protected void onAminEvent4UserResource(AdminEvent event) {
		RealmModel realm = session.realms().getRealm(event.getRealmId());
		String userId = event.getResourcePath().substring("users/".length());
		UserModel user = session.users().getUserById(userId, realm);
		UserUtil.setPhoneNumberMasked(user);
	}

	@Override
	public void close() {
		// NOOP
	}

	public void onNotSupportedEvent(Event event) {
		// just to easy tests with mockito
	}

}

Especially:

Where called methods of UserUtil is:

Thank you for the answer. But in my case Keycloak does not commit the new attributes to the database. And when I reload the user, the attributes are missing.

That’s very odd, my event listener also just uses the setSingleAttribute ( keycloak-last-login-event-listener/LastLoginEventListenerProvider.java at main · ThoreKr/keycloak-last-login-event-listener · GitHub ).
Are you sure the event listener is registered correctly?

Hi @dschneider i have your same issue on a keycloak 16.1.1

I’m trying to use this spi.
The code runs without errors but Keycloak doesn’t save the user attribute on the user profile.

Have you resolved your issue?