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 ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) 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.