diff --git a/README.md b/README.md index e370680..164be3a 100755 --- a/README.md +++ b/README.md @@ -1,70 +1,97 @@ # keycloak-event-listener-http -A Keycloak SPI that publishes events to an HTTP Webhook. +A KeyCloak SPI that publishes events to an HTTP Webhook. A (largely) adaptation of @mhui mhuin/keycloak-event-listener-mqtt SPI. Extended by @darrensapalo to [enable building the JAR files from docker images](https://sapalo.dev/2021/06/16/send-keycloak-webhook-events/). -# Build +Built for KeyCloak 20 or above. -## Build on your local machine +## Build -``` +### Build on your local machine + +```sh mvn clean install ``` -## Build using docker +### Build using docker Alternatively, you can [build the JAR files from a docker image](https://sapalo.dev/2021/06/16/send-keycloak-webhook-events/). You must have `docker` installed. 1. Run `make package-image`. 2. The JAR files should show up on your `mvn-output` folder. -If you encounter the following issue: +If you encounter the following issue: + ``` open {PATH}/mvn-output/event-listener-http-jar-with-dependencies.jar: permission denied ``` Simply add write permissions to the `mvn-output` folder: -``` +```sh sudo chown $USER:$USER mvn-output ``` -# Deploy +## Deploy + +* Copy `mvn-output/event-listener-http-jar-with-dependencies.jar` to `{KEYCLOAK_HOME}/providers` +* Configure the webhook settings (see below) using the configuration way you would normally use (see https://www.keycloak.org/server/configuration). +* Restart the KeyCloak server. + +## Configuration + +You can set these configuration values for the "SPI Events Listeners" "http" object: + +- `server-uri`: the webhook to call +- `username`: For authentication of the webhook. Leave username and password out if the service allows anonymous access. +- `password`: For authentication of the webhook. Leave username and password out if the service allows anonymous access. +- `topic`: unknown. If unset, the default message topic is "keycloak/events". +- `include-user-events`: A comma separated list of user events you want to get notifications for. Enter `none` of you want to disable notification for user events altogether. +- `exclude-user-events`: A comma separated list of user events you don't want to get notifications for. +- `include-admin-events`: A comma separated list of admin events you want to get notifications for. Enter `none` of you want to disable notification for user events altogether. +- `exclude-admin-events`: A comma separated list of admin events you don't want to get notifications for. +- `admin-event-resource-path-prefixes`: A comma separated list of path prefixes. Allows you to filter resource paths. Will send notifications for paths that begin with these prefixes. +- `include-admin-event-representation`: If true, the HTTP webhook request will include the data of CREATE and UPDATE admin events (username etc.) -* Copy target/event-listener-http-jar-with-dependencies.jar to {KEYCLOAK_HOME}/standalone/deployments -* Edit standalone.xml to configure the Webhook settings. Find the following - section in the configuration: +Example with environment variables: ``` - - auth +KC_SPI_EVENTS_LISTENER_HTTP_SERVER_URI=http://127.0.0.1:8080/webhook +KC_SPI_EVENTS_LISTENER_HTTP_USERNAME=auth_user +KC_SPI_EVENTS_LISTENER_HTTP_PASSWORD=auth_password +KC_SPI_EVENTS_LISTENER_HTTP_TOPIC=my_topic +KC_SPI_EVENTS_LISTENER_HTTP_INCLUDE_USER_EVENTS=none +KC_SPI_EVENTS_LISTENER_HTTP_EXCLUDE_USER_EVENTS=LOGIN +KC_SPI_EVENTS_LISTENER_HTTP_INCLUDE_ADMIN_EVENTS=DELETE +KC_SPI_EVENTS_LISTENER_HTTP_EXCLUDE_ADMIN_EVENTS=ACTION +KC_SPI_EVENTS_LISTENER_HTTP_ADMIN_EVENT_RESOURCE_PATH_PREFIXES=users/ +KC_SPI_EVENTS_LISTENER_HTTP_INCLUDE_ADMIN_EVENT_REPRESENTATION=true ``` -And add below: +Example with option to `kc.sh start`: ``` - - - - - - - - - - +--spi-events-listener-http-server-uri=http://127.0.0.1:8080/webhook +--spi-events-listener-http-exclude-user-events=none +... ``` -Leave username and password out if the service allows anonymous access. -If unset, the default message topic is "keycloak/events". -* Restart the keycloak server. +### Valid user event names -# Use -Add/Update a user, your webhook should be called, looks at the keycloak syslog for debug +LOGIN, LOGIN_ERROR, REGISTER, REGISTER_ERROR, LOGOUT, LOGOUT_ERROR, CODE_TO_TOKEN, CODE_TO_TOKEN_ERROR, CLIENT_LOGIN, CLIENT_LOGIN_ERROR, REFRESH_TOKEN, REFRESH_TOKEN_ERROR, INTROSPECT_TOKEN, INTROSPECT_TOKEN_ERROR,FEDERATED_IDENTITY_LINK, FEDERATED_IDENTITY_LINK_ERROR, REMOVE_FEDERATED_IDENTITY, REMOVE_FEDERATED_IDENTITY_ERROR, UPDATE_EMAIL, UPDATE_EMAIL_ERROR, UPDATE_PROFILE, UPDATE_PROFILE_ERROR, UPDATE_PASSWORD, UPDATE_PASSWORD_ERROR,UPDATE_TOTP, UPDATE_TOTP_ERROR, VERIFY_EMAIL, VERIFY_EMAIL_ERROR, VERIFY_PROFILE, VERIFY_PROFILE_ERROR, REMOVE_TOTP, REMOVE_TOTP_ERROR, GRANT_CONSENT, GRANT_CONSENT_ERROR, UPDATE_CONSENT, UPDATE_CONSENT_ERROR, REVOKE_GRANT,REVOKE_GRANT_ERROR, SEND_VERIFY_EMAIL, SEND_VERIFY_EMAIL_ERROR, SEND_RESET_PASSWORD, SEND_RESET_PASSWORD_ERROR, SEND_IDENTITY_PROVIDER_LINK, SEND_IDENTITY_PROVIDER_LINK_ERROR, RESET_PASSWORD, RESET_PASSWORD_ERROR,RESTART_AUTHENTICATION, RESTART_AUTHENTICATION_ERROR, INVALID_SIGNATURE, INVALID_SIGNATURE_ERROR, REGISTER_NODE, REGISTER_NODE_ERROR, UNREGISTER_NODE, UNREGISTER_NODE_ERROR, USER_INFO_REQUEST, USER_INFO_REQUEST_ERROR,IDENTITY_PROVIDER_LINK_ACCOUNT, IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR, IDENTITY_PROVIDER_LOGIN, IDENTITY_PROVIDER_LOGIN_ERROR, IDENTITY_PROVIDER_FIRST_LOGIN, IDENTITY_PROVIDER_FIRST_LOGIN_ERROR, IDENTITY_PROVIDER_POST_LOGIN,IDENTITY_PROVIDER_POST_LOGIN_ERROR, IDENTITY_PROVIDER_RESPONSE, IDENTITY_PROVIDER_RESPONSE_ERROR, IDENTITY_PROVIDER_RETRIEVE_TOKEN, IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR, IMPERSONATE, IMPERSONATE_ERROR, CUSTOM_REQUIRED_ACTION,CUSTOM_REQUIRED_ACTION_ERROR, EXECUTE_ACTIONS, EXECUTE_ACTIONS_ERROR, EXECUTE_ACTION_TOKEN, EXECUTE_ACTION_TOKEN_ERROR, CLIENT_INFO, CLIENT_INFO_ERROR, CLIENT_REGISTER, CLIENT_REGISTER_ERROR, CLIENT_UPDATE, CLIENT_UPDATE_ERROR,CLIENT_DELETE, CLIENT_DELETE_ERROR, CLIENT_INITIATED_ACCOUNT_LINKING, CLIENT_INITIATED_ACCOUNT_LINKING_ERROR, TOKEN_EXCHANGE, TOKEN_EXCHANGE_ERROR, OAUTH2_DEVICE_AUTH, OAUTH2_DEVICE_AUTH_ERROR, OAUTH2_DEVICE_VERIFY_USER_CODE,OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR, OAUTH2_DEVICE_CODE_TO_TOKEN, OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR, AUTHREQID_TO_TOKEN, AUTHREQID_TO_TOKEN_ERROR, PERMISSION_TOKEN, PERMISSION_TOKEN_ERROR, DELETE_ACCOUNT, DELETE_ACCOUNT_ERROR,PUSHED_AUTHORIZATION_REQUEST, PUSHED_AUTHORIZATION_REQUEST_ERROR + +### Valid admin event names + +CREATE, UPDATE, DELETE, ACTION + +## Use + +When you add or update a user, your webhook should be called. Check the KeyCloak syslog for debugging information. Request example -``` + +```json { "type": "REGISTER", "realmId": "myrealm", @@ -81,4 +108,4 @@ Request example "username": "username" } } -``` \ No newline at end of file +``` diff --git a/pom.xml b/pom.xml index 8d01d3e..10fa12f 100755 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ org.softwarefactory.keycloak.providers.events.http - 5.0.0 + 1.0.0 Keycloak: Event Publisher to HTTP @@ -29,7 +29,7 @@ 14.0.1.Final - ${project.version} + 19.0.3 1.2.2.Final 1.0.2.Final diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java index 14b0cbb..ab26a86 100755 --- a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.java @@ -37,17 +37,25 @@ */ public class HTTPEventListenerProvider implements EventListenerProvider { private final OkHttpClient httpClient = new OkHttpClient(); - private Set excludedEvents; - private Set excludedAdminOperations; + private Set excludedUserEvents; + private Set includedUserEvents; + private Set excludedAdminEvents; + private Set includedAdminEvents; + private String[] adminEventResourcePathPrefixes; + private Boolean includeAdminEventRepresentation; private String serverUri; private String username; private String password; public static final String publisherId = "keycloak"; public String TOPIC; - public HTTPEventListenerProvider(Set excludedEvents, Set excludedAdminOperations, String serverUri, String username, String password, String topic) { - this.excludedEvents = excludedEvents; - this.excludedAdminOperations = excludedAdminOperations; + public HTTPEventListenerProvider(Set excludedUserEvents, Set excludedAdminEvents, Set includedUserEvents, Set includedAdminEvents, String[] adminEventResourcePathPrefixes, Boolean includeAdminEventRepresentation, String serverUri, String username, String password, String topic) { + this.excludedUserEvents = excludedUserEvents; + this.excludedAdminEvents = excludedAdminEvents; + this.includedUserEvents = includedUserEvents; + this.includedAdminEvents = includedAdminEvents; + this.adminEventResourcePathPrefixes = adminEventResourcePathPrefixes; + this.includeAdminEventRepresentation = includeAdminEventRepresentation; this.serverUri = serverUri; this.username = username; this.password = password; @@ -55,9 +63,13 @@ public HTTPEventListenerProvider(Set excludedEvents, Set 0) { + boolean found = false; + for(String prefix : adminEventResourcePathPrefixes) { + String resourcePath = event.getResourcePath(); + if (resourcePath != null && resourcePath.startsWith(prefix)) { + found = true; + break; + } + } + if (!found) { + return; + } + } + // Ignore excluded operations - if (excludedAdminOperations != null && excludedAdminOperations.contains(event.getOperationType())) { + if (excludedAdminEvents != null && excludedAdminEvents.contains(event.getOperationType())) { return; } else { String stringEvent = toString(event); @@ -140,31 +170,31 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { private String toString(Event event) { StringBuilder sb = new StringBuilder(); - sb.append("{'type': '"); + sb.append("{\"type\": \""); sb.append(event.getType()); - sb.append("', 'realmId': '"); + sb.append("\", \"realmId\": \""); sb.append(event.getRealmId()); - sb.append("', 'clientId': '"); + sb.append("\", \"clientId\": \""); sb.append(event.getClientId()); - sb.append("', 'userId': '"); + sb.append("\", \"userId\": \""); sb.append(event.getUserId()); - sb.append("', 'ipAddress': '"); + sb.append("\", \"ipAddress\": \""); sb.append(event.getIpAddress()); - sb.append("'"); + sb.append("\""); if (event.getError() != null) { - sb.append(", 'error': '"); + sb.append(", \"error\": \""); sb.append(event.getError()); - sb.append("'"); + sb.append("\""); } - sb.append(", 'details': {"); + sb.append(", \"details\": {"); if (event.getDetails() != null) { for (Map.Entry e : event.getDetails().entrySet()) { - sb.append("'"); + sb.append("\""); sb.append(e.getKey()); - sb.append("': '"); + sb.append("\": \""); sb.append(e.getValue()); - sb.append("', "); + sb.append("\", "); } } sb.append("}}"); @@ -176,24 +206,35 @@ private String toString(Event event) { private String toString(AdminEvent adminEvent) { StringBuilder sb = new StringBuilder(); - sb.append("{'type': '"); - sb.append(adminEvent.getOperationType()); - sb.append("', 'realmId': '"); + OperationType type = adminEvent.getOperationType(); + + sb.append("{\"type\": \""); + sb.append(type); + sb.append("\", \"realmId\": \""); sb.append(adminEvent.getAuthDetails().getRealmId()); - sb.append("', 'clientId': '"); + sb.append("\", \"clientId\": \""); sb.append(adminEvent.getAuthDetails().getClientId()); - sb.append("', 'userId': '"); + sb.append("\", \"userId\": \""); sb.append(adminEvent.getAuthDetails().getUserId()); - sb.append("', 'ipAddress': '"); - sb.append(adminEvent.getAuthDetails().getIpAddress()); - sb.append("', 'resourcePath': '"); + sb.append("\", \"ipAddress\": \""); + sb.append(adminEvent.getAuthDetails().getIpAddress()); + sb.append("\", \"resourcePath\": \""); sb.append(adminEvent.getResourcePath()); - sb.append("'"); + sb.append("\""); + + if(includeAdminEventRepresentation && (type == OperationType.CREATE + || type == OperationType.UPDATE )) { + String representation = adminEvent.getRepresentation(); + if(representation != null) { + sb.append(", \"representation\": "); + sb.append(representation); + } + } if (adminEvent.getError() != null) { - sb.append(", 'error': '"); + sb.append(", \"error\": \""); sb.append(adminEvent.getError()); - sb.append("'"); + sb.append("\""); } sb.append("}"); return sb.toString(); diff --git a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java index 101a22d..613a91a 100755 --- a/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java +++ b/src/main/java/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.java @@ -27,44 +27,70 @@ import java.util.HashSet; import java.util.Set; -import java.lang.Exception; /** * @author Jessy Lennee */ public class HTTPEventListenerProviderFactory implements EventListenerProviderFactory { - private Set excludedEvents; - private Set excludedAdminOperations; - private String serverUri; + private Set excludeUserEvents; + private Set includeUserEvents; + private Set excludeAdminEvents; + private Set includedAdminEvents; + private String[] adminEventResourcePathPrefixes; + private Boolean includeAdminEventRepresentation; + private String serverUri; private String username; private String password; private String topic; @Override public EventListenerProvider create(KeycloakSession session) { - return new HTTPEventListenerProvider(excludedEvents, excludedAdminOperations, serverUri, username, password, topic); + return new HTTPEventListenerProvider(excludeUserEvents, excludeAdminEvents, includeUserEvents, includedAdminEvents, adminEventResourcePathPrefixes, includeAdminEventRepresentation, serverUri, username, password, topic); } @Override public void init(Config.Scope config) { - String[] excludes = config.getArray("exclude-events"); - if (excludes != null) { - excludedEvents = new HashSet<>(); - for (String e : excludes) { - excludedEvents.add(EventType.valueOf(e)); + String[] excludeUserEventsConfig = config.getArray("exclude-user-events"); + if (excludeUserEventsConfig != null) { + excludeUserEvents = new HashSet<>(); + for (String e : excludeUserEventsConfig) { + excludeUserEvents.add(EventType.valueOf(e)); } } - String[] excludesOperations = config.getArray("excludesOperations"); - if (excludesOperations != null) { - excludedAdminOperations = new HashSet<>(); - for (String e : excludesOperations) { - excludedAdminOperations.add(OperationType.valueOf(e)); + String[] excludeAdminEventsConfig = config.getArray("exclude-admin-events"); + if (excludeAdminEventsConfig != null) { + excludeAdminEvents = new HashSet<>(); + for (String e : excludeAdminEventsConfig) { + excludeAdminEvents.add(OperationType.valueOf(e)); } } - serverUri = config.get("serverUri", "http://nginx/frontend_dev.php/webhook/keycloak"); + String[] includeUserEventsConfig = config.getArray("include-user-events"); + if (includeUserEventsConfig != null) { + includeUserEvents = new HashSet<>(); + if (!(includeUserEventsConfig.length == 1 && includeUserEventsConfig[0].equals("none"))) { + for (String i : includeUserEventsConfig) { + includeUserEvents.add(EventType.valueOf(i)); + } + } + } + + String[] includeAdminEventsConfig = config.getArray("include-admin-events"); + if (includeAdminEventsConfig != null) { + includedAdminEvents = new HashSet<>(); + if (!(includeAdminEventsConfig.length == 1 && includeAdminEventsConfig[0].equals("none"))) { + for (String i : includeAdminEventsConfig) { + includedAdminEvents.add(OperationType.valueOf(i)); + } + } + } + + adminEventResourcePathPrefixes = config.getArray("admin-event-resource-path-prefixes"); + includeAdminEventRepresentation = config.getBoolean("include-admin-event-representation", false); + + serverUri = config.get("server-uri", "http://nginx/frontend_dev.php/webhook/keycloak"); username = config.get("username", null); password = config.get("password", null); topic = config.get("topic", "keycloak/events"); diff --git a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class index 5627ff0..f4b6219 100644 Binary files a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class and b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProvider.class differ diff --git a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class index fd62952..e06fb38 100644 Binary files a/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class and b/target/classes/org/softwarefactory/keycloak/providers/events/http/HTTPEventListenerProviderFactory.class differ diff --git a/target/event-listener-http-jar-with-dependencies.jar b/target/event-listener-http-jar-with-dependencies.jar index 67223ce..4760bd9 100644 Binary files a/target/event-listener-http-jar-with-dependencies.jar and b/target/event-listener-http-jar-with-dependencies.jar differ diff --git a/target/event-listener-http.jar b/target/event-listener-http.jar index afc334f..50a69be 100644 Binary files a/target/event-listener-http.jar and b/target/event-listener-http.jar differ diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties index 33aa8ac..11f56bb 100644 --- a/target/maven-archiver/pom.properties +++ b/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Tue Jan 28 10:33:34 CET 2020 -groupId=org.softwarefactory.keycloak.providers.events.http +#Tue Jan 03 19:20:26 CET 2023 artifactId=event-listener-http -version=5.0.0 +groupId=org.softwarefactory.keycloak.providers.events.http +version=1.0.0