Skip to content

Fix double-caching of OAuth tokens in Azure client secret credentials#1573

Merged
renaudhartert-db merged 1 commit intomainfrom
fix-azure-client-secret-double-caching
Mar 24, 2026
Merged

Fix double-caching of OAuth tokens in Azure client secret credentials#1573
renaudhartert-db merged 1 commit intomainfrom
fix-azure-client-secret-double-caching

Conversation

@renaudhartert-db
Copy link
Copy Markdown
Contributor

@renaudhartert-db renaudhartert-db commented Mar 21, 2026

Summary

Fixes the double-caching of OAuth tokens in Azure client secret credentials (#1549) and unifies the internal token source infrastructure on auth.TokenSource.

Why

AzureClientSecretCredentials.tokenSourceFor called clientcredentials.Config{}.TokenSource(ctx), which returns an oauth2.ReuseTokenSource with a hardcoded 10-second expiryDelta. This token source was then wrapped in azureReuseTokenSource (a cachedTokenSource) and again in serviceToServiceVisitor (another cachedTokenSource). The inner ReuseTokenSource swallowed proactive refresh attempts from the outer layers -- it kept returning its cached token until only ~10 seconds remained before expiry, defeating the 20-minute async refresh window and causing bursts of 401s at token expiry.

This is the same class of bug as #1550 (M2M OAuth), but in the Azure client secret path.

What changed

Interface changes

None. All changes are to unexported types and functions.

Behavioral changes

  • AzureClientSecretCredentials no longer double-caches tokens. Each call to the underlying token source results in an HTTP request to the token endpoint; caching is purely controlled by the outer azureReuseTokenSource and serviceToServiceVisitor layers.

Internal changes

  • azureHostResolver interface now returns auth.TokenSource instead of oauth2.TokenSource, and no longer takes a context.Context parameter. Context is passed per-call via Token(ctx).
  • serviceToServiceVisitor and refreshableVisitor now accept auth.TokenSource directly, removing the authconv.AuthTokenSource wrapping at call sites.
  • azureReuseTokenSource and wrap operate on auth.TokenSource instead of oauth2.TokenSource, eliminating the authconv round-trip.
  • AzureClientSecretCredentials: removed the duplicate tokenSourceFor (which used clientcredentials.Config.TokenSource with inner caching). The former authTokenSourceFor -- which calls clientcredentials.Config.Token(ctx) without inner caching -- is now the sole tokenSourceFor.
  • AzureCliCredentials.tokenSourceFor: returns an auth.TokenSource that creates a fresh azureCliTokenSource per call, properly threading the caller's context.
  • AzureMsiCredentials.tokenSourceFor: wraps NewAzureMsiTokenSource with authconv.AuthTokenSource.
  • GCP and M2M credential providers: callers wrap their oauth2.TokenSource with authconv.AuthTokenSource before passing to the visitor functions.

How is this tested?

  • TestAzureClientSecretCredentials_tokenSourceFor_noCaching: calls Token() N times and asserts N HTTP requests hit the token endpoint. Fails when the fix is reverted (inner caching collapses N calls into 1).
  • TestAzureClientSecretCredentials_Configure: happy-path test for the full Azure client secret credential flow.
  • All existing config/ tests pass.

github-merge-queue bot pushed a commit that referenced this pull request Mar 22, 2026
## Changes

Fixes #1549. Related: #1550, #1573.

Google's Cloud Go libraries (`impersonate.IDTokenSource`,
`impersonate.CredentialsTokenSource`, `idtoken.NewTokenSource`,
`google.CredentialsFromJSON`) all internally wrap their token sources in
`oauth2.ReuseTokenSource`. This means the SDK's `cachedTokenSource`
async refresh is swallowed by the inner `ReuseTokenSource` cache, making
it entirely wasted work.


This is the same double-caching bug as M2M OAuth (#1549). Though, unlike
M2M (#1550) and Azure Client Secret (#1573), Google's libraries don't
expose an uncached `Token(ctx)` method -- the inner sources are
unexported types. Recreating the token source on each call is expensive
and will require further considerations.

The pragmatic fix is to disable async refresh for both GCP providers:
- `GoogleDefaultCredentials` (`config/auth_gcp_google_id.go`)
- `GoogleCredentials` (`config/auth_gcp_google_credentials.go`)

Google's own `ReuseTokenSource` (with a 10-second early refresh window)
still provides token renewal before expiry. Thorough comments explain
the rationale and warn against re-enabling async refresh.

## Tests

Existing `config` tests pass (`go test ./config/ -v -count=1`). No new
tests needed -- this is a configuration change (appending
`auth.WithAsyncRefresh(false)` to cache options) and the async refresh
mechanism itself is already well-tested.

---------

Signed-off-by: Ubuntu <renaud.hartert@databricks.com>
Co-authored-by: simon <simon.faltum@databricks.com>
The tokenSourceFor method previously used clientcredentials.Config.TokenSource
which wraps the result in oauth2.ReuseTokenSource with a 10s expiryDelta.
This inner cache swallowed proactive refresh attempts from azureReuseTokenSource
and serviceToServiceVisitor, causing bursts of 401s at token expiry.

Use auth.TokenSourceFn with clientcredentials.Config.Token instead, so that
caching is purely controlled by the outer layers.

Fixes #1549.

Co-authored-by: Isaac
Signed-off-by: Ubuntu <renaud.hartert@databricks.com>
@renaudhartert-db renaudhartert-db force-pushed the fix-azure-client-secret-double-caching branch from f8ca20e to 555ecd6 Compare March 23, 2026 16:55
@github-actions
Copy link
Copy Markdown

If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/sdk-go

Inputs:

  • PR number: 1573
  • Commit SHA: 555ecd6fe1a30db6ce7135c636cd43c372938593

Checks will be approved automatically on success.

@renaudhartert-db renaudhartert-db added this pull request to the merge queue Mar 24, 2026
Merged via the queue into main with commit fab1ebd Mar 24, 2026
13 checks passed
@renaudhartert-db renaudhartert-db deleted the fix-azure-client-secret-double-caching branch March 24, 2026 08:17
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.

2 participants