Skip to content

Commit

Permalink
feat: add basic auth support
Browse files Browse the repository at this point in the history
  • Loading branch information
Betisman committed Oct 31, 2024
1 parent 129fcbe commit b6ea86c
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 61 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ hs_err_pid*
replay_pid*

target/
.vscode/
48 changes: 45 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,43 @@
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- JUnit 5 Dependency -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<!-- System Stubs Dependency for setting environment variables in tests -->
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-jupiter</artifactId>
<version>2.1.3</version>
<scope>test</scope>
</dependency>
<!-- Mockito Dependency -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>uk.org.lidalia</groupId>
<artifactId>slf4j-test</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing -->
Expand All @@ -51,6 +84,15 @@
<version>2.11.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
<properties>
<keycloak.version>20.0.5</keycloak.version>
<maven.compiler.source>11</maven.compiler.source>
Expand Down
113 changes: 77 additions & 36 deletions src/main/java/com/example/keycloak/WebhookEventListenerProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,109 @@
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.KeycloakSession;
import org.slf4j.Logger;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import org.apache.http.protocol.BasicHttpContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebhookEventListenerProvider implements EventListenerProvider {
private static final Logger logger = LoggerFactory.getLogger(WebhookEventListenerProvider.class);
private final KeycloakSession session;
private final CloseableHttpClient httpClient;
private final Logger logger;

public WebhookEventListenerProvider(KeycloakSession session) {
public WebhookEventListenerProvider(KeycloakSession session, CloseableHttpClient httpClient, Logger logger) {
this.session = session;
this.httpClient = httpClient;
this.logger = logger;
}

@Override
public void onEvent(Event event) {
logger.info("Received event: {}", event);
processEvent(event, "USER_EVENT");
}

@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
processEvent(event, "ADMIN_EVENT");
}

private void processEvent(Object event, String eventType) {
logger.info("Processing {}: {}", eventType, event);
String realmName = session.getContext().getRealm().getName();
String webhookUrl = getWebhookUrlForRealm(realmName);
sendWebhook("USER_EVENT", event, webhookUrl);

try {
logger.info("Sending webhook for {} type", eventType);
String webhookUrl = getWebhookUrlForRealm(realmName);
HttpPost httpPost = prepareHttpPost(webhookUrl, realmName, event);
httpClient.execute(httpPost);
} catch (Exception e) {
logger.error("Error while sending webhook for {}: {}", eventType, e.getMessage(), e);
}
}

private String getWebhookUrlForRealm(String realmName) {
String envVarName = "WEBHOOK_URL_" + realmName.toUpperCase().replace("-", "_");
String webhookUrl = System.getenv(envVarName);
if (webhookUrl == null) {
throw new IllegalArgumentException("Webhook URL not specified for realm: " + realmName
+ ". Please set the WEBHOOK_URL_" + realmName.toUpperCase().replace("-", "_) environment variable."));
private String getEnvVariableForRealm(String prefix, String realm, String severity) {
String envVar = prefix + "_" + realm.toUpperCase().replace("-", "_");
String value = System.getenv(envVar);
if (value == null) {
String message = "Environment variable " + envVar + " not specified for realm: " + realm;
if ("error".equals(severity)) {
throw new IllegalArgumentException(message);
} else {
logger.warn(message);
}
}
return webhookUrl;
return value;
}

@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
logger.info("Received admin event: {}", event);
String realmName = session.getContext().getRealm().getName();
String webhookUrl = getWebhookUrlForRealm(realmName);
sendWebhook("ADMIN_EVENT", event, webhookUrl);
private String getWebhookUrlForRealm(String realmName) {
return getEnvVariableForRealm("WEBHOOK_URL", realmName, "error");
}

@Override
public void close() {
logger.info("Closing WebhookEventListenerProvider");
private String getWebhookAuthMethodForRealm(String realmName) {
return getEnvVariableForRealm("WEBHOOK_AUTH_METHOD", realmName, "warn");
}

private String getWebhookBasicAuthUsernameForRealm(String realmName) {
return getEnvVariableForRealm("WEBHOOK_BASIC_AUTH_USERNAME", realmName, "warn");
}

private void sendWebhook(String type, Object event, String webhookUrl) {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(webhookUrl);
httpPost.setHeader("Content-Type", "application/json");
private String getWebhookBasicAuthPasswordForRealm(String realmName) {
return getEnvVariableForRealm("WEBHOOK_BASIC_AUTH_PASSWORD", realmName, "warn");
}

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
private HttpPost prepareHttpPost(String webhookUrl, String realmName, Object event) throws Exception {
HttpPost httpPost = new HttpPost(webhookUrl);
httpPost.setHeader("Content-Type", "application/json");

httpPost.setEntity(new StringEntity(json));
String authMethod = getWebhookAuthMethodForRealm(realmName);
if ("basic".equalsIgnoreCase(authMethod)) {
configureBasicAuth(httpPost, realmName);
}

logger.info("Sending webhook for event type: {}", type);
client.execute(httpPost);
} catch (Exception e) {
e.printStackTrace();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
httpPost.setEntity(new StringEntity(json));

return httpPost;
}

private void configureBasicAuth(HttpPost httpPost, String realmName) throws Exception {
String username = getWebhookBasicAuthUsernameForRealm(realmName);
String password = getWebhookBasicAuthPasswordForRealm(realmName);
if (username != null && password != null) {
UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password);
httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, new BasicHttpContext()));
} else {
logger.warn("Basic authentication enabled, but username or password not set.");
}
}

@Override
public void close() {
logger.info("Closing WebhookEventListenerProvider");
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package com.example.keycloak;

import org.keycloak.models.KeycloakSession;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebhookEventListenerProviderFactory implements EventListenerProviderFactory {

private static final Logger logger = Logger.getLogger(WebhookEventListenerProviderFactory.class);
private static final Logger logger = LoggerFactory.getLogger(WebhookEventListenerProviderFactory.class);
private static final Logger providerLogger = LoggerFactory.getLogger(WebhookEventListenerProvider.class);

@Override
public EventListenerProvider create(KeycloakSession session) {
logger.info("Creating WebhookEventListenerProvider");
return new WebhookEventListenerProvider(session);

// Create or configure the CloseableHttpClient
CloseableHttpClient httpClient = HttpClients.createDefault();

// Pass the httpClient to the provider
return new WebhookEventListenerProvider(session, httpClient, providerLogger);
}

@Override
Expand Down
18 changes: 0 additions & 18 deletions src/test/java/com/example/keycloak/AppTest.java

This file was deleted.

Loading

0 comments on commit b6ea86c

Please sign in to comment.