From b191d3575fdbea2f0b7802ca2b91f68d1d2bc19a Mon Sep 17 00:00:00 2001 From: Darren Sapalo Date: Fri, 18 Jun 2021 14:12:31 +0800 Subject: [PATCH 1/6] Fixed support for JSON request bodies; rather than form requests. --- .../http/HTTPEventListenerProvider.java | 56 +++++++++---------- .../HTTPEventListenerProviderFactory.java | 2 + 2 files changed, 29 insertions(+), 29 deletions(-) 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 9b45e36..5e95e6a 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,6 +37,7 @@ */ public class HTTPEventListenerProvider implements EventListenerProvider { private final OkHttpClient httpClient = new OkHttpClient(); + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private Set excludedEvents; private Set excludedAdminOperations; private String serverUri; @@ -62,9 +63,8 @@ public void onEvent(Event event) { } else { String stringEvent = toString(event); try { - RequestBody formBody = new FormBody.Builder() - .add("json", stringEvent) - .build(); + + okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(JSON, stringEvent); okhttp3.Request.Builder builder = new Request.Builder() .url(this.serverUri) @@ -75,7 +75,7 @@ public void onEvent(Event event) { builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); } - Request request = builder.post(formBody) + Request request = builder.post(jsonRequestBody) .build(); Response response = httpClient.newCall(request).execute(); @@ -103,9 +103,7 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { } else { String stringEvent = toString(event); try { - RequestBody formBody = new FormBody.Builder() - .add("json", stringEvent) - .build(); + okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(JSON, stringEvent); okhttp3.Request.Builder builder = new Request.Builder() .url(this.serverUri) @@ -116,7 +114,7 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); } - Request request = builder.post(formBody) + Request request = builder.post(jsonRequestBody) .build(); Response response = httpClient.newCall(request).execute(); @@ -140,31 +138,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 +174,24 @@ private String toString(Event event) { private String toString(AdminEvent adminEvent) { StringBuilder sb = new StringBuilder(); - sb.append("{'type': '"); + sb.append("{\"type\": \""); sb.append(adminEvent.getOperationType()); - sb.append("', 'realmId': '"); + 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("\", \"ipAddress\": \""); sb.append(adminEvent.getAuthDetails().getIpAddress()); - sb.append("', 'resourcePath': '"); + sb.append("\", \"resourcePath\": \""); sb.append(adminEvent.getResourcePath()); - sb.append("'"); + sb.append("\""); 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..4c8c9de 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 @@ -68,6 +68,8 @@ public void init(Config.Scope config) { username = config.get("username", null); password = config.get("password", null); topic = config.get("topic", "keycloak/events"); + + System.out.println("Forwarding keycloak events to: " + serverUri); } @Override From 197a3aaefac49d894b776e4e332efa4c81b525ad Mon Sep 17 00:00:00 2001 From: Darren Sapalo Date: Tue, 29 Jun 2021 13:48:26 +0800 Subject: [PATCH 2/6] Fixed JSON preparation; no longer has trailing comma. --- .../providers/events/http/HTTPEventListenerProvider.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 5e95e6a..e6d0d7c 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 @@ -157,12 +157,15 @@ private String toString(Event event) { } sb.append(", \"details\": {"); if (event.getDetails() != null) { + int i = 0; for (Map.Entry e : event.getDetails().entrySet()) { sb.append("\""); sb.append(e.getKey()); sb.append("\": \""); sb.append(e.getValue()); - sb.append("\", "); + if (i < event.getDetails().size() - 1) { + sb.append("\", "); + } } sb.append("}}"); } From 34ec1d3b4a3ad76ecf6ad8217df97b27e413332c Mon Sep 17 00:00:00 2001 From: Darren Sapalo Date: Wed, 7 Jul 2021 12:04:18 +0800 Subject: [PATCH 3/6] Added display JSON request body string if the request fails. --- .../providers/events/http/HTTPEventListenerProvider.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 e6d0d7c..9759092 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 @@ -88,7 +88,8 @@ public void onEvent(Event event) { System.out.println(response.body().string()); } catch(Exception e) { // ? - System.out.println("UH OH!! " + e.toString()); + System.out.println("Failed to forward webhook event " + e.toString()); + System.out.println("Request body string: " + stringEvent); e.printStackTrace(); return; } @@ -102,6 +103,7 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { return; } else { String stringEvent = toString(event); + try { okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(JSON, stringEvent); @@ -127,7 +129,8 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { System.out.println(response.body().string()); } catch(Exception e) { // ? - System.out.println("UH OH!! " + e.toString()); + System.out.println("Failed to forward webhook event " + e.toString()); + System.out.println("Request body string: " + stringEvent); e.printStackTrace(); return; } From b2211b23c429f94b2d464f637d262a072b857aa7 Mon Sep 17 00:00:00 2001 From: Darren Sapalo Date: Wed, 7 Jul 2021 14:37:42 +0800 Subject: [PATCH 4/6] Fixed the for loop including the last comma at the end. --- .../providers/events/http/HTTPEventListenerProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 9759092..58d9806 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 @@ -88,7 +88,7 @@ public void onEvent(Event event) { System.out.println(response.body().string()); } catch(Exception e) { // ? - System.out.println("Failed to forward webhook event " + e.toString()); + System.out.println("Failed to forward webhook Event " + e.toString()); System.out.println("Request body string: " + stringEvent); e.printStackTrace(); return; @@ -129,7 +129,7 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { System.out.println(response.body().string()); } catch(Exception e) { // ? - System.out.println("Failed to forward webhook event " + e.toString()); + System.out.println("Failed to forward webhook AdminEvent " + e.toString()); System.out.println("Request body string: " + stringEvent); e.printStackTrace(); return; @@ -166,7 +166,7 @@ private String toString(Event event) { sb.append(e.getKey()); sb.append("\": \""); sb.append(e.getValue()); - if (i < event.getDetails().size() - 1) { + if (i < event.getDetails().size()) { sb.append("\", "); } } From 258b569b7e79e30b2a4e7ece825a321b8b7b270c Mon Sep 17 00:00:00 2001 From: Darren Sapalo Date: Fri, 9 Jul 2021 16:40:01 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20fix:=20The=20end=20quote=20i?= =?UTF-8?q?s=20included,=20but=20the=20conditional=20element=20only=20is?= =?UTF-8?q?=20the=20comma.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../providers/events/http/HTTPEventListenerProvider.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 58d9806..1e175c6 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 @@ -166,9 +166,11 @@ private String toString(Event event) { sb.append(e.getKey()); sb.append("\": \""); sb.append(e.getValue()); - if (i < event.getDetails().size()) { - sb.append("\", "); + sb.append("\""); + if (i < event.getDetails().entrySet().size() - 1) { + sb.append(", "); } + i = i + 1; } sb.append("}}"); } From 5530babd6af1f9b22ce0183fb78d7c4d6d7a8f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Wed, 9 Mar 2022 18:16:18 +0100 Subject: [PATCH 6/6] FIX: Fixed Basic Authentication implementation. CHORE: Some cleanup --- .../http/HTTPEventListenerProvider.java | 24 +++++++++--------- .../HTTPEventListenerProviderFactory.java | 5 +--- .../http/HTTPEventListenerProvider.class | Bin 6532 -> 7227 bytes .../HTTPEventListenerProviderFactory.class | Bin 3022 -> 3278 bytes 4 files changed, 13 insertions(+), 16 deletions(-) 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 1e175c6..b427920 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 @@ -23,12 +23,12 @@ import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.OperationType; +import java.util.Base64; import java.util.Map; import java.util.Set; import java.lang.Exception; import okhttp3.*; -import okhttp3.OkHttpClient.Builder; import java.io.IOException; @@ -44,15 +44,13 @@ public class HTTPEventListenerProvider implements EventListenerProvider { 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) { + public HTTPEventListenerProvider(Set excludedEvents, Set excludedAdminOperations, String serverUri, String username, String password) { this.excludedEvents = excludedEvents; this.excludedAdminOperations = excludedAdminOperations; this.serverUri = serverUri; this.username = username; this.password = password; - this.TOPIC = topic; } @Override @@ -64,15 +62,14 @@ public void onEvent(Event event) { String stringEvent = toString(event); try { - okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(JSON, stringEvent); + okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(stringEvent, JSON); okhttp3.Request.Builder builder = new Request.Builder() .url(this.serverUri) .addHeader("User-Agent", "KeycloakHttp Bot"); - if (this.username != null && this.password != null) { - builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); + builder.addHeader("Authorization", this.getAuthHeader()); } Request request = builder.post(jsonRequestBody) @@ -105,15 +102,14 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { String stringEvent = toString(event); try { - okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(JSON, stringEvent); + okhttp3.RequestBody jsonRequestBody = okhttp3.RequestBody.create(stringEvent, JSON); okhttp3.Request.Builder builder = new Request.Builder() .url(this.serverUri) .addHeader("User-Agent", "KeycloakHttp Bot"); - if (this.username != null && this.password != null) { - builder.addHeader("Authorization", "Basic " + this.username + ":" + this.password.toCharArray()); + builder.addHeader("Authorization", this.getAuthHeader()); } Request request = builder.post(jsonRequestBody) @@ -137,6 +133,11 @@ public void onEvent(AdminEvent event, boolean includeRepresentation) { } } + private String getAuthHeader() { + String token = this.username + ":" + this.password; + String encodedToken = Base64.getEncoder().encodeToString(token.getBytes()); + return "Basic " + encodedToken; + } private String toString(Event event) { StringBuilder sb = new StringBuilder(); @@ -177,8 +178,7 @@ private String toString(Event event) { return sb.toString(); } - - + private String toString(AdminEvent adminEvent) { StringBuilder sb = new StringBuilder(); 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 4c8c9de..3aacf9f 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,7 +27,6 @@ import java.util.HashSet; import java.util.Set; -import java.lang.Exception; /** * @author Jessy Lennee @@ -39,11 +38,10 @@ public class HTTPEventListenerProviderFactory implements EventListenerProviderFa 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(excludedEvents, excludedAdminOperations, serverUri, username, password); } @Override @@ -67,7 +65,6 @@ public void init(Config.Scope config) { serverUri = config.get("serverUri", "http://nginx/frontend_dev.php/webhook/keycloak"); username = config.get("username", null); password = config.get("password", null); - topic = config.get("topic", "keycloak/events"); System.out.println("Forwarding keycloak events to: " + serverUri); } 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 5627ff0ebc375a5752dbb41e5717977513a383e5..f8d3e48bab5fe6212a7579b51858d1cd51732e95 100644 GIT binary patch literal 7227 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 EzhTIe*Z=?k literal 6532 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 literal 3022 zcmb_eO>@&$6g|%$l42YJ1_LSI-*FO0DU=lAByE}!NHIx@0~BaW63((~kx*v7Ohn6ci|lkPq5zH`sL)*t`;wh7<^RAM-Q zD=M;G*pI7nn3cnv9OmWlxg4&=5JFDF!Zw`4brm;ah~r8OH}SKi0(;E6T+|}?+j3|bRMHP7!1%d76gM6t{FbfygOxyPadb0P7HAAoXR!Ps9 zeny}xXBBP3uehebaO>N#tm78-6>~jba*P$-3^4QnYGJ)>W+v#qx4y$Q3Wa)c2Upyu4NawuU4UjZLBhCt!#MSeaGc_ zqrOwN@&d}3Wn2D)K)5e;Lm)Ec6lmC!wQO^?vbt!x3&vuJPZC)tZ_Uu3t@NN>6F(* z-Jqcfns(=WBfoOhCW9{7tq_{>wMCB0%X8yEt>*^tu&{yWDSk#x?QyF2l|rh*sP(#-GaF`R9-IY_szv+$60CAU>q)}j-Iv-MM+DXIEL}Q zKu-3scf~_ZL;^d4Qw1$)ZtymySKDM z-LRV*1r}Yh8LP^ECUu)}cZ#ONAB@0wGeYezrE{C@;Lf0Z^jKR!$k0Z_dc+~(yGzRZkI~=9+0yJ(19C?n=t^QvS<`v-;<)!dad1)Ky z`k6K%Y~cflRU=zos*tqpWP zL3nUBy*UyIk3^HvWaJSJrIXP!qjK+XuksYzpP?s&o8L#pPp|$ONbCq8X45Y*wgnMM zk4AbUn}#2(MLGk8%Zi>MrHJoUiB$<)=(6p{MkQ{NF;VW!|pJi@Sp>Uq^KZAle->RlWjDXm_F;q!e28(*Q^+8TG0byy2NUq|5x(R6 z1K&Kvr}z;U@fg$i9hU+ICzz)p^xy&$tI)c7r%JvWtiHe~QTZ{44I+<|^DHeVco!x* z7(wI}Rivocui~7FkNFc+#NRmjipo-(pEq6|Bj6-HsV0dKGQG@JyMD4wd1ea_^dl8C z#k0sfUu?)ws!7W5HHf(;3DzM5DY!&)>68wDNN%FHU1hl@*Kl?B9g|e=<(lg-gO*`4 gZ@=QbcJF5GOO~bfLzoH1&zLVDMx1jnF`{(+3vc-ZQUCw|