Skip to content

Support automatic OAuth client-credentials token refresh - #838

Merged
PierreBtz merged 7 commits into
cloudbees-oss:masterfrom
chrisrentsman:cr/tokens
Aug 27, 2026
Merged

Support automatic OAuth client-credentials token refresh#838
PierreBtz merged 7 commits into
cloudbees-oss:masterfrom
chrisrentsman:cr/tokens

Conversation

@chrisrentsman

Copy link
Copy Markdown
Contributor

Summary

Adds support for Zendesk's OAuth client_credentials grant so a Zendesk client can mint and refresh short-lived access tokens instead of requiring a pre-minted OAuth token.

Addresses #829.

Changes

  • Commit 1: Base implementation

    • Add HttpTokenMinter class for minting OAuth tokens, taking care to prevent credential leaks.
    • Add SharedFutureTokenProvider class for handling thread-safe token caching and refresh.
      • Refresh begins when the current token's remaining lifetime reaches a configurable threshold (50% by default).
      • One elected leader thread synchronously performs and publishes the refresh result.
      • While the cached token remains unexpired/usable, concurrent callers continue to use it rather than block.
      • If no usable cached token exists, concurrent callers await the result of the leader's in-flight mint attempt.
      • A failed proactive refresh is non-fatal so long as a usable cached token exists.
    • Inject the current bearer token when building requests.
    • Add Zendesk.Builder configuration.
    • Add Zendesk#warmUp() for clients who would like to mint the OAuth token ahead of the first request.
    • Surface token-minting failures through ZendeskOAuthException when no usable token remains.
  • Commit 2: Add backoff on refresh failures

    • Add backoff logic in SharedFutureTokenProvider to suppress proactive mint attempts for 10 seconds after a failed mint attempt while a usable cached token remains.

Validation

  • mvn verify using JDK 11.
  • Stress-tested thread safety using a local custom harness with soak/herd tests and jcstress (not included in the PR).

References:

@Helmsdown

Helmsdown commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

@PierreBtz this is in reference to #829. (heh, Captain obvious here. I missed the reference that is clearly at the top of the PR description :-D )

@PierreBtz PierreBtz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the contribution! I still have to run some tests on this PR but this looks very promising.

*
* @see <a href="https://developer.zendesk.com/api-reference/ticketing/oauth/grant_type_tokens/">
* OAuth grant type tokens</a>
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated all of these in 5ebbaaa. Thanks!

* does nothing when there is nothing to prepare.
*
* @throws ZendeskOAuthException if minting fails
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

* Default requested token lifetime. 30 minutes: short enough to limit the exposure of a leaked
* token, long enough to keep minting infrequent.
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

/**
* Default refresh threshold. Refreshes when the current token has half its lifetime left.
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

* @param clientSecret the OAuth client's secret
* @param scope space-separated scopes to request, for example {@code "tickets:read"}
* @return this builder instance
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

* Supplies a currently-usable bearer token. Called on every request from many threads at once, so
* implementations must be thread-safe and may block while a token is minted.
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

* not handed out.
* </ol>
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

/**
* An immutable OAuth access token with the instants it was issued and expires.
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0

/**
* Acquires a brand-new access token. Does no caching, no coordination and no retry.
*
* @since FIXME

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @since FIXME
* @since 1.6.0


public Zendesk build() {
if (oauthClientCredentialsConfigured) {
Objects.requireNonNull(oauthClientId, "OAuth client id cannot be null");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note that a blank ("") oauthClientId would pass this check, yet would not be valid and fail at runtime on the first mint.

@chrisrentsman chrisrentsman Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Added blank checks for all of the new OAuth fields in 2be8014.

private Future<Result> startMintingLeader(
ExecutorService pool, TokenProvider provider, FakeMinter minter) {
var leader = pool.submit(() -> call(provider));
await().atMost(5, TimeUnit.SECONDS).until(() -> minter.threadsInsideMint.get() == 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is a problem with this line if you run this test class and RealSmokeTest together:

ERROR] org.zendesk.client.v2.SharedFutureTokenProviderTest.failedMintExpectSameExceptionForAllAwaiters -- Time elapsed: 0.001 s <<< ERROR!
java.lang.IllegalArgumentException: Timeout (5 seconds) must be greater than the poll delay (10 seconds).
	at org.awaitility.core.ConditionFactory.generateConditionSettings(ConditionFactory.java:1141)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
	at org.zendesk.client.v2.SharedFutureTokenProviderTest.startMintingLeader(SharedFutureTokenProviderTest.java:507)
	at org.zendesk.client.v2.SharedFutureTokenProviderTest.failedMintExpectSameExceptionForAllAwaiters(SharedFutureTokenProviderTest.java:231)

This is because RealSmokeTest globally sets a custom poll value that conflicts with the timeout you chose here:

Awaitility.setDefaultPollDelay(10, TimeUnit.SECONDS);
. Opened #839 to fix this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah I see, thanks for fixing this up.

PierreBtz
PierreBtz previously approved these changes Aug 26, 2026

@PierreBtz PierreBtz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I finished preparing the repo to fix the test issue (#839) and to inject the proper credentials in the CI pipeline (#841).

Once the since FIXME and the client-id validation are fixed, we can merge this PR.

@chrisrentsman

chrisrentsman commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

I finished preparing the repo to fix the test issue (#839) and to inject the proper credentials in the CI pipeline (#841).

Once the since FIXME and the client-id validation are fixed, we can merge this PR.

Thanks @PierreBtz for the feedback and the prep. I've addressed your feedback and re-ran mvn verify too.

@PierreBtz PierreBtz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm, thanks!

@PierreBtz
PierreBtz merged commit f6616c6 into cloudbees-oss:master Aug 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants