diff --git a/Changelog.md b/Changelog.md index e7c849c..156180e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,14 @@ # Changelog +### v1.3.0 - 0 Apr 2025 + +* Passkey functions +* JWT will be reused and renewed automatically +* Added option to add arbitrary parameters to the requests + ### v1.2.2 - 5 Mar 2024 -* Fixed a problem with the threadpool where thread would not time out and accumulate over time +* Fixed a problem with the thread pool where thread would not time out and accumulate over time * Added the option to set http timeouts ### v1.2.1 - 9 Aug 2023 @@ -42,4 +48,4 @@ ### v0.1 - 18 Sep 2020 * First version -* Supports basic OTP token +* Supports basic OTP token \ No newline at end of file diff --git a/pom.xml b/pom.xml index 54179b0..b6a5432 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.privacyidea privacyidea-java-client - 1.2.2 + 1.3.0 jar UTF-8 @@ -16,7 +16,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.12.4 + 3.5.3 false @@ -50,8 +50,8 @@ maven-compiler-plugin 3.13.0 - 11 - 11 + 14 + 14 @@ -121,4 +121,4 @@ 4.4.0 - \ No newline at end of file + diff --git a/src/main/java/org/privacyidea/AsyncRequestCallable.java b/src/main/java/org/privacyidea/AsyncRequestCallable.java index 7615584..26f00ef 100644 --- a/src/main/java/org/privacyidea/AsyncRequestCallable.java +++ b/src/main/java/org/privacyidea/AsyncRequestCallable.java @@ -38,35 +38,25 @@ public class AsyncRequestCallable implements Callable, Callback private final String method; private final Map headers; private final Map params; - private final boolean authTokenRequired; private final Endpoint endpoint; private final PrivacyIDEA privacyIDEA; final String[] callbackResult = {null}; private CountDownLatch latch; public AsyncRequestCallable(PrivacyIDEA privacyIDEA, Endpoint endpoint, String path, Map params, - Map headers, boolean authTokenRequired, String method) + Map headers, String method) { this.privacyIDEA = privacyIDEA; this.endpoint = endpoint; this.path = path; this.params = params; this.headers = headers; - this.authTokenRequired = authTokenRequired; this.method = method; } @Override public String call() throws Exception { - // If an auth token is required for the request, get that first then do the actual request - if (this.authTokenRequired) - { - // Wait for the auth token to be retrieved and add it to the header - headers.put(PIConstants.HEADER_AUTHORIZATION, privacyIDEA.getJWT()); - } - - // Do the actual request latch = new CountDownLatch(1); endpoint.sendRequestAsync(path, params, headers, method, this); if (!latch.await(30, TimeUnit.SECONDS)) @@ -90,7 +80,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO if (response.body() != null) { String s = response.body().string(); - if (!privacyIDEA.logExcludedEndpoints().contains(path) && !ENDPOINT_AUTH.equals(path)) + if (!privacyIDEA.logExcludedEndpoints().contains(path)) { privacyIDEA.log(path + ":\n" + privacyIDEA.parser.formatJson(s)); } diff --git a/src/main/java/org/privacyidea/Endpoint.java b/src/main/java/org/privacyidea/Endpoint.java index 24cbeae..efdcde1 100644 --- a/src/main/java/org/privacyidea/Endpoint.java +++ b/src/main/java/org/privacyidea/Endpoint.java @@ -118,11 +118,6 @@ void sendRequestAsync(String endpoint, Map params, Map { @@ -150,7 +145,17 @@ void sendRequestAsync(String endpoint, Map params, Map + { + if (v == null) + { + privacyIDEA.error("Unable to add header " + k + " because the value is null"); + } + else + { + requestBuilder.addHeader(k, v); + } + }); } if (POST.equals(method)) diff --git a/src/main/java/org/privacyidea/JSONParser.java b/src/main/java/org/privacyidea/JSONParser.java index 70ea860..84b4495 100644 --- a/src/main/java/org/privacyidea/JSONParser.java +++ b/src/main/java/org/privacyidea/JSONParser.java @@ -65,7 +65,7 @@ public String formatJson(String json) * @param serverResponse response of the server * @return the AuthToken obj or null if error */ - LinkedHashMap extractAuthToken(String serverResponse) + LinkedHashMap getJWT(String serverResponse) { if (serverResponse != null && !serverResponse.isEmpty()) { @@ -75,21 +75,22 @@ LinkedHashMap extractAuthToken(String serverResponse) try { JsonObject obj = root.getAsJsonObject(); - String authToken = obj.getAsJsonObject(RESULT).getAsJsonObject(VALUE).getAsJsonPrimitive(TOKEN).getAsString(); - var parts = authToken.split("\\."); + String jwt = obj.getAsJsonObject(RESULT).getAsJsonObject(VALUE).getAsJsonPrimitive(TOKEN).getAsString(); + var parts = jwt.split("\\."); String dec = new String(Base64.getDecoder().decode(parts[1])); // Extract the expiration date from the token - int respDate = obj.getAsJsonPrimitive(TIME).getAsInt(); - int expDate = JsonParser.parseString(dec).getAsJsonObject().getAsJsonPrimitive(EXP).getAsInt(); - int difference = expDate - respDate; - privacyIDEA.log("JWT Validity: " + difference / 60 + " minutes. Token expires at: " + new Date(expDate * 1000L)); + int responseTime = obj.getAsJsonPrimitive(TIME).getAsInt(); + int expirationTime = JsonParser.parseString(dec).getAsJsonObject().getAsJsonPrimitive(EXP).getAsInt(); + int difference = expirationTime - responseTime; + privacyIDEA.log("JWT Validity: " + difference / 60 + " minutes. Token expires at: " + new Date(expirationTime * 1000L)); - return new LinkedHashMap<>(Map.of(AUTH_TOKEN, authToken, AUTH_TOKEN_EXP, String.valueOf(expDate))); + return new LinkedHashMap<>(Map.of(JWT, jwt, JWT_EXPIRATION_TIME, String.valueOf(expirationTime))); } catch (Exception e) { - privacyIDEA.error("Auth token extraction failed: " + e); + privacyIDEA.error("JWT token extraction failed: " + e); + privacyIDEA.error("Server response: " + serverResponse); } } } diff --git a/src/main/java/org/privacyidea/PIConfig.java b/src/main/java/org/privacyidea/PIConfig.java index cf9326d..21c4303 100644 --- a/src/main/java/org/privacyidea/PIConfig.java +++ b/src/main/java/org/privacyidea/PIConfig.java @@ -26,7 +26,6 @@ class PIConfig public String serviceAccountPass = ""; public String serviceAccountRealm = ""; public boolean disableLog = false; - public String forwardClientIP = ""; public int httpTimeoutMs = 30000; protected String proxyHost = ""; protected int proxyPort = 0; diff --git a/src/main/java/org/privacyidea/PIConstants.java b/src/main/java/org/privacyidea/PIConstants.java index efcd378..5735fd3 100644 --- a/src/main/java/org/privacyidea/PIConstants.java +++ b/src/main/java/org/privacyidea/PIConstants.java @@ -51,8 +51,8 @@ public class PIConstants public static final String TIME = "time"; public static final String EXP = "exp"; public static final String CHALLENGE_STATUS = "challenge_status"; - public static final String AUTH_TOKEN = "authToken"; - public static final String AUTH_TOKEN_EXP = "authTokenExp"; + public static final String JWT = "jwt"; + public static final String JWT_EXPIRATION_TIME = "jwt_expiration_time"; public static final String TYPE = "type"; public static final String TRANSACTION_ID = "transaction_id"; public static final String REALM = "realm"; diff --git a/src/main/java/org/privacyidea/PrivacyIDEA.java b/src/main/java/org/privacyidea/PrivacyIDEA.java index b8dd308..607d6a3 100644 --- a/src/main/java/org/privacyidea/PrivacyIDEA.java +++ b/src/main/java/org/privacyidea/PrivacyIDEA.java @@ -41,8 +41,8 @@ public class PrivacyIDEA implements Closeable private CountDownLatch jwtRetrievalLatch; final JSONParser parser; // Responses from these endpoints will not be logged. The list can be overwritten. - private List logExcludedEndpoints = Arrays.asList(PIConstants.ENDPOINT_AUTH, - PIConstants.ENDPOINT_POLLTRANSACTION); //Collections.emptyList(); + private List logExcludedEndpoints = Arrays.asList( + PIConstants.ENDPOINT_POLLTRANSACTION); //Collections.emptyList();PIConstants.ENDPOINT_AUTH, private PrivacyIDEA(PIConfig configuration, IPILogger logger, IPISimpleLogger simpleLog) { @@ -56,6 +56,10 @@ private PrivacyIDEA(PIConfig configuration, IPILogger logger, IPISimpleLogger si { retrieveJWT(); } + else + { + error("No service account configured. No JWT will be retrieved."); + } } /** @@ -95,10 +99,11 @@ public PIResponse validateCheck(String username, String pass, String transaction * Which parameters to send depends on the use case and how privacyIDEA is configured. * (E.g. this can also be used to trigger challenges without a service account) * - * @param username username - * @param pass pass/otp value - * @param transactionID optional, will be appended if set - * @param headers optional headers for the request + * @param username username + * @param pass pass/otp value + * @param transactionID optional, will be appended if set + * @param additionalParams additional parameters for the request + * @param headers optional headers for the request * @return PIResponse object containing the response or null if error */ public PIResponse validateCheck(String username, String pass, String transactionID, Map additionalParams, @@ -108,27 +113,27 @@ public PIResponse validateCheck(String username, String pass, String transaction } /** - * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map) + * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map, Map) */ public PIResponse validateCheckSerial(String serial, String pass) { - return this.validateCheckSerial(serial, pass, null, Collections.emptyMap()); + return this.validateCheckSerial(serial, pass, null, Collections.emptyMap(), Collections.emptyMap()); } /** - * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map) + * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map, Map) */ public PIResponse validateCheckSerial(String serial, String pass, Map headers) { - return this.validateCheckSerial(serial, pass, null, headers); + return this.validateCheckSerial(serial, pass, null, Collections.emptyMap(), headers); } /** - * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map) + * @see PrivacyIDEA#validateCheckSerial(String, String, String, Map, Map) */ public PIResponse validateCheckSerial(String serial, String pass, String transactionID) { - return this.validateCheckSerial(serial, pass, transactionID, Collections.emptyMap()); + return this.validateCheckSerial(serial, pass, transactionID, Collections.emptyMap(), Collections.emptyMap()); } /** @@ -139,49 +144,44 @@ public PIResponse validateCheckSerial(String serial, String pass, String transac * @param transactionID transaction ID * @return PIResponse or null if error */ - public PIResponse validateCheckSerial(String serial, String pass, String transactionID, Map headers) + public PIResponse validateCheckSerial(String serial, String pass, String transactionID, Map additionalParams, + Map headers) { - return getPIResponse(SERIAL, serial, pass, headers, transactionID, Collections.emptyMap()); + return getPIResponse(SERIAL, serial, pass, headers, transactionID, additionalParams); } /** * Used by validateCheck and validateCheckSerial to get the PI Response. * - * @param type distinguish between user and serial to set forwarded input to the right PI-request param - * @param input forwarded username for classic validateCheck or serial to trigger exact token - * @param pass OTP, PIN+OTP or password to use - * @param headers optional headers for the request - * @param transactionID optional, will be appended if set + * @param type distinguish between user and serial to set forwarded input to the right PI-request param + * @param input forwarded username for classic validateCheck or serial to trigger exact token + * @param pass OTP, PIN+OTP or password to use + * @param headers optional headers for the request + * @param transactionID optional, will be appended if set + * @param additionalParams additional parameters for the request * @return PIResponse object containing the response or null if error */ private PIResponse getPIResponse(String type, String input, String pass, Map headers, String transactionID, Map additionalParams) { - Map params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(additionalParams); params.put(type, input); params.put(PASS, (pass != null ? pass : "")); - params.putAll(additionalParams); appendRealm(params); if (transactionID != null && !transactionID.isEmpty()) { params.put(TRANSACTION_ID, transactionID); } String response = runRequestAsync(ENDPOINT_VALIDATE_CHECK, params, headers, false, POST); - // Shutdown the scheduler if user successfully authenticated - PIResponse piResponse = this.parser.parsePIResponse(response); - if (piResponse != null && piResponse.value) - { - this.scheduler.shutdownNow(); - } - return piResponse; + return this.parser.parsePIResponse(response); } /** - * @see PrivacyIDEA#validateCheckWebAuthn(String, String, String, String, Map) + * @see PrivacyIDEA#validateCheckWebAuthn(String, String, String, String, Map, Map) */ public PIResponse validateCheckWebAuthn(String user, String transactionID, String signResponse, String origin) { - return this.validateCheckWebAuthn(user, transactionID, signResponse, origin, Collections.emptyMap()); + return this.validateCheckWebAuthn(user, transactionID, signResponse, origin, Collections.emptyMap(), Collections.emptyMap()); } /** @@ -191,13 +191,14 @@ public PIResponse validateCheckWebAuthn(String user, String transactionID, Strin * @param transactionID transaction ID * @param webAuthnSignResponse the WebAuthnSignResponse as returned from the browser * @param origin server name that was used for + * @param additionalParams additional parameters for the request * @param headers optional headers for the request * @return PIResponse or null if error */ public PIResponse validateCheckWebAuthn(String user, String transactionID, String webAuthnSignResponse, String origin, - Map headers) + Map additionalParams, Map headers) { - Map params = new LinkedHashMap<>(); + Map params = new LinkedHashMap<>(additionalParams); // Standard validateCheck data params.put(USER, user); params.put(TRANSACTION_ID, transactionID); @@ -287,34 +288,35 @@ public PIResponse validateCheckCompletePasskeyRegistration(String transactionID, } /** - * @see PrivacyIDEA#triggerChallenges(String, Map) + * @see PrivacyIDEA#triggerChallenges(String, Map, Map) */ public PIResponse triggerChallenges(String username) { - return this.triggerChallenges(username, new LinkedHashMap<>()); + return this.triggerChallenges(username, Collections.emptyMap(), Collections.emptyMap()); } /** * Trigger all challenges for the given username. This requires a service account to be set. * - * @param username username to trigger challenges for - * @param headers optional headers for the request + * @param username username to trigger challenges for + * @param additionalParams additional parameters for the request + * @param headers optional headers for the request * @return the server response or null if error */ - public PIResponse triggerChallenges(String username, Map headers) + public PIResponse triggerChallenges(String username, Map additionalParams, Map headers) { Objects.requireNonNull(username, "Username is required!"); - if (!serviceAccountAvailable()) { log("No service account configured. Cannot trigger challenges"); return null; } - Map params = new LinkedHashMap<>(); + Map headersCopy = new LinkedHashMap<>(headers); + Map params = new LinkedHashMap<>(additionalParams); params.put(USER, username); appendRealm(params); - String response = runRequestAsync(ENDPOINT_TRIGGERCHALLENGE, params, headers, true, POST); + String response = runRequestAsync(ENDPOINT_TRIGGERCHALLENGE, params, headersCopy, true, POST); return this.parser.parsePIResponse(response); } @@ -448,30 +450,31 @@ private void appendRealm(Map params) */ private void retrieveJWT() { + log("Getting new JWT with service account..."); this.jwtRetrievalLatch = new CountDownLatch(1); try { String response = runRequestAsync(ENDPOINT_AUTH, serviceAccountParam(), Collections.emptyMap(), false, POST); if (response == null) { - error("Failed to retrieve auth token, response was empty. Retrying in 10 seconds."); + error("Failed to retrieve JWT: Response was empty. Retrying in 10 seconds."); this.scheduler.schedule(this::retrieveJWT, 10, TimeUnit.SECONDS); } else { - LinkedHashMap authTokenMap = parser.extractAuthToken(response); - this.jwt = authTokenMap.get(AUTH_TOKEN); - long authTokenExp = Integer.parseInt(authTokenMap.get(AUTH_TOKEN_EXP)); + LinkedHashMap jwtMap = parser.getJWT(response); + this.jwt = jwtMap.get(JWT); + long jwtExpiration = Integer.parseInt(jwtMap.get(JWT_EXPIRATION_TIME)); // Schedule the next token retrieval to 1 min before expiration - long delay = Math.max(1, authTokenExp - 60 - (System.currentTimeMillis() / 1000L)); + long delay = Math.max(1, jwtExpiration - 60 - (System.currentTimeMillis() / 1000L)); this.scheduler.schedule(this::retrieveJWT, delay, TimeUnit.SECONDS); log("Next JWT retrieval in " + delay + " seconds."); } } catch (Exception e) { - error("Failed to retrieve auth token: " + e.getMessage()); + error("Failed to retrieve JWT: " + e.getMessage()); } this.jwtRetrievalLatch.countDown(); } @@ -483,12 +486,17 @@ private void retrieveJWT() */ public String getJWT() { + if (jwtRetrievalLatch.getCount() == 0 && this.jwt == null) + { + retrieveJWT(); + } try { jwtRetrievalLatch.await(); } catch (InterruptedException e) { + error("Error while waiting for JWT retrieval: " + e.getMessage()); error(e); return null; } @@ -508,21 +516,22 @@ public boolean serviceAccountAvailable() * Run a request in a thread of the thread pool. Then join that thread to the one that was calling this method. * If the server takes longer to answer a request, the other requests do not have to wait. * - * @param path path to the endpoint of the privacyIDEA server - * @param params request parameters - * @param headers request headers - * @param authTokenRequired whether an auth token should be acquired prior to the request - * @param method http request method + * @param path path to the endpoint of the privacyIDEA server + * @param params request parameters + * @param headers request headers + * @param authorizationRequired whether an JWT for Authorization should be acquired prior to the request. Requires a service account. + * @param method http request method * @return response of the server as string or null */ - private String runRequestAsync(String path, Map params, Map headers, boolean authTokenRequired, + private String runRequestAsync(String path, Map params, Map headers, boolean authorizationRequired, String method) { - if (!configuration.forwardClientIP.isEmpty()) + if (authorizationRequired) { - params.put(CLIENT_IP, configuration.forwardClientIP); + // Wait for the JWT to be retrieved and add it to the header + headers.put(PIConstants.HEADER_AUTHORIZATION, getJWT()); } - Callable callable = new AsyncRequestCallable(this, this.endpoint, path, params, headers, authTokenRequired, method); + Callable callable = new AsyncRequestCallable(this, this.endpoint, path, params, headers, method); Future future = this.threadPool.submit(callable); String response = null; try @@ -679,11 +688,10 @@ public static class Builder private String serviceAccountName = ""; private String serviceAccountPass = ""; private String serviceAccountRealm = ""; - private String forwardClientIP = ""; private IPILogger logger = null; private boolean disableLog = false; private IPISimpleLogger simpleLogBridge = null; - private int httpTimeoutMs = 30000; + private int httpTimeoutMs = 10000; private String proxyHost = ""; private int proxyPort = 0; @@ -774,18 +782,6 @@ public Builder serviceRealm(String serviceAccountRealm) return this; } - /** - * Set the client IP to be forwarded to the privacyIDEA server. - * - * @param clientIP client IP or an empty String - * @return Builder - */ - public Builder forwardClientIP(String clientIP) - { - this.forwardClientIP = clientIP; - return this; - } - /** * Disable logging completely regardless of any set loggers. * @@ -837,7 +833,6 @@ public PrivacyIDEA build() configuration.serviceAccountName = serviceAccountName; configuration.serviceAccountPass = serviceAccountPass; configuration.serviceAccountRealm = serviceAccountRealm; - configuration.forwardClientIP = forwardClientIP; configuration.disableLog = disableLog; configuration.httpTimeoutMs = httpTimeoutMs; configuration.setProxy(proxyHost, proxyPort); diff --git a/src/test/java/org/privacyidea/TestTriggerChallenge.java b/src/test/java/org/privacyidea/TestTriggerChallenge.java index af217d7..0be106b 100644 --- a/src/test/java/org/privacyidea/TestTriggerChallenge.java +++ b/src/test/java/org/privacyidea/TestTriggerChallenge.java @@ -16,6 +16,7 @@ */ package org.privacyidea; +import java.util.Collections; import java.util.List; import org.junit.After; import org.junit.Before; @@ -35,7 +36,7 @@ public class TestTriggerChallenge private PrivacyIDEA privacyIDEA; String serviceAccount = "service"; String servicePass = "pass"; - String forwardClientIP = "127.0.0.1"; + String clientIP = "127.0.0.1"; @Before public void setup() @@ -47,13 +48,11 @@ public void setup() public void testTriggerChallengeSuccess() { mockServer.when(HttpRequest.request().withPath(PIConstants.ENDPOINT_AUTH).withMethod("POST").withBody("")) - .respond(HttpResponse.response() - .withBody(Utils.postAuthSuccessResponse())); + .respond(HttpResponse.response().withBody(Utils.postAuthSuccessResponse())); privacyIDEA = PrivacyIDEA.newBuilder("https://127.0.0.1:1080", "test") .verifySSL(false) .serviceAccount(serviceAccount, servicePass) - .forwardClientIP(forwardClientIP) .logger(new PILogImplementation()) .realm("realm") .build(); @@ -61,26 +60,27 @@ public void testTriggerChallengeSuccess() mockServer.when(HttpRequest.request() .withPath(PIConstants.ENDPOINT_TRIGGERCHALLENGE) .withMethod("POST") - .withBody("user=testuser&realm=realm&client=127.0.0.1")) + .withBody("clientip=127.0.0.1&user=testuser&realm=realm")) .respond(HttpResponse.response().withBody(Utils.triggerChallengeSuccess())); String username = "testuser"; - PIResponse responseTriggerChallenge = privacyIDEA.triggerChallenges(username); - - assertEquals("otp", responseTriggerChallenge.preferredClientMode); - assertEquals(1, responseTriggerChallenge.id); - assertEquals("BittegebenSieeinenOTP-Wertein:", responseTriggerChallenge.message); - assertEquals("2.0", responseTriggerChallenge.jsonRPCVersion); - assertEquals("3.6.3", responseTriggerChallenge.piVersion); - assertEquals("rsa_sha256_pss:4b0f0e12c2...89409a2e65c87d27b", responseTriggerChallenge.signature); + PIResponse response = privacyIDEA.triggerChallenges(username, Collections.singletonMap("clientip", clientIP), + Collections.emptyMap()); + + assertEquals("otp", response.preferredClientMode); + assertEquals(1, response.id); + assertEquals("BittegebenSieeinenOTP-Wertein:", response.message); + assertEquals("2.0", response.jsonRPCVersion); + assertEquals("3.6.3", response.piVersion); + assertEquals("rsa_sha256_pss:4b0f0e12c2...89409a2e65c87d27b", response.signature); // Trim all whitespaces, newlines - assertEquals(Utils.triggerChallengeSuccess().replaceAll("[\n\r]", ""), responseTriggerChallenge.rawMessage.replaceAll("[\n\r]", "")); - assertEquals(Utils.triggerChallengeSuccess().replaceAll("[\n\r]", ""), responseTriggerChallenge.toString().replaceAll("[\n\r]", "")); + assertEquals(Utils.triggerChallengeSuccess().replaceAll("[\n\r]", ""), response.rawMessage.replaceAll("[\n\r]", "")); + assertEquals(Utils.triggerChallengeSuccess().replaceAll("[\n\r]", ""), response.toString().replaceAll("[\n\r]", "")); // result - assertTrue(responseTriggerChallenge.status); - assertFalse(responseTriggerChallenge.value); + assertTrue(response.status); + assertFalse(response.value); - List challenges = responseTriggerChallenge.multiChallenge; + List challenges = response.multiChallenge; String imageTOTP = ""; for (Challenge c : challenges) { @@ -98,10 +98,7 @@ public void testTriggerChallengeSuccess() @Test public void testNoServiceAccount() { - privacyIDEA = PrivacyIDEA.newBuilder("https://127.0.0.1:1080", "test") - .verifySSL(false) - .logger(new PILogImplementation()) - .build(); + privacyIDEA = PrivacyIDEA.newBuilder("https://127.0.0.1:1080", "test").verifySSL(false).logger(new PILogImplementation()).build(); PIResponse responseTriggerChallenge = privacyIDEA.triggerChallenges("Test"); @@ -112,8 +109,7 @@ public void testNoServiceAccount() public void testNoUsername() { mockServer.when(HttpRequest.request().withPath(PIConstants.ENDPOINT_AUTH).withMethod("POST").withBody("")) - .respond(HttpResponse.response() - .withBody(Utils.postAuthSuccessResponse())); + .respond(HttpResponse.response().withBody(Utils.postAuthSuccessResponse())); privacyIDEA = PrivacyIDEA.newBuilder("https://127.0.0.1:1080", "test") .verifySSL(false)