Skip to content

feat: configure client-credentials token refresh timing - #389

Open
dpkass wants to merge 4 commits into
openfga:mainfrom
dpkass:feat/configurable-token-refresh
Open

dpkass wants to merge 4 commits into
openfga:mainfrom
dpkass:feat/configurable-token-refresh

Conversation

@dpkass

@dpkass dpkass commented Sep 10, 2026 •

Copy link
Copy Markdown

Description

What problem is being solved?

Five-minute access tokens are immediately considered stale by the fixed 300-second refresh buffer plus jitter, causing repeated token exchanges.

How is it being solved?

Make the refresh buffer and jitter configurable at the client level while preserving existing defaults.

What changes are made to solve it?

  • Add nonnegative tokenExpiryBufferSeconds and tokenExpiryJitterSeconds settings to Configuration and fluent ClientConfiguration. Zero jitter disables jitter.
  • Preserve these settings through configuration overrides and distinguish refresh policies in the OAuth client cache.
  • Document configuration and extend authentication tests for token reuse, refresh thresholds, overrides, and cache separation.

Validation: ./gradlew build test-integration passed on Java 21. Formatting and focused tests also passed after removing a redundant setter-validation test.

References

Closes #388

Review Checklist

  • I have allowed edits by maintainers.
  • I have added documentation for new/changed functionality in this PR.
  • The correct base branch is being used (main).
  • I have added tests to validate that the change in functionality is working as expected.

Summary by CodeRabbit

  • New Features
    • Added configurable token refresh timing for OAuth2 client-credentials authentication.
    • Tokens can now use an expiry buffer and optional random jitter before refresh.
    • Added validation to prevent negative timing values and support disabling jitter.
    • Refresh settings are preserved when applying per-request configuration overrides.
  • Documentation
    • Documented token refresh timing, configuration options, defaults, and usage guidance.

dpkass and others added 3 commits September 10, 2026 15:42
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 <noreply@openai.com>
Keep ClientCredentials limited to token request parameters. Preserve refresh policy through request overrides.

Co-Authored-By: Codex GPT-5 <noreply@openai.com>
Co-Authored-By: Codex GPT-5 <noreply@openai.com>
@dpkass
dpkass requested review from a team as code owners September 10, 2026 14:05
@linux-foundation-easycla

linux-foundation-easycla Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

CLA Not Signed

@coderabbitai

coderabbitai Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4587c818-62d2-40f0-a46f-9de4caf6f58d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3339112d-74a5-4de6-bfb9-245e1e92a154

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5c5c7 and 148f93c.

📒 Files selected for processing (8)
  • README.md
  • src/main/java/dev/openfga/sdk/api/auth/AccessToken.java
  • src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
  • src/main/java/dev/openfga/sdk/api/client/ApiClient.java
  • src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java
  • src/main/java/dev/openfga/sdk/api/configuration/Configuration.java
  • src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java
  • src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The change adds configurable token-expiry buffer and jitter settings. OAuth2 token validation uses these settings, configuration overrides preserve them, and OAuth2 client cache keys include them. Tests and documentation cover the new behavior.

Changes

OAuth2 refresh timing

Layer / File(s) Summary
Refresh timing configuration
src/main/java/dev/openfga/sdk/api/configuration/Configuration.java, src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java, README.md
Adds validated buffer and jitter settings, preserves them across overrides, exposes typed fluent methods, and documents the configuration.
Configurable token validity
src/main/java/dev/openfga/sdk/api/auth/AccessToken.java, src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java, src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java
Token validity now uses configured buffer and jitter values. OAuth2Client applies the settings in both cache-check paths.
Refresh-aware client caching
src/main/java/dev/openfga/sdk/api/client/ApiClient.java, src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java
OAuth2 client cache keys include refresh settings. Tests cover refresh-window behavior, request overrides, and separate caches.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant ApiClient
  participant OAuth2Client
  participant AccessToken
  ApiClient->>OAuth2Client: Request access token
  OAuth2Client->>AccessToken: Validate with buffer and jitter
  AccessToken-->>OAuth2Client: Return validity result
  OAuth2Client-->>ApiClient: Reuse or refresh token
Loading

Suggested reviewers: jimmyjames

Merge Risk: ⚪ Minimal · up to 148f9

The SDK now supports configurable client-credentials token refresh timing while preserving defaults. Configuration overrides, cache separation, and refresh behavior are covered, with no concrete merge-blocking risk evident.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 7 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: configurable client-credentials token refresh timing.
Linked Issues check ✅ Passed The changes satisfy issue #388 by making token-expiry buffer and jitter configurable, preserving defaults, validating nonnegative values, supporting zero jitter, preserving settings through overrides,…
Out of Scope Changes check ✅ Passed All code, documentation, and test changes directly support configurable client-credentials token refresh timing and the requirements in issue #388.
Full details: Docstring Coverage

Explanation

Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 7 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dpkass

dpkass commented Sep 10, 2026

Copy link
Copy Markdown
Author

should i remove codex for cla to be accepted?

@curfew-marathon

Copy link
Copy Markdown
Contributor

@dpkass yes, you'll need to remove codex

@codecov-commenter

codecov-commenter commented Sep 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.69697% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.42%. Comparing base (209bd3d) to head (adbecd7).

Files with missing lines Patch % Lines
...fga/sdk/api/configuration/ClientConfiguration.java 0.00% 4 Missing ⚠️
...v/openfga/sdk/api/configuration/Configuration.java 71.42% 2 Missing and 2 partials ⚠️
...in/java/dev/openfga/sdk/api/auth/OAuth2Client.java 75.00% 0 Missing and 1 partial ⚠️
...ain/java/dev/openfga/sdk/api/client/ApiClient.java 87.50% 0 Missing and 1 partial ⚠️

❌ Your project status has failed because the head coverage (39.42%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #389      +/-   ##
============================================
+ Coverage     39.34%   39.42%   +0.08%     
- Complexity     1336     1341       +5     
============================================
  Files           202      202              
  Lines          7791     7815      +24     
  Branches        912      913       +1     
============================================
+ Hits           3065     3081      +16     
- Misses         4579     4585       +6     
- Partials        147      149       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SoulPancake

Copy link
Copy Markdown
Member

Hi @dpkass
You can rebase and amend author to just you and force push it

@hemanthreddy7 hemanthreddy7 left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @dpkass, and for raising #388 alongside it. A hard-coded 300s buffer plus up-to-300s jitter means short-lived tokens get treated as stale almost immediately, so making the timing configurable is a sensible and backward-compatible fix. The direction is good. I've left inline comments on the Files changed tab; collecting the main points here so they're in one place.

1. Design

  1. buffer and jitter config (client level vs request level vs under Credentials object) - here
  2. removing buffer and jitter from the token cache key - here

2. Cross-SDK consistency
These SDKs are meant to behave consistently, so in parallel I'll check whether the other OpenFGA SDKs have the same hard-coded buffer/jitter and whether they expose configurable timing. If we settle on a shape here, it's worth tracking matching changes in the others (ideally coordinated through sdk-generator where it applies) so the behavior stays aligned across languages. This PR effectively sets the pattern the others would follow, which is another reason to get the design right here first.

3. Suggested sequencing
I'd suggest picking the comments related to buffer/jitter config and the cache-key design first so the other comments naturally follow.

4. Tests and coverage
Added inline comments where needed

Please let me know if there are any questions or if you need any clarifications.

Thank you again for the contribution.

Comment on lines +92 to +93
result.tokenExpiryBufferSeconds(tokenExpiryBufferSeconds);
result.tokenExpiryJitterSeconds(tokenExpiryJitterSeconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines copy the client-level buffer/jitter as is, unlike every other field in override(). And buffer/jitter are the only two Configuration fields not mirrored in ConfigurationOverride.

Here are my thoughts.

I think the current approach is fine. I don't think these should be per-request overridable. The access token is shared per identity, so "use buffer=X for this one call" has no well-defined meaning on a shared token: whichever request happens to trigger the refresh wins, and it affects every other caller using that token. That's unlike credentials (overriding them yields a genuinely different token) or timeouts/headers (which apply only to your own request).

There is a legitimate case where these values could differ: two credentials whose tokens have very different lifetimes (say 1h vs 5m), where a single buffer is wrong for one of them. But that is per-credential, not per-request, so the natural home would be on the Credentials object (the policy travels with the identity). I'd treat that as a follow-up, not part of this PR.

So my suggestion: keep these as client-level only (don't add them to ConfigurationOverride), and add a short comment here noting that's intentional.

@curfew-marathon, @SoulPancake - Does this make sense or is there something I'm missing? What do you suggest?

return this;
}

public int getTokenExpiryBufferSeconds() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Housekeeping nit: Could you add a short Javadoc with @return? Thank you.

return this;
}

public int getTokenExpiryJitterSeconds() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Housekeeping nit: Could you add a short Javadoc with @return? Thank you.

Comment thread src/main/java/dev/openfga/sdk/api/auth/AccessToken.java
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor consistency: Assign the defaults in the constructor - rather than inline initializers.

// 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()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason an override is used here? An empty one merges to the same config, so it reuses the same cache entry either way.


@ParameterizedTest
@CsvSource({"31,10", "30,11"})
void clientCredentials_differentRefreshSettings_useSeparateCaches(int buffer, int jitter) throws Exception {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be picked once https://github.com/openfga/java-sdk/pull/389/changes#r4156168685 is resolved.

This test exercises the behavior I flagged over on ApiClient.java (buffer/jitter in the cache key) here: same credentials with different timing producing separate token exchanges. It's tied to that discussion, so once we settle it there and take timing out of the key, this test should be deleted.

Once we remove the above, it'd be worth replacing it with the inverse, a guard that same credentials share one cached client regardless of refresh timing, which is exactly the contract we'd be committing to. Sharing a skeleton below:

@Test
void clientCredentials_differentRefreshSettings_shareOneCache() throws Exception {
    HttpClientMock mockHttpClient = new HttpClientMock();
    mockHttpClient
            .onPost(FgaConstants.TEST_ISSUER_URL + "/oauth/token")
            .doReturn(200, "{\"access_token\":\"token\",\"expires_in\":3600}");
    ApiClient apiClient = new ApiClient(mockHttpClientBuilder(mockHttpClient));

    ClientCredentials credentials = new ClientCredentials()
            .clientId("client")
            .clientSecret("secret")
            .apiTokenIssuer(FgaConstants.TEST_ISSUER_URL);

    Configuration first = new Configuration()
            .credentials(new Credentials(credentials))
            .tokenExpiryBufferSeconds(30)
            .tokenExpiryJitterSeconds(10);
    Configuration second = new Configuration()
            .credentials(new Credentials(credentials))
            .tokenExpiryBufferSeconds(300)
            .tokenExpiryJitterSeconds(0);

    apiClient.applyAuthHeader(HttpRequest.newBuilder(), first);
    apiClient.applyAuthHeader(HttpRequest.newBuilder(), second);

    // Same credentials => one cached client/token, regardless of refresh timing.
    mockHttpClient.verify().post(FgaConstants.TEST_ISSUER_URL + "/oauth/token").called(1);
}

* Defaults to 300. This is a client-level setting, preserved by request overrides.
* @throws IllegalArgumentException if seconds is negative.
*/
public Configuration tokenExpiryBufferSeconds(int seconds) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new buffer/jitter setters and getters don't have unit coverage in ConfigurationTest. Could you please add the relevant tests in ConfigurationTest file?

For each of buffer and jitter: a valid set/read-back (with the assertSame chaining check), the zero case (0 is meaningful here, it disables the buffer / disables jitter), the negative-value IllegalArgumentException (like minimumRetryDelay_negativeDuration_throwsException), and a _hasDefaultValue test asserting 300. And others that I might have missed.

You could refer to the minimumRetryDelay_* cluster and other test cases in the file for reference.

Credentials overrideCredentials = configurationOverride.getCredentials();
result.credentials(overrideCredentials != null ? overrideCredentials : credentials);
result.tokenExpiryBufferSeconds(tokenExpiryBufferSeconds);
result.tokenExpiryJitterSeconds(tokenExpiryJitterSeconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be picked up after https://github.com/openfga/java-sdk/pull/389/changes#r4154934650 is resolved.

Without these two lines, override() would reset buffer/jitter to the 300 defaults on every request. Nothing in the test file covers that currently. Worth a small direct unit test in ConfigurationTest, in the override_* style, something like:

@Test
void override_preservesTokenExpirySettings() {
    // Given
    Configuration original = new Configuration().tokenExpiryBufferSeconds(42).tokenExpiryJitterSeconds(7);

    // When
    Configuration result = original.override(new ConfigurationOverride());

    // Then
    assertEquals(42, result.getTokenExpiryBufferSeconds());
    assertEquals(7, result.getTokenExpiryJitterSeconds());
}

That matches the existing override_apiUrl / override_userAgent tests. Feel free to also assert the original is unmodified if you want to mirror them fully.

Comment thread README.md
}
```

#### Token refresh timing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoulPancake - checking placement: README.md is autogenerated (it's in .openapi-generator/FILES, and CONTRIBUTING notes changes to those files go through sdk-generator), so as-is this edit will be overwritten on the next generation, right? Could you confirm whether these changes should go to sdk-generator instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it should be nice to have the changes in the generator to avoid drift later but it's okay for the maintainers to do that chore instead of the contributors.
So, non-blocking for the PR context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make client-credentials token refresh buffer and jitter configurable

5 participants