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?

@dvlpphb @dschneider I am in the same situation - trying to set a user attribute from an SPI in KC 16.1, an EventListener in my case. Extension is registered, doesn’t throw any errors but attributes are not set. If you managed to figure this one out I’d appreciate to know how.

Thanks.

Hi @the-dude check out this issue Missing terms_accepted in user attributes · Issue #43 · thomasdarimont/keycloak-extension-playground · GitHub

I hope this help you, keep me update

Seems like setSingleAttribute works fine and persists the attribute with LOGIN event, but not with REGISTER