Skip to content

[improve][client] Enable configurable preemptive OAuth2 token refresh#25363

Open
lhotari wants to merge 18 commits intoapache:masterfrom
lhotari:preemptive-token-refresh
Open

[improve][client] Enable configurable preemptive OAuth2 token refresh#25363
lhotari wants to merge 18 commits intoapache:masterfrom
lhotari:preemptive-token-refresh

Conversation

@lhotari
Copy link
Member

@lhotari lhotari commented Mar 19, 2026

Continuation of #13951 (originally authored by @michaeljmarshall). Rebased onto master, merge conflicts resolved, and all reviewer feedback addressed.

Motivation

In some client use cases it is helpful to start refreshing the OAuth2 token in the background before it expires. This reduces latency spikes caused by blocking token-fetch calls and improves resilience when the Identity Provider is temporarily unavailable.

A key use case is reducing coupling to OAuth server availability. When the feature is enabled, the client continuously attempts to refresh the token in the background using exponential backoff, while continuing to serve requests with the existing valid token. This means a transient OAuth server outage does not immediately affect the Pulsar client — the client tolerates the outage for as long as the current token remains valid, which can be up to (1 - earlyTokenRefreshPercent) * expires_in seconds after the first refresh failure.

Modifications

Core feature (AuthenticationOAuth2)

  • Add optional preemptive background token refresh, scheduled at earlyTokenRefreshPercent * expires_in milliseconds after a successful fetch.
  • Default value is 1 (100 %), which disables the feature and preserves existing behaviour.
  • On refresh failure the scheduler retries with exponential backoff; a final synchronous attempt is made on the next getAuthData() call if the token has since expired.
  • Replace the per-instance ScheduledThreadPoolExecutor (raised as a reviewer concern) with a shared static INTERNAL_SCHEDULER that uses io.netty.util.concurrent.DefaultThreadFactory (daemon threads, named oauth2-token-refresher) and allows the core thread to time out after 10 s of idle time — so no OS thread is held when no instances are actively refreshing.
  • Callers may supply their own ScheduledExecutorService; this class never shuts it down.
  • close() cancels the pending refresh task via the scheduler thread to avoid a race where a running refresh re-schedules itself; neither the shared nor the caller-supplied scheduler is shut down.
  • Add earlyTokenRefreshPercent JSON configuration parameter, enabling the feature for broker-side / reflection-created instances. Accepts a decimal ("0.8" → 0.8) or an integer percentage ("80" → 0.8, "100" → 1.0 = disabled).

AuthenticationFactoryOAuth2 (updated)

  • Remove class-level @Deprecated; keep @Deprecated only on the individual clientCredentials(...) static methods.
  • Extend ClientCredentialsBuilder with two new methods that cover the early refresh use case, making it the single builder for all client-credentials configurations:
    • earlyTokenRefreshPercent(double) — fractional value in (0, 1) to enable preemptive refresh, ≥ 1 to disable (default).
    • scheduler(ScheduledExecutorService) — optional external scheduler; if omitted the shared internal daemon scheduler is used automatically when early refresh is enabled.

AuthenticationOAuth2StandardAuthzServer (updated)

  • Overrides parseAuthParameters() to inject the RFC 8414 well-known metadata path, keeping the configure() refactor transparent to subclasses.

Verifying this change

New and updated unit tests in AuthenticationOAuth2Test:

  • Token caching and expiry-driven refresh (no early refresh).
  • Early refresh schedules background calls before expiry.
  • External scheduler is used when provided and is not shut down on close().
  • configure() via default constructor: decimal and integer earlyTokenRefreshPercent values, disabled (100), zero and non-numeric values throw IllegalArgumentException.
  • parseEarlyRefreshPercent unit tests for all branches.
  • Background scheduler actually triggers authenticate() when enabled via configure().

Does this pull request potentially affect one of the following parts?

  • Default behaviour is unchangedearlyTokenRefreshPercent defaults to 1 (disabled).
  • Adds new configuration surface for OAuth2 users wanting proactive token management.
  • Thread-resource concern (one thread per client) addressed by shared daemon executor that releases the thread when idle.

Documentation

  • docearlyTokenRefreshPercent parameter and early-refresh behaviour documented in javadoc on AuthenticationOAuth2 and ClientCredentialsBuilder.

Closes #13951

michaeljmarshall and others added 13 commits January 25, 2022 16:16
…th2 token refresh scheduler

- Resolve merge conflicts in AuthenticationFactoryOAuth2, ClientCredentialsFlow,
  AuthenticationOAuth2Test, TokenOauth2AuthenticatedProducerConsumerTest
- Rename ClientCredentialsFlow field from keyFileUrl to privateKey to match
  origin/master's constructor parameter name
- Update AuthenticationOAuth2Builder to use ClientCredentialsFlow.builder()
  instead of removed ClientCredentialsFlow(ClientCredentialsConfiguration) constructor
- Fix AuthenticationFactoryOAuth2.ClientCredentialsBuilder.build() to use
  correct AuthenticationOAuth2 constructor
- Accept deletion of site2/docs/security-oauth2.md (moved in origin/master)
- Use daemon threads for internally created ScheduledThreadPoolExecutor to
  prevent resource leaks when close() is not called (addresses reviewer concern)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n refresh

Instead of creating a new ScheduledThreadPoolExecutor per AuthenticationOAuth2
instance (which creates a thread per client), use a single shared static
executor (INTERNAL_SCHEDULER) that:
- Uses a daemon thread so it does not block JVM shutdown
- Allows the core thread to time out after 10s of idle time, releasing the
  OS thread when no OAuth2 instances are actively refreshing tokens
- Is shared across all AuthenticationOAuth2 instances, addressing the
  concern about too many threads in systems with multiple clients

This directly addresses the reviewer concern from eolivelli ("Isn't creating
a new thread per each client instance too heavyweight?") and lhotari
("The lifecycle of the scheduler/executor will need to be managed").

When a caller supplies their own scheduler via AuthenticationOAuth2Builder,
that scheduler is used as-is and is never shut down by this class.

Also change scheduler type from ScheduledThreadPoolExecutor to the more
general ScheduledExecutorService in the builder API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… remove unused field

- Use io.netty.util.concurrent.DefaultThreadFactory to create named daemon
  threads for the shared internal scheduler
- Remove unused ownsScheduler field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows the early token refresh feature to be configured when the class
is created via reflection (e.g., broker authentication plugins).

The parameter "earlyRefreshPercent" in the JSON auth config accepts:
- A decimal value (contains '.') used directly, e.g. "0.8" → 0.8
- An integer value divided by 100, e.g. "80" → 0.8, "100" → 1.0 (disabled)

When the configured value is < 1, the shared internal scheduler is
automatically activated if no external scheduler was provided.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rlyTokenRefreshPercent

New tests cover the reflection-based usage pattern (default constructor + configure):
- Default constructor leaves early refresh disabled (percent = 1)
- Decimal value "0.8" is used directly
- Integer value "80" is divided by 100 → 0.8
- Integer "100" → 1.0 (disabled)
- Zero and non-numeric values throw IllegalArgumentException
- When early refresh is enabled via configure(), the background scheduler
  is activated and triggers authenticate() before token expiry

Also extract minimalCredentialsJson/credentialsJsonWithEarlyRefresh helpers
to reduce duplication, and use CONFIG_PARAM_EARLY_TOKEN_REFRESH_PERCENT
constant instead of the hardcoded string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added the doc Your PR contains doc changes, no matter whether the changes are in markdown or code files. label Mar 19, 2026
@lhotari lhotari changed the title [OAuth2] Enable configurable preemptive token refresh in Java Client [improve][client] Enable configurable preemptive OAuth2 token refresh in Java Client Mar 19, 2026
@lhotari lhotari changed the title [improve][client] Enable configurable preemptive OAuth2 token refresh in Java Client [improve][client] Enable configurable preemptive OAuth2 token refresh Mar 19, 2026
@lhotari lhotari added this to the 4.2.0 milestone Mar 19, 2026
@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 62.03704% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.74%. Comparing base (74f4e5a) to head (7174a1f).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../impl/auth/oauth2/AuthenticationOAuth2Builder.java 0.00% 23 Missing ⚠️
.../client/impl/auth/oauth2/AuthenticationOAuth2.java 78.04% 11 Missing and 7 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##             master   #25363   +/-   ##
=========================================
  Coverage     72.73%   72.74%           
- Complexity    34264    34279   +15     
=========================================
  Files          1954     1955    +1     
  Lines        154792   154872   +80     
  Branches      17731    17739    +8     
=========================================
+ Hits         112586   112654   +68     
- Misses        33170    33177    +7     
- Partials       9036     9041    +5     
Flag Coverage Δ
inttests 25.79% <0.00%> (-0.12%) ⬇️
systests 22.52% <0.00%> (+0.04%) ⬆️
unittests 73.71% <62.03%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../impl/auth/oauth2/AuthenticationFactoryOAuth2.java 94.11% <100.00%> (ø)
...auth2/AuthenticationOAuth2StandardAuthzServer.java 71.42% <100.00%> (+18.48%) ⬆️
...client/impl/auth/oauth2/ClientCredentialsFlow.java 80.00% <ø> (ø)
.../client/impl/auth/oauth2/AuthenticationOAuth2.java 80.37% <78.04%> (-7.13%) ⬇️
.../impl/auth/oauth2/AuthenticationOAuth2Builder.java 0.00% <0.00%> (ø)

... and 76 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lhotari lhotari marked this pull request as draft March 19, 2026 21:47
…; add early refresh to ClientCredentialsBuilder

ClientCredentialsFlow already has @builder, so a separate configuration
class and dedicated builder class were unnecessary duplication.

- Delete ClientCredentialsConfiguration.java
- Delete AuthenticationOAuth2Builder.java
- Remove class-level @deprecated from AuthenticationFactoryOAuth2
  (keep @deprecated only on the individual deprecated static methods)
- Add earlyTokenRefreshPercent(double) and scheduler(ScheduledExecutorService)
  to AuthenticationFactoryOAuth2.ClientCredentialsBuilder, making it the
  single builder for all client-credentials use cases
- Remove stale javadoc reference to ClientCredentialsConfiguration in
  ClientCredentialsFlow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lhotari lhotari marked this pull request as ready for review March 19, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/authn area/client doc Your PR contains doc changes, no matter whether the changes are in markdown or code files. ready-to-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants