From 125990d9d5a7cc933b2f39c23095d702c2187a11 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Mon, 20 Oct 2025 12:51:05 +0200 Subject: [PATCH 1/8] 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/8] 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 From 477a455c0e26562f9cf2efc2c21f73a5ba277c45 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 08:54:03 +0100 Subject: [PATCH 3/8] update actions --- .github/workflows/build.yml | 54 ++----------------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 979bab4..8cb1782 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -name: Java CI with Maven, Run Tests, Coverage Report and Badge +name: Build on: push: @@ -15,56 +15,6 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Setup Java Development Kits - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: microsoft - cache: maven - + - name: Build with Maven run: mvn -B package --file pom.xml - - - name: Generate JavaCodeCoverage badge - id: jacoco - uses: cicirello/jacoco-badge-generator@v2 - with: - badges-directory: .github/badges - generate-branches-badge: true - generate-summary: true - - - name: Log coverage percentages to workflow output - run: | - echo "coverage = ${{ steps.jacoco.outputs.coverage }}" - echo "branches = ${{ steps.jacoco.outputs.branches }}" - - - name: Upload JaCoCo coverage report as a workflow artifact - uses: actions/upload-artifact@v4 - with: - name: jacoco-report - path: target/site/jacoco/ - - - name: Commit and push the coverage badges and summary file - if: ${{ github.event_name != 'pull_request' }} - run: | - cd .github/badges - if [[ `git status --porcelain *.svg *.json` ]]; then - git config --global user.name 'github-actions' - git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add *.svg *.json - git commit -m "Autogenerated JaCoCo coverage badges" *.svg *.json - git push - fi - -# - name: Comment on PR with coverage percentages -# if: ${{ github.event_name == 'pull_request' }} -# run: | -# REPORT=$(<.github/badges/coverage-summary.json) -# COVERAGE=$(jq -r '.coverage' <<< "$REPORT")% -# BRANCHES=$(jq -r '.branches' <<< "$REPORT")% -# NEWLINE=$'\n' -# BODY="## JaCoCo Test Coverage Summary Statistics${NEWLINE}* __Coverage:__ ${COVERAGE}${NEWLINE}* __Branches:__ ${BRANCHES}" -# gh pr comment ${{github.event.pull_request.number}} -b "${BODY}" -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 0edf6f6ee4375501a5c01ff8ce930010d6594282 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 08:55:41 +0100 Subject: [PATCH 4/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/org/privacyidea/AsyncRequestCallable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/privacyidea/AsyncRequestCallable.java b/src/main/java/org/privacyidea/AsyncRequestCallable.java index 1388e4b..850ecb0 100644 --- a/src/main/java/org/privacyidea/AsyncRequestCallable.java +++ b/src/main/java/org/privacyidea/AsyncRequestCallable.java @@ -85,7 +85,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO if (responseBody != null) { String s = responseBody.string(); - if (!privacyIDEA.logExcludedEndpoints().contains(path)) + if (!privacyIDEA.logExcludedEndpoints().contains(path) && !ENDPOINT_AUTH.equals(path)) { privacyIDEA.log(path + " (" + response.code() + "):\n" + privacyIDEA.parser.formatJson(s)); } From 259fd086fde850c99d30ceea5704c701d589abfc Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 08:55:47 +0100 Subject: [PATCH 5/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/org/privacyidea/AsyncRequestCallable.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/privacyidea/AsyncRequestCallable.java b/src/main/java/org/privacyidea/AsyncRequestCallable.java index 850ecb0..ba44831 100644 --- a/src/main/java/org/privacyidea/AsyncRequestCallable.java +++ b/src/main/java/org/privacyidea/AsyncRequestCallable.java @@ -77,9 +77,9 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { - // 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. + // For OkHttp, the response body is always available via `body()`, regardless of HTTP status. + // We must ensure the body is closed to prevent resource leaks, and it can only be consumed once. + // Using try-with-resources guarantees the body is properly closed after reading. try (ResponseBody responseBody = response.body()) { if (responseBody != null) From 04df91c9af74f7bb9ca90c5a47efadfb5d8c7a95 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 08:56:58 +0100 Subject: [PATCH 6/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/org/privacyidea/PIResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/privacyidea/PIResponse.java b/src/main/java/org/privacyidea/PIResponse.java index 481a116..39c00b7 100644 --- a/src/main/java/org/privacyidea/PIResponse.java +++ b/src/main/java/org/privacyidea/PIResponse.java @@ -169,7 +169,7 @@ private String reduceChallengeMessagesWhere(Predicate predicate) .reduce("", (a, s) -> a + s + ", ") .trim()); - if (!sb.isEmpty()) + if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } From e243e6bc2e50a5acdfe8e1c7718bd350a96ccacf Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 08:57:17 +0100 Subject: [PATCH 7/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/org/privacyidea/PIResponse.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/org/privacyidea/PIResponse.java b/src/main/java/org/privacyidea/PIResponse.java index 39c00b7..5b06b4f 100644 --- a/src/main/java/org/privacyidea/PIResponse.java +++ b/src/main/java/org/privacyidea/PIResponse.java @@ -149,14 +149,7 @@ private boolean isNotBlank(String str) { */ public String otpMessage() { - 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; - }); + return reduceChallengeMessagesWhere(c -> "interactive".equals(c.getClientMode())); } private String reduceChallengeMessagesWhere(Predicate predicate) From bc6baa005d4b71b5db28cfa41e800cb5777a1f34 Mon Sep 17 00:00:00 2001 From: Nils Behlen Date: Thu, 26 Mar 2026 09:00:46 +0100 Subject: [PATCH 8/8] remove badges, dont skip tests, update dep --- .github/badges/branches.svg | 1 - .github/badges/coverage-summary.json | 1 - .github/badges/jacoco.svg | 1 - pom.xml | 4 ++-- 4 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .github/badges/branches.svg delete mode 100644 .github/badges/coverage-summary.json delete mode 100644 .github/badges/jacoco.svg diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg deleted file mode 100644 index 28be886..0000000 --- a/.github/badges/branches.svg +++ /dev/null @@ -1 +0,0 @@ -branches54.1% \ No newline at end of file diff --git a/.github/badges/coverage-summary.json b/.github/badges/coverage-summary.json deleted file mode 100644 index 92e8263..0000000 --- a/.github/badges/coverage-summary.json +++ /dev/null @@ -1 +0,0 @@ -{"branches": 54.109589041095894, "coverage": 74.64907355418305} \ No newline at end of file diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg deleted file mode 100644 index 8db44d9..0000000 --- a/.github/badges/jacoco.svg +++ /dev/null @@ -1 +0,0 @@ -coverage74.6% \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2af6fcd..0f04fd3 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ maven-surefire-plugin 3.5.3 - true + false @@ -118,7 +118,7 @@ com.auth0 java-jwt - 4.4.0 + 4.5.1