From 668aaf84611005748ac5fd97547a5a7289dfd870 Mon Sep 17 00:00:00 2001 From: xalsina-sequent Date: Fri, 10 Jul 2026 10:40:27 +0200 Subject: [PATCH 1/3] Refactor OAuth2 client integration and add new module for oauth2-client --- .../conditional-authenticators/pom.xml | 6 ++ ...uthorizedElectionsUserAttributeMapper.java | 36 +-------- .../keycloak-extensions/oauth2-client/pom.xml | 54 +++++++++++++ .../oauth2/ClientCredentialsTokenClient.java | 75 +++++++++++++++++++ .../ClientCredentialsTokenClientTest.java | 71 ++++++++++++++++++ packages/keycloak-extensions/pom.xml | 2 + .../voter-enrollment/pom.xml | 5 ++ .../voter_enrollment/LookupAndUpdateUser.java | 35 ++------- 8 files changed, 222 insertions(+), 62 deletions(-) create mode 100644 packages/keycloak-extensions/oauth2-client/pom.xml create mode 100644 packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java create mode 100644 packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java diff --git a/packages/keycloak-extensions/conditional-authenticators/pom.xml b/packages/keycloak-extensions/conditional-authenticators/pom.xml index 991c076ddb3..43fab75af21 100644 --- a/packages/keycloak-extensions/conditional-authenticators/pom.xml +++ b/packages/keycloak-extensions/conditional-authenticators/pom.xml @@ -46,6 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only message-otp-authenticator ${project.version} + + sequent + oauth2-client + ${project.version} + org.keycloak @@ -184,6 +189,7 @@ SPDX-License-Identifier: AGPL-3.0-only javax:* software.amazon.awssdk:* org.reactivestreams:reactive-streams + sequent:oauth2-client diff --git a/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java b/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java index 751ca3d7bc1..c930c0ab9a5 100644 --- a/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java +++ b/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java @@ -18,7 +18,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,7 +35,7 @@ import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.IDToken; -import org.keycloak.util.JsonSerialization; +import sequent.keycloak.oauth2.ClientCredentialsTokenClient; /** * Mappings UserModel.attribute to an ID Token claim. Token claim name can be a full qualified @@ -312,44 +311,17 @@ public static ProtocolMapperModel createClaimMapper( } public String authenticate(String tenantId) { - HttpClient client = HttpClient.newHttpClient(); String url = this.keycloakUrl + "/realms/" + getTenantRealmName(tenantId) + "/protocol/openid-connect/token"; - Map data = new HashMap<>(); - data.put("client_id", this.clientId); - data.put("scope", "openid"); - data.put("client_secret", this.clientSecret); - data.put("grant_type", "client_credentials"); - - String form = - data.entrySet().stream() - .map(entry -> entry.getKey() + "=" + entry.getValue()) - .reduce((entry1, entry2) -> entry1 + "&" + entry2) - .orElse(""); - log.info(form); - HttpRequest request = - HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/x-www-form-urlencoded") - .POST(HttpRequest.BodyPublishers.ofString(form)) - .build(); - - CompletableFuture> responseFuture; - responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); - String responseBody = responseFuture.join().body(); - Object accessToken; try { - log.info("responseBody " + responseBody); - accessToken = JsonSerialization.readValue(responseBody, Map.class).get("access_token"); - log.info("authenticate " + accessToken.toString()); - return accessToken.toString(); + return ClientCredentialsTokenClient.requestAccessToken( + client, url, this.clientId, this.clientSecret); } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException("Failed to parse Keycloak token response", e); } - return responseBody; } private String getTenantRealmName(String tenantId) { diff --git a/packages/keycloak-extensions/oauth2-client/pom.xml b/packages/keycloak-extensions/oauth2-client/pom.xml new file mode 100644 index 00000000000..1674369eaf7 --- /dev/null +++ b/packages/keycloak-extensions/oauth2-client/pom.xml @@ -0,0 +1,54 @@ + + + + + + 4.0.0 + + + sequent + keycloak-extensions + 1.0-SNAPSHOT + + + oauth2-client + + + + org.keycloak + keycloak-core + ${keycloak.version} + + + org.junit.jupiter + junit-jupiter + 5.13.4 + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + 1.23.0 + + + + + + + + + diff --git a/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java new file mode 100644 index 00000000000..55c49c1f1a8 --- /dev/null +++ b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only + +package sequent.keycloak.oauth2; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.keycloak.util.JsonSerialization; + +/** + * Shared client for the OAuth2 client_credentials grant against a Keycloak realm's token + * endpoint. Extracted so the request-encoding and response-parsing logic exists in exactly one + * place; see meta#1252 for the incident (an unencoded client secret containing '%' broke + * application/x-www-form-urlencoded decoding) that motivated pulling this out of two separately + * copy-pasted implementations. + */ +public final class ClientCredentialsTokenClient { + + private ClientCredentialsTokenClient() {} + + public static String requestAccessToken( + HttpClient httpClient, String tokenUrl, String clientId, String clientSecret) + throws IOException { + Map data = new HashMap<>(); + data.put("client_id", clientId); + data.put("scope", "openid"); + data.put("client_secret", clientSecret); + data.put("grant_type", "client_credentials"); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(tokenUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(formUrlEncode(data))) + .build(); + + String responseBody = + httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join().body(); + return extractAccessToken(responseBody); + } + + // Percent-encodes each key/value pair so that secrets containing reserved + // form-encoding characters (%, &, +, =, space) survive as a valid + // application/x-www-form-urlencoded body. + static String formUrlEncode(Map data) { + return data.entrySet().stream() + .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue())) + .collect(Collectors.joining("&")); + } + + private static String encode(Object value) { + return URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8); + } + + // Fails loudly instead of NPE-ing on ".toString()" or silently returning the + // raw error body as if it were a token. + static String extractAccessToken(String responseBody) throws IOException { + Object accessToken = JsonSerialization.readValue(responseBody, Map.class).get("access_token"); + if (accessToken == null) { + throw new IllegalStateException( + "Keycloak client_credentials response did not include an access_token: " + + responseBody); + } + return accessToken.toString(); + } +} diff --git a/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java new file mode 100644 index 00000000000..b387ca2097e --- /dev/null +++ b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2025 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only + +package sequent.keycloak.oauth2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ClientCredentialsTokenClientTest { + + @Test + void formUrlEncodeSurvivesReservedFormCharacters() { + // Regression test for meta#1252: a secret containing '%' broke + // application/x-www-form-urlencoded decoding on the token request. + Map data = new HashMap<>(); + data.put("client_id", "service-account"); + data.put("client_secret", "ab%12&cd+ef=gh ij"); + data.put("grant_type", "client_credentials"); + + String form = ClientCredentialsTokenClient.formUrlEncode(data); + + Map decoded = new HashMap<>(); + for (String pair : form.split("&")) { + String[] kv = pair.split("=", 2); + decoded.put( + URLDecoder.decode(kv[0], StandardCharsets.UTF_8), + URLDecoder.decode(kv[1], StandardCharsets.UTF_8)); + } + + assertEquals("service-account", decoded.get("client_id")); + assertEquals("ab%12&cd+ef=gh ij", decoded.get("client_secret")); + assertEquals("client_credentials", decoded.get("grant_type")); + } + + @Test + void extractAccessTokenReturnsTokenOnSuccess() throws IOException { + String responseBody = "{\"access_token\":\"the-token\",\"token_type\":\"Bearer\"}"; + + String token = ClientCredentialsTokenClient.extractAccessToken(responseBody); + + assertEquals("the-token", token); + } + + @Test + void extractAccessTokenThrowsClearErrorWhenTokenMissing() { + // This is the exact failure mode from the incident: Keycloak's token + // endpoint rejects a malformed request and returns an error body with no + // access_token. Previously this caused a NullPointerException. + String responseBody = "{\"error\":\"invalid_client\",\"error_description\":\"bad secret\"}"; + + IllegalStateException ex = + assertThrows( + IllegalStateException.class, + () -> ClientCredentialsTokenClient.extractAccessToken(responseBody)); + assertEquals(true, ex.getMessage().contains("invalid_client")); + } + + @Test + void extractAccessTokenThrowsOnMalformedJson() { + assertThrows( + IOException.class, () -> ClientCredentialsTokenClient.extractAccessToken("not json")); + } +} diff --git a/packages/keycloak-extensions/pom.xml b/packages/keycloak-extensions/pom.xml index 51b38f2d79c..0ca88a87105 100644 --- a/packages/keycloak-extensions/pom.xml +++ b/packages/keycloak-extensions/pom.xml @@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only aws-ses-email-sender-provider + oauth2-client conditional-authenticators ivr-config-provider dummy-email-sender-provider @@ -129,6 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only software.amazon.awssdk:* org.reactivestreams:reactive-streams com.rabbitmq:* + sequent:oauth2-client diff --git a/packages/keycloak-extensions/voter-enrollment/pom.xml b/packages/keycloak-extensions/voter-enrollment/pom.xml index ae6fe6d1fbf..63d217d5e27 100644 --- a/packages/keycloak-extensions/voter-enrollment/pom.xml +++ b/packages/keycloak-extensions/voter-enrollment/pom.xml @@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only message-otp-authenticator ${project.version} + + sequent + oauth2-client + ${project.version} + org.junit.jupiter junit-jupiter diff --git a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java index 81816938395..03fce7dbcf4 100644 --- a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java +++ b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; -import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.extern.jbosslog.JBossLog; @@ -62,11 +61,11 @@ import org.keycloak.representations.userprofile.config.UPAttribute; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.theme.Theme; -import org.keycloak.util.JsonSerialization; import sequent.keycloak.authenticator.MessageOTPAuthenticator; import sequent.keycloak.authenticator.Utils.MessageCourier; import sequent.keycloak.authenticator.credential.MessageOTPCredentialModel; import sequent.keycloak.authenticator.credential.MessageOTPCredentialProvider; +import sequent.keycloak.oauth2.ClientCredentialsTokenClient; /** Lookups an user using a field */ @JBossLog @@ -994,36 +993,12 @@ public void authenticate(String tenantId) { + "/realms/" + getTenantRealmName(tenantId) + "/protocol/openid-connect/token"; - Map data = new HashMap<>(); - data.put("client_id", this.clientId); - data.put("scope", "openid"); - data.put("client_secret", this.clientSecret); - data.put("grant_type", "client_credentials"); - - String form = - data.entrySet().stream() - .map(entry -> entry.getKey() + "=" + entry.getValue()) - .reduce((entry1, entry2) -> entry1 + "&" + entry2) - .orElse(""); - log.info(form); - HttpRequest request = - HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/x-www-form-urlencoded") - .POST(HttpRequest.BodyPublishers.ofString(form)) - .build(); - - CompletableFuture> responseFuture; - responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); - String responseBody = responseFuture.join().body(); - Object accessToken; try { - log.info("responseBody " + responseBody); - accessToken = JsonSerialization.readValue(responseBody, Map.class).get("access_token"); - log.info("authenticate " + accessToken.toString()); - this.access_token = accessToken.toString(); + this.access_token = + ClientCredentialsTokenClient.requestAccessToken( + client, url, this.clientId, this.clientSecret); } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException("Failed to parse Keycloak token response", e); } } From c033155061b210e918d04cb44953912104fd1589 Mon Sep 17 00:00:00 2001 From: xalsina-sequent Date: Wed, 15 Jul 2026 10:11:12 +0200 Subject: [PATCH 2/3] Fix Keycloak OAuth2 PR review comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AuthorizedElectionsUserAttributeMapper.java | 4 ++-- packages/keycloak-extensions/oauth2-client/pom.xml | 5 +++++ .../oauth2/ClientCredentialsTokenClient.java | 12 ++++++++---- .../oauth2/ClientCredentialsTokenClientTest.java | 5 +++-- packages/keycloak-extensions/pom.xml | 2 ++ .../voter_enrollment/LookupAndUpdateUser.java | 4 ++-- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java b/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java index c930c0ab9a5..82f1df29db2 100644 --- a/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java +++ b/packages/keycloak-extensions/conditional-authenticators/src/main/java/sequent/keycloak/protocol/oidc/mappers/AuthorizedElectionsUserAttributeMapper.java @@ -319,8 +319,8 @@ public String authenticate(String tenantId) { try { return ClientCredentialsTokenClient.requestAccessToken( client, url, this.clientId, this.clientSecret); - } catch (IOException e) { - throw new RuntimeException("Failed to parse Keycloak token response", e); + } catch (IOException | IllegalStateException e) { + throw new RuntimeException("Failed to retrieve Keycloak access token", e); } } diff --git a/packages/keycloak-extensions/oauth2-client/pom.xml b/packages/keycloak-extensions/oauth2-client/pom.xml index 1674369eaf7..374b7a4571b 100644 --- a/packages/keycloak-extensions/oauth2-client/pom.xml +++ b/packages/keycloak-extensions/oauth2-client/pom.xml @@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + com.diffplug.spotless spotless-maven-plugin diff --git a/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java index 55c49c1f1a8..b8d3c1ff2bf 100644 --- a/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java +++ b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java @@ -19,7 +19,7 @@ /** * Shared client for the OAuth2 client_credentials grant against a Keycloak realm's token * endpoint. Extracted so the request-encoding and response-parsing logic exists in exactly one - * place; see meta#1252 for the incident (an unencoded client secret containing '%' broke + * place; see meta#12526 for the incident (an unencoded client secret containing '%' broke * application/x-www-form-urlencoded decoding) that motivated pulling this out of two separately * copy-pasted implementations. */ @@ -43,9 +43,13 @@ public static String requestAccessToken( .POST(HttpRequest.BodyPublishers.ofString(formUrlEncode(data))) .build(); - String responseBody = - httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join().body(); - return extractAccessToken(responseBody); + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return extractAccessToken(response.body()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while requesting Keycloak access token", e); + } } // Percent-encodes each key/value pair so that secrets containing reserved diff --git a/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java index b387ca2097e..8f96c5705f1 100644 --- a/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java +++ b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java @@ -5,6 +5,7 @@ package sequent.keycloak.oauth2; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; @@ -18,7 +19,7 @@ class ClientCredentialsTokenClientTest { @Test void formUrlEncodeSurvivesReservedFormCharacters() { - // Regression test for meta#1252: a secret containing '%' broke + // Regression test for meta#12526: a secret containing '%' broke // application/x-www-form-urlencoded decoding on the token request. Map data = new HashMap<>(); data.put("client_id", "service-account"); @@ -60,7 +61,7 @@ void extractAccessTokenThrowsClearErrorWhenTokenMissing() { assertThrows( IllegalStateException.class, () -> ClientCredentialsTokenClient.extractAccessToken(responseBody)); - assertEquals(true, ex.getMessage().contains("invalid_client")); + assertTrue(ex.getMessage().contains("invalid_client")); } @Test diff --git a/packages/keycloak-extensions/pom.xml b/packages/keycloak-extensions/pom.xml index 0ca88a87105..639d10bd9e1 100644 --- a/packages/keycloak-extensions/pom.xml +++ b/packages/keycloak-extensions/pom.xml @@ -148,6 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only ${basedir} aws-ses-email-sender-provider/pom.xml + oauth2-client/pom.xml conditional-authenticators/pom.xml ivr-config-provider/pom.xml dummy-email-sender-provider/pom.xml @@ -165,6 +166,7 @@ SPDX-License-Identifier: AGPL-3.0-only ${basedir} aws-ses-email-sender-provider/pom.xml + oauth2-client/pom.xml conditional-authenticators/pom.xml ivr-config-provider/pom.xml dummy-email-sender-provider/pom.xml diff --git a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java index 03fce7dbcf4..55bf266e1eb 100644 --- a/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java +++ b/packages/keycloak-extensions/voter-enrollment/src/main/java/sequent/keycloak/voter_enrollment/LookupAndUpdateUser.java @@ -997,8 +997,8 @@ public void authenticate(String tenantId) { this.access_token = ClientCredentialsTokenClient.requestAccessToken( client, url, this.clientId, this.clientSecret); - } catch (IOException e) { - throw new RuntimeException("Failed to parse Keycloak token response", e); + } catch (IOException | IllegalStateException e) { + throw new RuntimeException("Failed to retrieve Keycloak access token", e); } } From 27fda97d8830967e02cccfa15d7c0a184685a328 Mon Sep 17 00:00:00 2001 From: xalsina-sequent Date: Wed, 15 Jul 2026 12:35:39 +0200 Subject: [PATCH 3/3] Fix formatting --- .../oauth2/ClientCredentialsTokenClient.java | 12 ++++++------ .../oauth2/ClientCredentialsTokenClientTest.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java index b8d3c1ff2bf..2841857cb4b 100644 --- a/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java +++ b/packages/keycloak-extensions/oauth2-client/src/main/java/sequent/keycloak/oauth2/ClientCredentialsTokenClient.java @@ -17,9 +17,9 @@ import org.keycloak.util.JsonSerialization; /** - * Shared client for the OAuth2 client_credentials grant against a Keycloak realm's token - * endpoint. Extracted so the request-encoding and response-parsing logic exists in exactly one - * place; see meta#12526 for the incident (an unencoded client secret containing '%' broke + * Shared client for the OAuth2 client_credentials grant against a Keycloak realm's token endpoint. + * Extracted so the request-encoding and response-parsing logic exists in exactly one place; see + * meta#12526 for the incident (an unencoded client secret containing '%' broke * application/x-www-form-urlencoded decoding) that motivated pulling this out of two separately * copy-pasted implementations. */ @@ -44,7 +44,8 @@ public static String requestAccessToken( .build(); try { - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); return extractAccessToken(response.body()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -71,8 +72,7 @@ static String extractAccessToken(String responseBody) throws IOException { Object accessToken = JsonSerialization.readValue(responseBody, Map.class).get("access_token"); if (accessToken == null) { throw new IllegalStateException( - "Keycloak client_credentials response did not include an access_token: " - + responseBody); + "Keycloak client_credentials response did not include an access_token: " + responseBody); } return accessToken.toString(); } diff --git a/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java index 8f96c5705f1..394a0b140a2 100644 --- a/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java +++ b/packages/keycloak-extensions/oauth2-client/src/test/java/sequent/keycloak/oauth2/ClientCredentialsTokenClientTest.java @@ -5,8 +5,8 @@ package sequent.keycloak.oauth2; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.net.URLDecoder;