Skip to content

Serve still-valid access tokens while a token refresh is in flight - #6

Open
Ignacio-Vidal wants to merge 429 commits into
mainfrom
oidc-client-serve-valid-token-during-refresh
Open

Ignacio-Vidal wants to merge 429 commits into
mainfrom
oidc-client-serve-valid-token-during-refresh

Conversation

@Ignacio-Vidal

Copy link
Copy Markdown
Owner

While TokensHelper has a token refresh in flight, the cached tokens are replaced by the pending Uni, so any request arriving in that window subscribes to it and waits for the token endpoint to respond. The tokens being replaced are normally still valid at that moment — refresh-token-time-skew exists precisely to start the refresh early — so that wait buys nothing.

This retains those tokens in TokenRequestState and keeps serving them for the duration of the refresh, falling back to the in-flight Uni only once they have genuinely expired.

quarkus.oidc-client.refresh-interval already moves most refreshes off the request path, but it does not close this window: the window is one token round trip wide and recurs on every refresh, so concurrent traffic still lands in it.

Measured on a service taking unauthenticated traffic and making authenticated downstream calls, with refresh-interval configured and deliberately short-lived (5s) tokens — 12 concurrent clients over 15s, ~13k requests: roughly 0.6% of requests blocked between 5ms and 32ms waiting on an in-flight refresh, the spread reflecting where in the refresh each request arrived.

TokensHelperTest covers the new behaviour: a caller arriving during a blocked refresh receives the previous, still-valid tokens, and the refresh is not duplicated. It fails without the change.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

😭 Deploy PR Preview failed.

@Ignacio-Vidal
Ignacio-Vidal force-pushed the oidc-client-serve-valid-token-during-refresh branch from ccd6b73 to 863023f Compare September 3, 2026 20:42
@Ignacio-Vidal
Ignacio-Vidal force-pushed the oidc-client-serve-valid-token-during-refresh branch from d45354b to 9f47497 Compare September 5, 2026 08:37
@Ignacio-Vidal
Ignacio-Vidal force-pushed the oidc-client-serve-valid-token-during-refresh branch from 9f47497 to 10ddaee Compare September 21, 2026 18:57
amaechler and others added 25 commits October 15, 2026 14:13
Override ConsoleHandler's stale System.out reference and
remove foreign handlers from the JUL root logger during
Quarkus logging initialization.
…date-mandrel

Add section about native-image-configure tool
Co-authored-by: Yoann Rodière <yoann@hibernate.org>
…l.INSTANCE) Pool beans

Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
…in/org.mvnpm-type-fest-5.10.0

Build(deps): Bump org.mvnpm:type-fest from 5.8.0 to 5.10.0
Services: add support for capturing date/time/tz and related types
Honor custom test port for @TestHTTPResource with Amazon Lambda HTTP
Update the guides for the Java 21 minimum
…avadoc

Fix Javadoc of `LambdaTransliterator`
Fix AgroalDataSourceUtil.activeDataSourceNames() to include named datasources
…ct-panache1-53368

Fail the build when classic Panache is used with Quarkus Data
Deprecate quarkus.hibernate-orm.log-session-metrics property
…ging-48763

Fix Gradle `testLogging.showStandardStreams` being ignored by Quarkus tests
Fix capabilities section in writing-extensions guide
closes: quarkusio#57231

Signed-off-by: Michal Vavřík <dev@michalvavrik.net>
OIDC Client: cache token requests based on additional params
…in/org.htmlunit-htmlunit-5.6.0

Build(deps): Bump org.htmlunit:htmlunit from 5.5.0 to 5.6.0
…ility-grafana-export

Export the Dev UI observability dashboard to Grafana
TokensHelper replaces the cached tokens with the in-flight Uni for the
duration of a refresh, so every request arriving in that window subscribes
to that Uni and waits for the token endpoint to respond. The tokens being
replaced are normally still valid at that point, because
refresh-token-time-skew starts the refresh before they expire, so that
wait is avoidable.

Retain those tokens in TokenRequestState and serve them to callers arriving
while the refresh is in flight, as long as they have not expired -- the
same rule already applied when no refresh is running. The only difference
is that the caller waits on a refresh someone else started rather than one
it starts itself.

This is opt-in via the new
quarkus.oidc-client.min-remaining-access-token-lifespan property, which
sets how much of the access token's lifespan must be left for it to still
be worth returning, so that it does not expire in transit or while the
target service is processing the request. When not configured, callers
wait for the refresh, as before.

A refresh only starts once the remaining lifespan has dropped below
refresh-token-time-skew, so a minimum which is not less than the skew
could never be satisfied and would silently disable the optimisation.
This combination, a minimum below one second (the access token expiry is
tracked in whole seconds, so it would truncate to zero) and a missing skew
are rejected with a ConfigurationException when the OIDC client is
created.
@Ignacio-Vidal
Ignacio-Vidal force-pushed the oidc-client-serve-valid-token-during-refresh branch from 470fb4d to 3543ec5 Compare October 18, 2026 11:51
Tokens is public API, so replacing its 7-arg constructor broke existing
callers. Restore it, delegating with no minimum remaining access token
lifespan so tokens created with it keep the previous behaviour, and
deprecate it for removal in favour of the 8-arg constructor.
A caller forces the acquisition of new tokens because the ones it was given
were rejected, typically after a 401 when refresh on unauthorized is
enabled. If a refresh is already in flight, the tokens being refreshed kept
being served to the other callers, even though they are now known to have
been rejected.

When new tokens are forced during a refresh, drop the tokens being refreshed
from the state so that every caller waits for the refresh in flight.
The access token expiry is tracked in whole seconds, and the test created
tokens expiring in 2s with a 2s minimum remaining lifespan, so crossing a
second boundary before they were requested was enough for them not to be
served. Use a 6s skew, 5s remaining and a 1s minimum, which leaves 4s for
the test to run.
… is set

The debug message explaining that the tokens being refreshed would not be
reused because too little of their lifespan was left was also logged when
no minimum remaining access token lifespan was configured, where it does
not apply. Log it only when a minimum is configured, and include it in the
message.

This branch was successfully deployed

1 active (outdated) deployment
ci — 470fb4d1 Deployed Oct 7, 2026 by Ignacio-Vidal via Wait for approval if Dependabot #181
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.