From 125990d9d5a7cc933b2f39c23095d702c2187a11 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Mon, 20 Oct 2025 12:51:05 +0200 Subject: [PATCH 1/2] small changes, prepare v1.5.0 --- pom.xml | 12 +++++------ .../org/privacyidea/AsyncRequestCallable.java | 21 +++++++++++++------ src/main/java/org/privacyidea/JSONParser.java | 6 ++++++ src/main/java/org/privacyidea/PIResponse.java | 11 ++++++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index a5ce3c7..70794c5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.privacyidea privacyidea-java-client - 1.4.0 + 1.5.0 jar UTF-8 @@ -83,16 +83,16 @@ 4.13.2 test - - com.squareup.okhttp3 - okhttp - 4.12.0 - org.jetbrains.kotlin kotlin-stdlib 1.9.0 + + com.squareup.okhttp3 + okhttp + 4.12.0 + com.squareup.okio okio diff --git a/src/main/java/org/privacyidea/AsyncRequestCallable.java b/src/main/java/org/privacyidea/AsyncRequestCallable.java index 46495a0..1388e4b 100644 --- a/src/main/java/org/privacyidea/AsyncRequestCallable.java +++ b/src/main/java/org/privacyidea/AsyncRequestCallable.java @@ -24,6 +24,7 @@ import okhttp3.Call; import okhttp3.Callback; import okhttp3.Response; +import okhttp3.ResponseBody; import org.jetbrains.annotations.NotNull; import static org.privacyidea.PIConstants.ENDPOINT_AUTH; @@ -76,15 +77,23 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { - if (response.body() != null) + // The body of the response can be in `body()` for success cases or in `errorBody()` for error cases. + // We need to handle both and ensure the body is closed to prevent resource leaks. + // The body can only be consumed once. + try (ResponseBody responseBody = response.body()) { - String s = response.body().string(); - if (!privacyIDEA.logExcludedEndpoints().contains(path) && !ENDPOINT_AUTH.equals(path)) + if (responseBody != null) { - privacyIDEA.log(path + ":\n" + privacyIDEA.parser.formatJson(s)); + String s = responseBody.string(); + if (!privacyIDEA.logExcludedEndpoints().contains(path)) + { + privacyIDEA.log(path + " (" + response.code() + "):\n" + privacyIDEA.parser.formatJson(s)); + } + callbackResult[0] = s; } - callbackResult[0] = s; } - latch.countDown(); + finally { + latch.countDown(); + } } } \ No newline at end of file diff --git a/src/main/java/org/privacyidea/JSONParser.java b/src/main/java/org/privacyidea/JSONParser.java index f67ea71..e065ea7 100644 --- a/src/main/java/org/privacyidea/JSONParser.java +++ b/src/main/java/org/privacyidea/JSONParser.java @@ -73,6 +73,7 @@ import static org.privacyidea.PIConstants.TIME; import static org.privacyidea.PIConstants.TOKEN; import static org.privacyidea.PIConstants.TOKENS; +import static org.privacyidea.PIConstants.TOKEN_TYPE_PASSKEY; import static org.privacyidea.PIConstants.TOKEN_TYPE_WEBAUTHN; import static org.privacyidea.PIConstants.TRANSACTION_ID; import static org.privacyidea.PIConstants.TYPE; @@ -277,6 +278,7 @@ else if ("interactive".equals(modeFromResponse)) }); } + // Multichallenge JsonArray arrChallenges = detail.getAsJsonArray(MULTI_CHALLENGE); if (arrChallenges != null) { @@ -311,6 +313,10 @@ else if ("interactive".equals(modeFromResponse)) webauthnSignRequests.add(webauthnSignRequest); } } + else if (TOKEN_TYPE_PASSKEY.equals(type)) + { + response.passkeyChallenge = challenge.toString(); + } else { response.multiChallenge.add(new Challenge(serial, message, clientMode, image, transactionID, type)); diff --git a/src/main/java/org/privacyidea/PIResponse.java b/src/main/java/org/privacyidea/PIResponse.java index 534ac74..8b277f7 100644 --- a/src/main/java/org/privacyidea/PIResponse.java +++ b/src/main/java/org/privacyidea/PIResponse.java @@ -124,6 +124,17 @@ public String pushTransactionId() { return null; } + public boolean hasChallenges() + { + return (multiChallenge != null && !multiChallenge.isEmpty()) || + isNotBlank(mergedSignRequest()) || + isNotBlank(passkeyChallenge); + } + + private boolean isNotBlank(String str) { + return str != null && !str.trim().isEmpty(); + } + /** * Get the messages of all token that require an input field (HOTP, TOTP, SMS, Email...) reduced to a single string. * From b9e6856039691db75e8b30f9e66fa9dc5d9ad486 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 5 Mar 2026 14:11:10 +0100 Subject: [PATCH 2/2] v1.5.0 --- pom.xml | 2 +- src/main/java/org/privacyidea/PIResponse.java | 55 ++++++++++++++----- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 70794c5..2af6fcd 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ maven-surefire-plugin 3.5.3 - false + true diff --git a/src/main/java/org/privacyidea/PIResponse.java b/src/main/java/org/privacyidea/PIResponse.java index 8b277f7..406417f 100644 --- a/src/main/java/org/privacyidea/PIResponse.java +++ b/src/main/java/org/privacyidea/PIResponse.java @@ -18,6 +18,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; @@ -84,10 +89,11 @@ public boolean authenticationSuccessful() */ public boolean pushAvailable() { - return multiChallenge.stream().anyMatch(c -> isPushOrSmartphoneContainer(c.getType())); + return multiChallenge.stream().anyMatch(c -> isPushOrSmartphoneContainer(c.getType()) && "poll".equals(c.getClientMode())); } - private boolean isPushOrSmartphoneContainer(String type) { + private boolean isPushOrSmartphoneContainer(String type) + { return TOKEN_TYPE_PUSH.equals(type) || CONTAINER_TYPE_SMARTPHONE.equals(type); } @@ -113,7 +119,8 @@ public String otpTransactionId() return null; } - public String pushTransactionId() { + public String pushTransactionId() + { for (Challenge challenge : multiChallenge) { if (isPushOrSmartphoneContainer(challenge.getType())) @@ -126,12 +133,11 @@ public String pushTransactionId() { public boolean hasChallenges() { - return (multiChallenge != null && !multiChallenge.isEmpty()) || - isNotBlank(mergedSignRequest()) || - isNotBlank(passkeyChallenge); + return (multiChallenge != null && !multiChallenge.isEmpty()) || isNotBlank(mergedSignRequest()) || isNotBlank(passkeyChallenge); } - private boolean isNotBlank(String str) { + private boolean isNotBlank(String str) + { return str != null && !str.trim().isEmpty(); } @@ -142,16 +148,27 @@ private boolean isNotBlank(String str) { */ public String otpMessage() { - return reduceChallengeMessagesWhere(c -> !(isPushOrSmartphoneContainer(c.getType()))); + return reduceChallengeMessagesWhere(c -> + { + String cm = c.getClientMode(); + System.out.println( + "challenge for " + c.getType() + " " + c.getSerial() + " with mode: " + cm); + boolean yes = "interactive".equals(cm); + return yes; + }); } private String reduceChallengeMessagesWhere(Predicate predicate) { StringBuilder sb = new StringBuilder(); - sb.append( - multiChallenge.stream().filter(predicate).map(Challenge::getMessage).distinct().reduce("", (a, s) -> a + s + ", ").trim()); - - if (sb.length() > 0) + sb.append(this.multiChallenge.stream() + .filter(predicate) + .map(Challenge::getMessage) + .distinct() + .reduce("", (a, s) -> a + s + ", ") + .trim()); + + if (!sb.isEmpty()) { sb.deleteCharAt(sb.length() - 1); } @@ -198,7 +215,19 @@ public String toJSON() public static PIResponse fromJSON(String json) { - return new Gson().fromJson(json, PIResponse.class); + JsonDeserializer challengeDeserializer = (jsonElement, type, ctx) -> + { + JsonObject obj = jsonElement.getAsJsonObject(); + String serial = obj.has("serial") && !obj.get("serial").isJsonNull() ? obj.get("serial").getAsString() : ""; + String message = obj.has("message") && !obj.get("message").isJsonNull() ? obj.get("message").getAsString() : ""; + String clientMode = obj.has("clientMode") && !obj.get("clientMode").isJsonNull() ? obj.get("clientMode").getAsString() : ""; + String image = obj.has("image") && !obj.get("image").isJsonNull() ? obj.get("image").getAsString() : ""; + String transactionID = obj.has("transactionID") && !obj.get("transactionID").isJsonNull() ? obj.get("transactionID").getAsString() : ""; + String tokenType = obj.has("type") && !obj.get("type").isJsonNull() ? obj.get("type").getAsString() : ""; + return new Challenge(serial, message, clientMode, image, transactionID, tokenType); + }; + Gson gson = new GsonBuilder().registerTypeAdapter(Challenge.class, challengeDeserializer).create(); + return gson.fromJson(json, PIResponse.class); } @Override