Skip to content

Add support for more update attribute user events#51017

Open
ksushant881 wants to merge 3 commits into
keycloak:mainfrom
ksushant881:add-support-for-user-events
Open

Add support for more update attribute user events#51017
ksushant881 wants to merge 3 commits into
keycloak:mainfrom
ksushant881:add-support-for-user-events

Conversation

@ksushant881

Copy link
Copy Markdown
Contributor

Closes #50453

Copilot AI review requested due to automatic review settings July 19, 2026 14:22
@ksushant881
ksushant881 requested a review from a team as a code owner July 19, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds workflow triggers for user credential, email, profile, and consent updates.

Changes:

  • Adds five workflow event providers with optional filters.
  • Registers the providers through Java SPI.
  • Adds integration coverage for trigger activation.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
WorkflowProviderEventsTest.java Tests new triggers.
WorkflowEventProviderFactory service file Registers providers.
UserUpdateProfileWorkflowEventProvider.java Handles profile updates.
UserUpdateProfileWorkflowEventFactory.java Defines profile trigger.
UserUpdateEmailWorkflowEventProvider.java Handles email updates.
UserUpdateEmailWorkflowEventFactory.java Defines email trigger.
UserUpdateCredentialWorkflowEventProvider.java Handles credential updates.
UserUpdateCredentialWorkflowEventFactory.java Defines credential-update trigger.
UserUpdateConsentWorkflowEventProvider.java Handles consent updates.
UserUpdateConsentWorkflowEventFactory.java Defines consent trigger.
UserRemoveCredentialWorkflowEventProvider.java Handles credential removal.
UserRemoveCredentialWorkflowEventFactory.java Defines credential-removal trigger.


@Override
public boolean supports(Event event) {
return EventType.UPDATE_CREDENTIAL.equals(event.getType()) || EventType.RESET_PASSWORD.equals(event.getType());
Comment on lines +46 to +47
return RESET_PASSWORD_PATH.matcher(path).matches()
|| (CREDENTIALS_PATH.matcher(path).matches() && !OperationType.DELETE.equals(adminEvent.getOperationType()));
if (!org.keycloak.events.admin.ResourceType.USER.equals(adminEvent.getResourceType())) {
return false;
}
if (!OperationType.DELETE.equals(adminEvent.getOperationType())) {
Comment on lines +52 to +53
UserRepresentation rep = JsonSerialization.readValue(representation, UserRepresentation.class);
return rep != null && rep.getEmail() != null;

@Override
public boolean supports(Event event) {
return EventType.UPDATE_CONSENT.equals(event.getType()) || EventType.GRANT_CONSENT.equals(event.getType());
Comment on lines +10 to +14
org.keycloak.models.workflow.events.UserUpdateCredentialWorkflowEventFactory
org.keycloak.models.workflow.events.UserRemoveCredentialWorkflowEventFactory
org.keycloak.models.workflow.events.UserUpdateEmailWorkflowEventFactory
org.keycloak.models.workflow.events.UserUpdateProfileWorkflowEventFactory
org.keycloak.models.workflow.events.UserUpdateConsentWorkflowEventFactory
Comment on lines +677 to +679
org.keycloak.events.Event event = eventBuilder.event(org.keycloak.events.EventType.RESET_PASSWORD)
.user(userId)
.getEvent();
Comment on lines +107 to +110
if ("email".equalsIgnoreCase(configParameter)) {
return rep.getEmail() != null;
} else if ("firstName".equalsIgnoreCase(configParameter) || "first_name".equalsIgnoreCase(configParameter)) {
return rep.getFirstName() != null;
Comment on lines +32 to +41
public boolean supports(AdminEvent adminEvent) {
if (!org.keycloak.events.admin.ResourceType.USER.equals(adminEvent.getResourceType())) {
return false;
}
String path = adminEvent.getResourcePath();
if (path == null) {
return false;
}
return CONSENTS_PATH.matcher(path).matches();
}
Copilot AI review requested due to automatic review settings July 22, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (7)

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateCredentialWorkflowEventProvider.java:35

  • RESET_PASSWORD represents the password-reset flow, not a completed credential update; the actual update path emits UPDATE_CREDENTIAL. Including it here can run update-credential workflows when a reset is merely initiated.
        return EventType.UPDATE_CREDENTIAL.equals(event.getType()) || EventType.RESET_PASSWORD.equals(event.getType());

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateCredentialWorkflowEventProvider.java:48

  • The admin credential-removal endpoint emits OperationType.ACTION, so this condition classifies a removal as an update. Require the update operation explicitly to keep user-update-credential from firing on removals.
        return RESET_PASSWORD_PATH.matcher(path).matches() 
            || (CREDENTIALS_PATH.matcher(path).matches() && !OperationType.DELETE.equals(adminEvent.getOperationType()));

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserRemoveCredentialWorkflowEventProvider.java:42

  • The actual admin removal endpoint records this operation as ACTION (services/.../UserResource.java:890), so admin credential removals never activate this provider. Match the operation emitted by that endpoint.
        if (!OperationType.DELETE.equals(adminEvent.getOperationType())) {

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateEmailWorkflowEventProvider.java:54

  • A non-null email only means the submitted user representation contains an email, not that it changed. Admin clients commonly submit full representations, so unrelated user updates will incorrectly activate user-update-email; the producer/provider contract needs actual before/after change data.
                return rep != null && rep.getEmail() != null;

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateProfileWorkflowEventProvider.java:104

  • This checks whether the requested attribute was present in the admin representation, not whether it was updated. A full user representation therefore activates filtered profile workflows for unchanged attributes, contrary to the attribute-update filter contract.
                UserRepresentation rep = JsonSerialization.readValue(representation, UserRepresentation.class);

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateConsentWorkflowEventProvider.java:29

  • user-update-consent also fires for GRANT_CONSENT, although issue #50453 requests the distinct Update consent event. A new consent grant should not execute workflows configured for updates.
        return EventType.UPDATE_CONSENT.equals(event.getType()) || EventType.GRANT_CONSENT.equals(event.getType());

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateConsentWorkflowEventProvider.java:41

  • The only matching admin mutation is consent revocation (UserResource.java:644-666), so this makes a user-update-consent workflow run for a revoke rather than an update. Do not claim these admin events unless a genuine admin consent-update operation is available.
        return CONSENTS_PATH.matcher(path).matches();

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Comment on lines +36 to +60
public boolean supports(AdminEvent adminEvent) {
if (!org.keycloak.events.admin.ResourceType.USER.equals(adminEvent.getResourceType())) {
return false;
}
if (!OperationType.UPDATE.equals(adminEvent.getOperationType())) {
return false;
}
String path = adminEvent.getResourcePath();
if (path == null) {
return false;
}
if (!USER_PATH.matcher(path).matches()) {
return false;
}
String representation = adminEvent.getRepresentation();
if (representation != null && !representation.isBlank()) {
try {
UserRepresentation rep = JsonSerialization.readValue(representation, UserRepresentation.class);
return rep != null && rep.getEmail() != null;
} catch (Exception e) {
// ignore and fallback to true
}
}
return true;
}
Comment on lines +98 to +107
String representation = adminEvent.getRepresentation();
if (representation == null || representation.isBlank()) {
// If representation is not enabled, we cannot filter by attribute, so we fallback to true
return true;
}
try {
UserRepresentation rep = JsonSerialization.readValue(representation, UserRepresentation.class);
if (rep == null) {
return true;
}
Comment on lines +117 to +119
} catch (Exception e) {
return true;
}
Comment on lines +88 to +96
if ("email".equalsIgnoreCase(configParameter)) {
return event.getDetails().containsKey(Details.UPDATED_EMAIL);
} else if ("firstName".equalsIgnoreCase(configParameter) || "first_name".equalsIgnoreCase(configParameter)) {
return event.getDetails().containsKey("updated_first_name");
} else if ("lastName".equalsIgnoreCase(configParameter) || "last_name".equalsIgnoreCase(configParameter)) {
return event.getDetails().containsKey("updated_last_name");
} else {
return event.getDetails().containsKey("updated_" + configParameter);
}
Comment on lines +112 to +123
String userId = credsMatcher.group(1);
String credentialId = credsMatcher.group(2);
RealmModel realm = session.realms().getRealm(adminEvent.getRealmId());
if (realm != null) {
UserModel user = session.users().getUserById(realm, userId);
if (user != null) {
org.keycloak.credential.CredentialModel stored = user.credentialManager().getStoredCredentialById(credentialId);
if (stored != null) {
return stored.getType();
}
}
}
Comment on lines +650 to +660
// 1. Create workflows for each of the new triggers
// update-credential with optional type password
String wCredentialId = createSimpleWorkflow("w-credential", "user-update-credential(password)");
// remove-credential
String wRemoveCredentialId = createSimpleWorkflow("w-remove-credential", "user-remove-credential");
// update-email
String wUpdateEmailId = createSimpleWorkflow("w-update-email", "user-update-email");
// update-profile with optional type firstName
String wUpdateProfileId = createSimpleWorkflow("w-update-profile", "user-update-profile(firstName)");
// update-consent
String wUpdateConsentId = createSimpleWorkflow("w-update-consent", "user-update-consent");
Closes keycloak#50453

Signed-off-by: ksushant881 <ksushant881@gmail.com>
Closes keycloak#50453

Signed-off-by: ksushant881 <ksushant881@gmail.com>
Copilot AI review requested due to automatic review settings July 22, 2026 19:14
@ksushant881
ksushant881 force-pushed the add-support-for-user-events branch from 7d2068d to b6ae639 Compare July 22, 2026 19:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateCredentialWorkflowEventProvider.java:35

  • RESET_PASSWORD is a distinct event used by the forgot-credentials flow, not the requested UPDATE_CREDENTIAL event, so this activates the workflow for password-reset events that are not credential updates. Restrict user events to UPDATE_CREDENTIAL; the test must emit that event with Details.CREDENTIAL_TYPE=password.
        return EventType.UPDATE_CREDENTIAL.equals(event.getType()) || EventType.RESET_PASSWORD.equals(event.getType());

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateCredentialWorkflowEventProvider.java:48

  • The admin credential-removal endpoint uses this exact path with OperationType.ACTION (UserResource.java:871-894), so the !DELETE check classifies removals as credential updates. Only accept an actual UPDATE operation for this branch.
        return RESET_PASSWORD_PATH.matcher(path).matches() 
            || (CREDENTIALS_PATH.matcher(path).matches() && !OperationType.DELETE.equals(adminEvent.getOperationType()));

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserRemoveCredentialWorkflowEventProvider.java:43

  • Admin credential removal is emitted as OperationType.ACTION, not DELETE (UserResource.java:890), so real admin removals never reach this workflow provider. Match the operation used by that endpoint.
        if (!OperationType.DELETE.equals(adminEvent.getOperationType())) {
            return false;

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateConsentWorkflowEventProvider.java:29

  • GRANT_CONSENT is a separate event for creating consent, while this trigger is specifically for the requested UPDATE_CONSENT event. Including it causes user-update-consent workflows to run on initial grants as well as updates.
        return EventType.UPDATE_CONSENT.equals(event.getType()) || EventType.GRANT_CONSENT.equals(event.getType());

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateEmailWorkflowEventProvider.java:54

  • Presence of email in an admin PUT representation does not mean the email changed; clients commonly submit a full UserRepresentation, so an unrelated profile update will activate user-update-email. Record or compare the old email when handling the admin update and filter on an actual change.
        String representation = adminEvent.getRepresentation();
        if (representation != null && !representation.isBlank()) {
            try {
                UserRepresentation rep = JsonSerialization.readValue(representation, UserRepresentation.class);
                return rep != null && rep.getEmail() != null;

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateProfileWorkflowEventProvider.java:112

  • An admin event representation contains submitted values, not the set of changed attributes. With a normal full user PUT, non-null firstName/email fields make these filters match even when only another attribute changed; capture changed-field details before the update and evaluate those instead.
                if ("email".equalsIgnoreCase(configParameter)) {
                    return rep.getEmail() != null;
                } else if ("firstName".equalsIgnoreCase(configParameter) || "first_name".equalsIgnoreCase(configParameter)) {
                    return rep.getFirstName() != null;
                } else if ("lastName".equalsIgnoreCase(configParameter) || "last_name".equalsIgnoreCase(configParameter)) {

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.forms.MultipleTabsLoginTest#testEmptyBaseUrl

Keycloak CI - Forms IT (firefox)

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

Closes keycloak#50453

Signed-off-by: ksushant881 <ksushant881@gmail.com>
Copilot AI review requested due to automatic review settings July 22, 2026 20:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

model/jpa/src/main/java/org/keycloak/models/workflow/events/UserUpdateConsentWorkflowEventProvider.java:41

  • This admin-event matcher classifies consent revocation as user-update-consent: the only matching admin endpoint is DELETE users/{id}/consents/{client}, which emits ACTION, while the equivalent account operation emits REVOKE_GRANT. Restrict this provider to actual consent updates (or omit admin support until such an operation exists).
        return CONSENTS_PATH.matcher(path).matches();

adminEvent.detail("updated_last_name", rep.getLastName());
}
for (Map.Entry<String, String> attrEntry : changedAttributes.entrySet()) {
adminEvent.detail("updated_" + attrEntry.getKey(), attrEntry.getValue());
adminEvent.detail("updated_last_name", rep.getLastName());
}
for (Map.Entry<String, String> attrEntry : changedAttributes.entrySet()) {
adminEvent.detail("updated_" + attrEntry.getKey(), attrEntry.getValue());
if (path == null) {
return false;
}
return USER_PATH.matcher(path).matches();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workflow] Add support for "Update [...]" user events

2 participants