From ea9249560b0e5918314d4fdbde77752c41250377 Mon Sep 17 00:00:00 2001 From: Taha El Amine Kassabi Date: Thu, 10 Sep 2026 15:42:18 +0200 Subject: [PATCH 1/3] feat: configure client-credentials token refresh timing Allow setting the expiry buffer and jitter while preserving existing defaults. Include refresh settings in the OAuth client cache key. Co-Authored-By: Codex GPT-5 --- README.md | 19 +++++++- .../dev/openfga/sdk/api/auth/AccessToken.java | 15 +++---- .../openfga/sdk/api/auth/OAuth2Client.java | 8 +++- .../dev/openfga/sdk/api/client/ApiClient.java | 11 ++++- .../api/configuration/ClientCredentials.java | 36 +++++++++++++++ .../openfga/sdk/api/auth/AccessTokenTest.java | 6 ++- .../openfga/sdk/api/client/ApiClientTest.java | 45 ++++++++++++++++--- .../configuration/ClientCredentialsTest.java | 6 +++ 8 files changed, 125 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5aa19029..1cd11e96 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,24 @@ public class Example { } ``` +#### Token refresh timing + +Client-credentials tokens are cached until their expiry minus a buffer and random jitter. +Both settings default to 300 seconds; jitter is sampled from zero up to, but excluding, +the configured value on each validity check. For short-lived tokens, configure a smaller window: + +```java +var credentials = new ClientCredentials() + .clientId(System.getenv("FGA_CLIENT_ID")) + .clientSecret(System.getenv("FGA_CLIENT_SECRET")) + .apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) + .tokenExpiryBufferSeconds(30) + .tokenExpiryJitterSeconds(5); +``` + +Values must be non-negative. Set jitter to zero to disable it. Keep the combined window +below the token lifetime to allow cached tokens to be reused. + ### Custom Headers #### Default Headers @@ -1514,4 +1532,3 @@ See [CONTRIBUTING](./CONTRIBUTING.md) for details. This project is licensed under the Apache-2.0 license. See the [LICENSE](https://github.com/openfga/java-sdk/blob/main/LICENSE) file for more info. The code in this repo was auto generated by [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) from a template based on the [Java template](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/Java), licensed under the [Apache License 2.0](https://github.com/OpenAPITools/openapi-generator/blob/master/LICENSE). - diff --git a/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java b/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java index 3732af0a..840f1cd0 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java +++ b/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java @@ -2,7 +2,6 @@ import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; -import dev.openfga.sdk.constants.FgaConstants; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.concurrent.ThreadLocalRandom; @@ -13,17 +12,13 @@ * even if there is some clock skew or delay between retrieval and use. */ record AccessToken(String token, Instant expiresAt) { - private static final int TOKEN_EXPIRY_BUFFER_THRESHOLD_IN_SEC = FgaConstants.TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC; - // We add some jitter so that token refreshes are less likely to collide - private static final int TOKEN_EXPIRY_JITTER_IN_SEC = FgaConstants.TOKEN_EXPIRY_JITTER_IN_SEC; - static final AccessToken EMPTY = new AccessToken(null, null); AccessToken { expiresAt = expiresAt != null ? expiresAt.truncatedTo(ChronoUnit.SECONDS) : null; } - boolean isValid() { + boolean isValid(int bufferSeconds, int jitterSeconds) { if (isNullOrWhitespace(token)) { return false; } @@ -33,11 +28,11 @@ boolean isValid() { return true; } - // A token should be considered valid until 5 minutes before the expiry with some jitter - // to account for multiple calls to `isValid` at the same time and prevent multiple refresh calls + // Refresh before expiry, with optional jitter to spread refreshes across clients. Instant expiresWithLeeway = expiresAt - .minusSeconds(TOKEN_EXPIRY_BUFFER_THRESHOLD_IN_SEC) - .minusSeconds(ThreadLocalRandom.current().nextInt(TOKEN_EXPIRY_JITTER_IN_SEC)) + .minusSeconds(bufferSeconds) + .minusSeconds( + jitterSeconds == 0 ? 0 : ThreadLocalRandom.current().nextInt(jitterSeconds)) .truncatedTo(ChronoUnit.SECONDS); return Instant.now().truncatedTo(ChronoUnit.SECONDS).isBefore(expiresWithLeeway); diff --git a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java index 166f3514..cdf17225 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -21,6 +21,8 @@ public class OAuth2Client { private final CredentialsFlowRequest authRequest; private final Configuration config; private final Telemetry telemetry; + private final int tokenExpiryBufferSeconds; + private final int tokenExpiryJitterSeconds; /** * Initializes a new instance of the {@link OAuth2Client} class @@ -29,6 +31,8 @@ public class OAuth2Client { */ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException { var clientCredentials = configuration.getCredentials().getClientCredentials(); + this.tokenExpiryBufferSeconds = clientCredentials.getTokenExpiryBufferSeconds(); + this.tokenExpiryJitterSeconds = clientCredentials.getTokenExpiryJitterSeconds(); this.apiClient = apiClient; this.authRequest = @@ -54,7 +58,7 @@ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws Fga public CompletableFuture getAccessToken() throws FgaInvalidParameterException, ApiException { // Fast path (lock-free): return cached token if still valid. AccessToken current = snapshot.get(); - if (current.isValid()) { + if (current.isValid(tokenExpiryBufferSeconds, tokenExpiryJitterSeconds)) { return CompletableFuture.completedFuture(current.token()); } @@ -62,7 +66,7 @@ public CompletableFuture getAccessToken() throws FgaInvalidParameterExce synchronized (this) { // Double-check: another thread may have refreshed while we waited. AccessToken rechecked = snapshot.get(); - if (rechecked.isValid()) { + if (rechecked.isValid(tokenExpiryBufferSeconds, tokenExpiryJitterSeconds)) { return CompletableFuture.completedFuture(rechecked.token()); } diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java index 82a2d7ee..6e898f70 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java @@ -415,6 +415,8 @@ private static final class CredentialsCacheKey { private final String apiTokenIssuer; private final String apiAudience; private final String scopes; + private final int tokenExpiryBufferSeconds; + private final int tokenExpiryJitterSeconds; CredentialsCacheKey(ClientCredentials cc) { this.clientId = cc.getClientId(); @@ -422,6 +424,8 @@ private static final class CredentialsCacheKey { this.apiTokenIssuer = cc.getApiTokenIssuer(); this.apiAudience = cc.getApiAudience(); this.scopes = cc.getScopes(); + this.tokenExpiryBufferSeconds = cc.getTokenExpiryBufferSeconds(); + this.tokenExpiryJitterSeconds = cc.getTokenExpiryJitterSeconds(); } private static byte[] sha256(String value) { @@ -441,12 +445,15 @@ public boolean equals(Object o) { && Arrays.equals(clientSecretHash, that.clientSecretHash) && Objects.equals(apiTokenIssuer, that.apiTokenIssuer) && Objects.equals(apiAudience, that.apiAudience) - && Objects.equals(scopes, that.scopes); + && Objects.equals(scopes, that.scopes) + && tokenExpiryBufferSeconds == that.tokenExpiryBufferSeconds + && tokenExpiryJitterSeconds == that.tokenExpiryJitterSeconds; } @Override public int hashCode() { - int result = Objects.hash(clientId, apiTokenIssuer, apiAudience, scopes); + int result = Objects.hash( + clientId, apiTokenIssuer, apiAudience, scopes, tokenExpiryBufferSeconds, tokenExpiryJitterSeconds); result = 31 * result + Arrays.hashCode(clientSecretHash); return result; } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java index dfcac13a..17ef3f6e 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java @@ -2,6 +2,7 @@ import static dev.openfga.sdk.util.Validation.assertParamExists; +import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.FgaInvalidParameterException; import dev.openfga.sdk.util.StringUtil; @@ -11,6 +12,8 @@ public class ClientCredentials { private String apiTokenIssuer; private String apiAudience; private String scopes; + private int tokenExpiryBufferSeconds = FgaConstants.TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC; + private int tokenExpiryJitterSeconds = FgaConstants.TOKEN_EXPIRY_JITTER_IN_SEC; public ClientCredentials() {} @@ -64,4 +67,37 @@ public ClientCredentials scopes(String scopes) { public String getScopes() { return this.scopes; } + + /** + * Sets how many seconds before expiry a cached token requires refresh. Defaults to 300. + * @throws IllegalArgumentException if seconds is negative. + */ + public ClientCredentials tokenExpiryBufferSeconds(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("tokenExpiryBufferSeconds must be non-negative"); + } + this.tokenExpiryBufferSeconds = seconds; + return this; + } + + public int getTokenExpiryBufferSeconds() { + return this.tokenExpiryBufferSeconds; + } + + /** + * Sets the exclusive upper bound of additional random seconds subtracted on each expiry check. + * Defaults to 300. Set to zero to disable jitter. + * @throws IllegalArgumentException if seconds is negative. + */ + public ClientCredentials tokenExpiryJitterSeconds(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("tokenExpiryJitterSeconds must be non-negative"); + } + this.tokenExpiryJitterSeconds = seconds; + return this; + } + + public int getTokenExpiryJitterSeconds() { + return this.tokenExpiryJitterSeconds; + } } diff --git a/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java b/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java index 2629133b..07d0b762 100644 --- a/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java +++ b/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import dev.openfga.sdk.api.configuration.ClientCredentials; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.stream.Stream; @@ -40,6 +41,9 @@ private static Stream expTimeAndResults() { @ParameterizedTest(name = "{0}") void testTokenValid(String name, Instant exp, boolean valid) { AccessToken snapshot = new AccessToken("token", exp); - assertEquals(valid, snapshot.isValid()); + var defaults = new ClientCredentials(); + assertEquals( + valid, + snapshot.isValid(defaults.getTokenExpiryBufferSeconds(), defaults.getTokenExpiryJitterSeconds())); } } diff --git a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java index 98d248a6..db041446 100644 --- a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java @@ -20,6 +20,8 @@ import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -168,8 +170,9 @@ void clientCredentials_failureAsApiException() { requestBuilder.build().headers().firstValue("Authorization").isPresent()); } - @Test - void clientCredentials_setsAuthHeader() throws Exception { + @ParameterizedTest + @CsvSource({"3600,300,300,1", "300,300,300,2", "300,30,0,1", "300,30,10,1", "20,30,0,2", "300,0,0,1"}) + void clientCredentials_setsAuthHeader(int lifetime, int buffer, int jitter, int exchanges) throws Exception { String clientId = "some-client-id"; String clientSecret = "some-client-secret"; String apiAudience = "some-audience"; @@ -183,7 +186,9 @@ void clientCredentials_setsAuthHeader() throws Exception { containsString("client_secret=" + clientSecret), containsString("audience=" + apiAudience), containsString("grant_type=client_credentials"))) - .doReturn(200, String.format("{\"access_token\":\"%s\",\"expires_in\":3600}", exchangedToken)); + .doReturn( + 200, + String.format("{\"access_token\":\"%s\",\"expires_in\":%d}", exchangedToken, lifetime)); HttpClient.Builder mockBuilder = mockHttpClientBuilder(mockHttpClient); ApiClient apiClient = new ApiClient(mockBuilder); @@ -194,6 +199,8 @@ void clientCredentials_setsAuthHeader() throws Exception { .clientId(clientId) .clientSecret(clientSecret) .apiAudience(apiAudience) + .tokenExpiryBufferSeconds(buffer) + .tokenExpiryJitterSeconds(jitter) .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL))); HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(FgaConstants.TEST_API_URL)); @@ -203,7 +210,7 @@ void clientCredentials_setsAuthHeader() throws Exception { "Bearer " + exchangedToken, requestBuilder.build().headers().firstValue("Authorization").orElseThrow()); - // A second call should reuse the cached token and not hit the issuer again. + // Reuse the token only while it is outside the configured refresh window. HttpRequest.Builder secondBuilder = HttpRequest.newBuilder().uri(URI.create(FgaConstants.TEST_API_URL)); apiClient.applyAuthHeader(secondBuilder, configuration); assertEquals( @@ -213,7 +220,35 @@ void clientCredentials_setsAuthHeader() throws Exception { mockHttpClient .verify() .post(String.format("%s/oauth/token", FgaConstants.TEST_ISSUER_URL)) - .called(1); + .called(exchanges); + } + + @ParameterizedTest + @CsvSource({"31,10", "30,11"}) + void clientCredentials_differentRefreshSettings_useSeparateCaches(int buffer, int jitter) throws Exception { + HttpClientMock mockHttpClient = new HttpClientMock(); + mockHttpClient + .onPost(FgaConstants.TEST_ISSUER_URL + "/oauth/token") + .doReturn(200, "{\"access_token\":\"token\",\"expires_in\":300}"); + ApiClient apiClient = new ApiClient(mockHttpClientBuilder(mockHttpClient)); + ClientCredentials credentials = new ClientCredentials() + .clientId("client") + .clientSecret("secret") + .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL) + .tokenExpiryBufferSeconds(30) + .tokenExpiryJitterSeconds(10); + Configuration configuration = new Configuration().credentials(new Credentials(credentials)); + + apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); + apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); + credentials.tokenExpiryBufferSeconds(buffer).tokenExpiryJitterSeconds(jitter); + apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); + apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); + + mockHttpClient + .verify() + .post(FgaConstants.TEST_ISSUER_URL + "/oauth/token") + .called(2); } @Test diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java index 58ece583..e5bc9d50 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java @@ -14,6 +14,12 @@ public class ClientCredentialsTest { private static final String VALID_API_TOKEN_ISSUER = "tokenissuer.fga.example"; private static final String VALID_API_AUDIENCE = "audience"; + @Test + void negativeTokenRefreshSettingsAreRejected() { + assertThrows(IllegalArgumentException.class, () -> new ClientCredentials().tokenExpiryBufferSeconds(-1)); + assertThrows(IllegalArgumentException.class, () -> new ClientCredentials().tokenExpiryJitterSeconds(-1)); + } + @Test public void assertValid_allValid() throws FgaInvalidParameterException { // When From 5f0a0df2cf7756aa4972162313542b70b1911e7b Mon Sep 17 00:00:00 2001 From: Taha El Amine Kassabi Date: Thu, 10 Sep 2026 15:59:22 +0200 Subject: [PATCH 2/3] refactor: keep token refresh policy in client configuration Keep ClientCredentials limited to token request parameters. Preserve refresh policy through request overrides. Co-Authored-By: Codex GPT-5 --- README.md | 12 +++--- .../openfga/sdk/api/auth/OAuth2Client.java | 4 +- .../dev/openfga/sdk/api/client/ApiClient.java | 10 ++--- .../configuration/ClientConfiguration.java | 12 ++++++ .../api/configuration/ClientCredentials.java | 36 ----------------- .../sdk/api/configuration/Configuration.java | 39 +++++++++++++++++++ .../openfga/sdk/api/auth/AccessTokenTest.java | 4 +- .../openfga/sdk/api/client/ApiClientTest.java | 16 ++++---- .../configuration/ClientCredentialsTest.java | 6 --- .../api/configuration/ConfigurationTest.java | 6 +++ 10 files changed, 82 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 1cd11e96..21d55f73 100644 --- a/README.md +++ b/README.md @@ -259,16 +259,18 @@ Both settings default to 300 seconds; jitter is sampled from zero up to, but exc the configured value on each validity check. For short-lived tokens, configure a smaller window: ```java -var credentials = new ClientCredentials() - .clientId(System.getenv("FGA_CLIENT_ID")) - .clientSecret(System.getenv("FGA_CLIENT_SECRET")) - .apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")) +var config = new ClientConfiguration() + .credentials(new Credentials(new ClientCredentials() + .clientId(System.getenv("FGA_CLIENT_ID")) + .clientSecret(System.getenv("FGA_CLIENT_SECRET")) + .apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER")))) .tokenExpiryBufferSeconds(30) .tokenExpiryJitterSeconds(5); ``` Values must be non-negative. Set jitter to zero to disable it. Keep the combined window -below the token lifetime to allow cached tokens to be reused. +below the token lifetime to allow cached tokens to be reused. These client-level settings +are preserved when applying per-request configuration overrides. ### Custom Headers diff --git a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java index cdf17225..f5e30682 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -31,8 +31,8 @@ public class OAuth2Client { */ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException { var clientCredentials = configuration.getCredentials().getClientCredentials(); - this.tokenExpiryBufferSeconds = clientCredentials.getTokenExpiryBufferSeconds(); - this.tokenExpiryJitterSeconds = clientCredentials.getTokenExpiryJitterSeconds(); + this.tokenExpiryBufferSeconds = configuration.getTokenExpiryBufferSeconds(); + this.tokenExpiryJitterSeconds = configuration.getTokenExpiryJitterSeconds(); this.apiClient = apiClient; this.authRequest = diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java index 6e898f70..ff754d98 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java @@ -398,8 +398,7 @@ public void applyAuthHeader(HttpRequest.Builder requestBuilder, Configuration co } private OAuth2Client ensureOAuth2Client(Configuration configuration) throws FgaInvalidParameterException { - ClientCredentials cc = configuration.getCredentials().getClientCredentials(); - CredentialsCacheKey key = new CredentialsCacheKey(cc); + CredentialsCacheKey key = new CredentialsCacheKey(configuration); OAuth2Client existing = oAuth2Clients.get(key); if (existing != null) { return existing; @@ -418,14 +417,15 @@ private static final class CredentialsCacheKey { private final int tokenExpiryBufferSeconds; private final int tokenExpiryJitterSeconds; - CredentialsCacheKey(ClientCredentials cc) { + CredentialsCacheKey(Configuration configuration) { + ClientCredentials cc = configuration.getCredentials().getClientCredentials(); this.clientId = cc.getClientId(); this.clientSecretHash = sha256(cc.getClientSecret()); this.apiTokenIssuer = cc.getApiTokenIssuer(); this.apiAudience = cc.getApiAudience(); this.scopes = cc.getScopes(); - this.tokenExpiryBufferSeconds = cc.getTokenExpiryBufferSeconds(); - this.tokenExpiryJitterSeconds = cc.getTokenExpiryJitterSeconds(); + this.tokenExpiryBufferSeconds = configuration.getTokenExpiryBufferSeconds(); + this.tokenExpiryJitterSeconds = configuration.getTokenExpiryJitterSeconds(); } private static byte[] sha256(String value) { diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java index 98d5a6c6..c49da64c 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java @@ -129,6 +129,18 @@ public ClientConfiguration telemetryConfiguration(TelemetryConfiguration telemet return this; } + @Override + public ClientConfiguration tokenExpiryBufferSeconds(int seconds) { + super.tokenExpiryBufferSeconds(seconds); + return this; + } + + @Override + public ClientConfiguration tokenExpiryJitterSeconds(int seconds) { + super.tokenExpiryJitterSeconds(seconds); + return this; + } + @Override public ClientConfiguration defaultHeaders(java.util.Map defaultHeaders) { super.defaultHeaders(defaultHeaders); diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java index 17ef3f6e..dfcac13a 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java @@ -2,7 +2,6 @@ import static dev.openfga.sdk.util.Validation.assertParamExists; -import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.FgaInvalidParameterException; import dev.openfga.sdk.util.StringUtil; @@ -12,8 +11,6 @@ public class ClientCredentials { private String apiTokenIssuer; private String apiAudience; private String scopes; - private int tokenExpiryBufferSeconds = FgaConstants.TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC; - private int tokenExpiryJitterSeconds = FgaConstants.TOKEN_EXPIRY_JITTER_IN_SEC; public ClientCredentials() {} @@ -67,37 +64,4 @@ public ClientCredentials scopes(String scopes) { public String getScopes() { return this.scopes; } - - /** - * Sets how many seconds before expiry a cached token requires refresh. Defaults to 300. - * @throws IllegalArgumentException if seconds is negative. - */ - public ClientCredentials tokenExpiryBufferSeconds(int seconds) { - if (seconds < 0) { - throw new IllegalArgumentException("tokenExpiryBufferSeconds must be non-negative"); - } - this.tokenExpiryBufferSeconds = seconds; - return this; - } - - public int getTokenExpiryBufferSeconds() { - return this.tokenExpiryBufferSeconds; - } - - /** - * Sets the exclusive upper bound of additional random seconds subtracted on each expiry check. - * Defaults to 300. Set to zero to disable jitter. - * @throws IllegalArgumentException if seconds is negative. - */ - public ClientCredentials tokenExpiryJitterSeconds(int seconds) { - if (seconds < 0) { - throw new IllegalArgumentException("tokenExpiryJitterSeconds must be non-negative"); - } - this.tokenExpiryJitterSeconds = seconds; - return this; - } - - public int getTokenExpiryJitterSeconds() { - return this.tokenExpiryJitterSeconds; - } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java index 2a736c61..43eeb6bd 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java @@ -36,6 +36,8 @@ public class Configuration implements BaseConfiguration { private Duration connectTimeout; private int maxRetries; private Duration minimumRetryDelay; + private int tokenExpiryBufferSeconds = FgaConstants.TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC; + private int tokenExpiryJitterSeconds = FgaConstants.TOKEN_EXPIRY_JITTER_IN_SEC; private Map defaultHeaders; private TelemetryConfiguration telemetryConfiguration; @@ -87,6 +89,8 @@ public Configuration override(ConfigurationOverride configurationOverride) { Credentials overrideCredentials = configurationOverride.getCredentials(); result.credentials(overrideCredentials != null ? overrideCredentials : credentials); + result.tokenExpiryBufferSeconds(tokenExpiryBufferSeconds); + result.tokenExpiryJitterSeconds(tokenExpiryJitterSeconds); String overrideUserAgent = configurationOverride.getUserAgent(); result.userAgent(overrideUserAgent != null ? overrideUserAgent : userAgent); @@ -303,6 +307,41 @@ public Duration getMinimumRetryDelay() { return minimumRetryDelay; } + /** + * Sets how many seconds before expiry a cached client-credentials token requires refresh. + * Defaults to 300. This is a client-level setting, preserved by request overrides. + * @throws IllegalArgumentException if seconds is negative. + */ + public Configuration tokenExpiryBufferSeconds(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("tokenExpiryBufferSeconds must be non-negative"); + } + this.tokenExpiryBufferSeconds = seconds; + return this; + } + + public int getTokenExpiryBufferSeconds() { + return this.tokenExpiryBufferSeconds; + } + + /** + * Sets the exclusive upper bound of additional random seconds subtracted on each token expiry check. + * Defaults to 300. Set to zero to disable jitter. This is a client-level setting, + * preserved by request overrides. + * @throws IllegalArgumentException if seconds is negative. + */ + public Configuration tokenExpiryJitterSeconds(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("tokenExpiryJitterSeconds must be non-negative"); + } + this.tokenExpiryJitterSeconds = seconds; + return this; + } + + public int getTokenExpiryJitterSeconds() { + return this.tokenExpiryJitterSeconds; + } + public Configuration defaultHeaders(Map defaultHeaders) { this.defaultHeaders = defaultHeaders; return this; diff --git a/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java b/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java index 07d0b762..6e04371c 100644 --- a/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java +++ b/src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import dev.openfga.sdk.api.configuration.ClientCredentials; +import dev.openfga.sdk.api.configuration.Configuration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.stream.Stream; @@ -41,7 +41,7 @@ private static Stream expTimeAndResults() { @ParameterizedTest(name = "{0}") void testTokenValid(String name, Instant exp, boolean valid) { AccessToken snapshot = new AccessToken("token", exp); - var defaults = new ClientCredentials(); + var defaults = new Configuration(); assertEquals( valid, snapshot.isValid(defaults.getTokenExpiryBufferSeconds(), defaults.getTokenExpiryJitterSeconds())); diff --git a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java index db041446..2c99010e 100644 --- a/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java @@ -11,6 +11,7 @@ import dev.openfga.sdk.api.configuration.ApiToken; import dev.openfga.sdk.api.configuration.ClientCredentials; import dev.openfga.sdk.api.configuration.Configuration; +import dev.openfga.sdk.api.configuration.ConfigurationOverride; import dev.openfga.sdk.api.configuration.Credentials; import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.ApiException; @@ -199,9 +200,9 @@ void clientCredentials_setsAuthHeader(int lifetime, int buffer, int jitter, int .clientId(clientId) .clientSecret(clientSecret) .apiAudience(apiAudience) - .tokenExpiryBufferSeconds(buffer) - .tokenExpiryJitterSeconds(jitter) - .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL))); + .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL))) + .tokenExpiryBufferSeconds(buffer) + .tokenExpiryJitterSeconds(jitter); HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(FgaConstants.TEST_API_URL)); apiClient.applyAuthHeader(requestBuilder, configuration); @@ -212,7 +213,7 @@ void clientCredentials_setsAuthHeader(int lifetime, int buffer, int jitter, int // Reuse the token only while it is outside the configured refresh window. HttpRequest.Builder secondBuilder = HttpRequest.newBuilder().uri(URI.create(FgaConstants.TEST_API_URL)); - apiClient.applyAuthHeader(secondBuilder, configuration); + apiClient.applyAuthHeader(secondBuilder, configuration.override(new ConfigurationOverride())); assertEquals( "Bearer " + exchangedToken, secondBuilder.build().headers().firstValue("Authorization").orElseThrow()); @@ -234,14 +235,15 @@ void clientCredentials_differentRefreshSettings_useSeparateCaches(int buffer, in ClientCredentials credentials = new ClientCredentials() .clientId("client") .clientSecret("secret") - .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL) + .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL); + Configuration configuration = new Configuration() + .credentials(new Credentials(credentials)) .tokenExpiryBufferSeconds(30) .tokenExpiryJitterSeconds(10); - Configuration configuration = new Configuration().credentials(new Credentials(credentials)); apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); - credentials.tokenExpiryBufferSeconds(buffer).tokenExpiryJitterSeconds(jitter); + configuration.tokenExpiryBufferSeconds(buffer).tokenExpiryJitterSeconds(jitter); apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); apiClient.applyAuthHeader(HttpRequest.newBuilder(), configuration); diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java index e5bc9d50..58ece583 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java @@ -14,12 +14,6 @@ public class ClientCredentialsTest { private static final String VALID_API_TOKEN_ISSUER = "tokenissuer.fga.example"; private static final String VALID_API_AUDIENCE = "audience"; - @Test - void negativeTokenRefreshSettingsAreRejected() { - assertThrows(IllegalArgumentException.class, () -> new ClientCredentials().tokenExpiryBufferSeconds(-1)); - assertThrows(IllegalArgumentException.class, () -> new ClientCredentials().tokenExpiryJitterSeconds(-1)); - } - @Test public void assertValid_allValid() throws FgaInvalidParameterException { // When diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java index b2f6e1d3..c621f33a 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java @@ -16,6 +16,12 @@ class ConfigurationTest { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); private static final Map DEFAULT_HEADERS = Map.of(); + @Test + void negativeTokenRefreshSettingsAreRejected() { + assertThrows(IllegalArgumentException.class, () -> new Configuration().tokenExpiryBufferSeconds(-1)); + assertThrows(IllegalArgumentException.class, () -> new Configuration().tokenExpiryJitterSeconds(-1)); + } + @Test void apiUrl_nullDefaults() throws FgaInvalidParameterException { // Given From 148f93c52b2343065c51070b77fe2dac5f2ab607 Mon Sep 17 00:00:00 2001 From: Taha El Amine Kassabi Date: Thu, 10 Sep 2026 16:04:08 +0200 Subject: [PATCH 3/3] test: remove trivial refresh setting validation test Co-Authored-By: Codex GPT-5 --- .../openfga/sdk/api/configuration/ConfigurationTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java index c621f33a..b2f6e1d3 100644 --- a/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java @@ -16,12 +16,6 @@ class ConfigurationTest { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); private static final Map DEFAULT_HEADERS = Map.of(); - @Test - void negativeTokenRefreshSettingsAreRejected() { - assertThrows(IllegalArgumentException.class, () -> new Configuration().tokenExpiryBufferSeconds(-1)); - assertThrows(IllegalArgumentException.class, () -> new Configuration().tokenExpiryJitterSeconds(-1)); - } - @Test void apiUrl_nullDefaults() throws FgaInvalidParameterException { // Given