diff --git a/README.md b/README.md
index 57c13a6e..70339d5c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+stuff stuff
+www
+xxx
+yyy
+uuuu
+
## FusionAuth Java Client 
If you're integrating FusionAuth with a Java application, this library will speed up your development time.
diff --git a/build.savant b/build.savant
index 1ed0c1b1..02164d8c 100644
--- a/build.savant
+++ b/build.savant
@@ -18,13 +18,13 @@ fusionauthJWTVersion = "5.2.4"
jacksonVersion = "2.21.1"
// No 2.21.0 version available, annotations dropped patch versioning in 2.20+
// pom target has hack to maintain this "patchless" version
-jacksonAnnotationsVersion = "2.21"
+jacksonAnnotationsVersion = "2.21"
jackson5Version = "3.0.1"
javaErrorVersion = "2.2.3"
-restifyVersion = "4.3.0"
+restifyVersion = "4.4.0"
testngVersion = "7.5.1"
-project(group: "io.fusionauth", name: "fusionauth-java-client", version: "1.64.0", licenses: ["ApacheV2_0"]) {
+project(group: "io.fusionauth", name: "fusionauth-java-client", version: "1.65.0", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
cache()
diff --git a/fusionauth-java-client.iml b/fusionauth-java-client.iml
index 387725d8..dad8415f 100644
--- a/fusionauth-java-client.iml
+++ b/fusionauth-java-client.iml
@@ -32,11 +32,11 @@
-
+
-
+
diff --git a/pom.xml b/pom.xml
index 352348b8..f763a4c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
io.fusionauth
fusionauth-java-client
- 1.64.0
+ 1.65.0
jar
FusionAuth Java Client Library
@@ -122,7 +122,7 @@
com.inversoft
restify
- 4.3.0
+ 4.4.0
jar
compile
false
diff --git a/src/main/java/io/fusionauth/client/FusionAuthClient.java b/src/main/java/io/fusionauth/client/FusionAuthClient.java
index 8d1d793c..75cd6747 100644
--- a/src/main/java/io/fusionauth/client/FusionAuthClient.java
+++ b/src/main/java/io/fusionauth/client/FusionAuthClient.java
@@ -34,6 +34,7 @@
import com.inversoft.rest.JSONBodyHandler;
import com.inversoft.rest.JSONResponseHandler;
import com.inversoft.rest.RESTClient;
+import com.inversoft.rest.RetryConfiguration;
import io.fusionauth.domain.LambdaType;
import io.fusionauth.domain.OpenIdConfiguration;
import io.fusionauth.domain.api.APIKeyRequest;
@@ -284,6 +285,18 @@ public class FusionAuthClient {
.registerModule(new JacksonModule())
.registerModule(new FusionAuthJacksonModule());
+ /**
+ * Suggested RetryConfiguration to use that retries on 409s when the general error code is [retryableConflict]
+ */
+ public static RetryConfiguration BASIC_RETRY_CONFIGURATION = new RetryConfiguration().with(rc -> rc.retryFunction = clientResponse -> {
+ if (clientResponse.status == 409 && clientResponse.errorResponse instanceof Errors) {
+ Errors errors = (Errors) clientResponse.errorResponse;
+ return errors.generalErrors.stream().anyMatch(e -> e.code.equals("[retryableConflict]"));
+ }
+ return false;
+ }
+ );
+
private final String apiKey;
private final String baseURL;
@@ -296,6 +309,8 @@ public class FusionAuthClient {
public int readTimeout;
+ public RetryConfiguration retryConfiguration;
+
public FusionAuthClient(String apiKey, String baseURL) {
this(apiKey, baseURL, null);
}
@@ -335,7 +350,9 @@ public FusionAuthClient setTenantId(UUID tenantId) {
return this;
}
- return new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId.toString());
+ FusionAuthClient client = new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId.toString());
+ client.retryConfiguration = this.retryConfiguration;
+ return client;
}
/**
@@ -346,7 +363,9 @@ public FusionAuthClient setTenantId(UUID tenantId) {
* @return the new FusionAuthClient
*/
public FusionAuthClient setObjectMapper(ObjectMapper objectMapper) {
- return new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId, objectMapper);
+ FusionAuthClient client = new FusionAuthClient(apiKey, baseURL, connectTimeout, readTimeout, tenantId, objectMapper);
+ client.retryConfiguration = this.retryConfiguration;
+ return client;
}
/**
@@ -6427,6 +6446,10 @@ protected RESTClient startAnonymous(Class type, Class errorTy
client.header(TENANT_ID_HEADER, tenantId);
}
+ if (retryConfiguration != null) {
+ client.retry(retryConfiguration);
+ }
+
return client;
}
diff --git a/src/main/java/io/fusionauth/client/JWTManager.java b/src/main/java/io/fusionauth/client/JWTManager.java
deleted file mode 100644
index 5355168d..00000000
--- a/src/main/java/io/fusionauth/client/JWTManager.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (c) 2018-2024, FusionAuth, All Rights Reserved
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific
- * language governing permissions and limitations under the License.
- */
-package io.fusionauth.client;
-
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-import io.fusionauth.jwt.domain.JWT;
-
-/**
- * A singleton cache of JWTs that have been revoked. This should be connected via a Webhook that listens for the
- * jwt.refresh-token.revoke event from FusionAuth.
- *
- * @author Brian Pontarelli
- */
-public class JWTManager {
- private static final ScheduledThreadPoolExecutor executorService;
-
- // Application Id -> Revocation Context
- // - Used to revoke Refresh Tokens by Application
- private static final Map revokedByApplication = new ConcurrentHashMap<>();
-
- // User Id -> Revocation Context
- // - Used to revoke Refresh Tokens by Application
- private static final Map> revokedByUser = new ConcurrentHashMap<>();
-
- // Refresh Token Id -> Expiration
- // - Used to revoke individual Refresh Tokens
- private static final Map revokedRefreshTokens = new ConcurrentHashMap<>();
-
- /**
- * Determines if a given JWT object is valid or not. This checks if the subject in the JWT is in the list of revoked
- * subjects (user ids) and if the expiration of the JWT is before the expiration in the Map of revoked JWTs.
- *
- * @param jwt The JWT to check.
- * @return True if the JWT is valid, false it not.
- */
- public static boolean isValid(JWT jwt) {
- boolean result;
-
- // 1. Look for revoked Refresh Tokens by Refresh Token Id
- try {
- String refreshTokenId = jwt.getString("sid");
- if (refreshTokenId != null) {
- ZonedDateTime expiration = revokedRefreshTokens.get(UUID.fromString(refreshTokenId));
- if (expiration != null) {
- result = expiration.isBefore(jwt.expiration);
- if (!result) {
- return false;
- }
- }
- }
- } catch (Exception ignore) {
- }
-
- // 2. Look for revoked Refresh Tokens by Application
- try {
- String applicationId = jwt.getString("applicationId");
- if (applicationId != null) {
- ZonedDateTime expiration = revokedByApplication.get(UUID.fromString(applicationId));
- if (expiration != null) {
- result = expiration.isBefore(jwt.expiration);
- if (!result) {
- return false;
- }
- }
-
- // 3. Look for revoked Refresh Tokens by User Id
- String userId = jwt.subject;
- if (userId != null) {
- Map context = revokedByUser.get(UUID.fromString(userId));
- if (context != null) {
- expiration = context.get(UUID.fromString(applicationId));
- if (expiration != null) {
- result = expiration.isBefore(jwt.expiration);
- if (!result) {
- return false;
- }
- }
- }
- }
- }
-
- } catch (Exception ignore) {
- }
-
- return true;
- }
-
- public static void reset() {
- revokedByApplication.clear();
- revokedByUser.clear();
- revokedRefreshTokens.clear();
- }
-
- public static void revokeByApplication(UUID applicationId, int durationsInSeconds) {
- ZonedDateTime expiration = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(durationsInSeconds);
- revokedByApplication.put(applicationId, expiration);
- }
-
- public static void revokeByRefreshToken(UUID refreshTokenId, int durationInSeconds) {
- ZonedDateTime expiration = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(durationInSeconds);
- revokedRefreshTokens.put(refreshTokenId, expiration);
- }
-
- public static void revokedByUser(UUID userId, Map durationsInSeconds) {
- Map context = revokedByUser.computeIfAbsent(userId, key -> new HashMap<>());
- for (UUID applicationId : durationsInSeconds.keySet()) {
- ZonedDateTime expiration = ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(durationsInSeconds.get(applicationId));
- context.put(applicationId, expiration);
- }
- }
-
- static {
- executorService = new ScheduledThreadPoolExecutor(1, r -> {
- Thread t = new Thread(r);
- t.setName("JWTManager Thread");
- t.setDaemon(true);
- return t;
- });
-
-
- executorService.schedule(() -> {
- ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
- revokedByUser.values().forEach(m -> m.entrySet().removeIf(e -> e.getValue().isBefore(now)));
- revokedByApplication.entrySet().removeIf(e -> e.getValue().isBefore(now));
- revokedRefreshTokens.entrySet().removeIf(e -> e.getValue().isBefore(now));
- }, 7, TimeUnit.SECONDS);
- }
-}
diff --git a/src/main/java/io/fusionauth/domain/search/WebhookEventLogSearchCriteria.java b/src/main/java/io/fusionauth/domain/search/WebhookEventLogSearchCriteria.java
index 77480873..02420ec1 100644
--- a/src/main/java/io/fusionauth/domain/search/WebhookEventLogSearchCriteria.java
+++ b/src/main/java/io/fusionauth/domain/search/WebhookEventLogSearchCriteria.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025, FusionAuth, All Rights Reserved
+ * Copyright (c) 2024-2026, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.