Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 58 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

```
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
<web-context>auth</web-context>
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 name="eventsListener">
<provider name="mqtt" enabled="true">
<properties>
<property name="serverUri" value="http://127.0.0.1:8080/webhook"/>
<property name="username" value="auth_user"/>
<property name="password" value="auth_password"/>
<property name="topic" value="my_topic"/>
</properties>
</provider>
</spi>
--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",
Expand All @@ -81,4 +108,4 @@ Request example
"username": "username"
}
}
```
```
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<groupId>org.softwarefactory.keycloak.providers.events.http</groupId>
<version>5.0.0</version>
<version>1.0.0</version>

<name>Keycloak: Event Publisher to HTTP</name>
<description/>
Expand All @@ -29,7 +29,7 @@

<properties>
<version.wildfly>14.0.1.Final</version.wildfly>
<version.keycloak>${project.version}</version.keycloak>
<version.keycloak>19.0.3</version.keycloak>

<version.wildfly.maven.plugin>1.2.2.Final</version.wildfly.maven.plugin>
<servlet.api.30.version>1.0.2.Final</servlet.api.30.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,39 @@
*/
public class HTTPEventListenerProvider implements EventListenerProvider {
private final OkHttpClient httpClient = new OkHttpClient();
private Set<EventType> excludedEvents;
private Set<OperationType> excludedAdminOperations;
private Set<EventType> excludedUserEvents;
private Set<EventType> includedUserEvents;
private Set<OperationType> excludedAdminEvents;
private Set<OperationType> 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<EventType> excludedEvents, Set<OperationType> excludedAdminOperations, String serverUri, String username, String password, String topic) {
this.excludedEvents = excludedEvents;
this.excludedAdminOperations = excludedAdminOperations;
public HTTPEventListenerProvider(Set<EventType> excludedUserEvents, Set<OperationType> excludedAdminEvents, Set<EventType> includedUserEvents, Set<OperationType> 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;
this.TOPIC = topic;
}

@Override
public void onEvent(Event event) {
public void onEvent(Event event) {
if (includedUserEvents != null && !includedUserEvents.contains(event.getType())){
return;
}

// Ignore excluded events
if (excludedEvents != null && excludedEvents.contains(event.getType())) {
if (excludedUserEvents != null && excludedUserEvents.contains(event.getType())) {
return;
} else {
String stringEvent = toString(event);
Expand Down Expand Up @@ -97,8 +109,26 @@ public void onEvent(Event event) {

@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (includedAdminEvents != null && !includedAdminEvents.contains(event.getOperationType())){
return;
}

if (adminEventResourcePathPrefixes != null && adminEventResourcePathPrefixes.length > 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);
Expand Down Expand Up @@ -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<String, String> 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("}}");
Expand All @@ -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();
Expand Down
Loading