From d821b8b8a8e839f72db04df16fdfa3083ded476e Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Thu, 2 Jul 2026 11:00:08 -0700 Subject: [PATCH 01/12] Promote AccessTokenCache to public API in com.azure.core.credential Move AccessTokenCache from com.azure.core.implementation to the public com.azure.core.credential package, alongside SimpleTokenCache. The class is now part of the public API surface of azure-core. Changes: - Move AccessTokenCache.java to com.azure.core.credential package - Add comprehensive public JavaDoc - Update import in BearerTokenAuthenticationPolicy - Move AccessTokenCacheTests to com.azure.core.credential test package - Update import in TokenCacheTests --- .../AccessTokenCache.java | 46 +++++++++++++++++-- .../BearerTokenAuthenticationPolicy.java | 2 +- .../AccessTokenCacheTests.java | 2 +- .../core/credential/TokenCacheTests.java | 2 +- 4 files changed, 44 insertions(+), 8 deletions(-) rename sdk/core/azure-core/src/main/java/com/azure/core/{implementation => credential}/AccessTokenCache.java (88%) rename sdk/core/azure-core/src/test/java/com/azure/core/{implementation => credential}/AccessTokenCacheTests.java (99%) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java similarity index 88% rename from sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessTokenCache.java rename to sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index ba01c09e6b08..b68e9e90d8eb 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -1,11 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.core.implementation; +package com.azure.core.credential; -import com.azure.core.credential.AccessToken; -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; +import com.azure.core.implementation.AccessTokenCacheInfo; import com.azure.core.util.logging.ClientLogger; import com.azure.core.util.logging.LogLevel; import com.azure.core.util.logging.LoggingEventBuilder; @@ -25,7 +23,45 @@ import java.util.function.Supplier; /** - * A token cache that supports caching a token and refreshing it. + *

+ * {@code AccessTokenCache} is a thread-safe token cache that wraps a {@link TokenCredential} and manages proactive + * token refresh. It supports both asynchronous and synchronous token retrieval via + * {@link #getToken(TokenRequestContext, boolean)} and {@link #getTokenSync(TokenRequestContext, boolean)}. + *

+ * + *

+ * The cache maintains a single cached {@link AccessToken} per instance and proactively refreshes it before expiry + * (by default 5 minutes before the expiry time, or at the {@code refreshAt} time if provided by the credential). + * If a refresh fails while a non-expired token is still available, the cached token continues to be returned until + * it expires. + *

+ * + *

+ * When the {@code checkToForceFetchToken} flag is {@code true}, the cache compares the incoming + * {@link TokenRequestContext} (scopes, tenant ID, claims) against the context used to acquire the current cached + * token. A mismatch causes an immediate token refresh regardless of expiry. + *

+ * + *

+ * Sample: Wrapping a TokenCredential with AccessTokenCache + *

+ * + * + *
+ * TokenCredential credential = new BasicAuthenticationCredential("username", "password");
+ * AccessTokenCache tokenCache = new AccessTokenCache(credential);
+ * TokenRequestContext requestContext = new TokenRequestContext().addScopes("https://management.azure.com/.default");
+ * // Async usage
+ * Mono<AccessToken> tokenMono = tokenCache.getToken(requestContext, false);
+ * // Sync usage
+ * AccessToken token = tokenCache.getTokenSync(requestContext, false);
+ * 
+ * + * + * @see TokenCredential + * @see AccessToken + * @see TokenRequestContext + * @see SimpleTokenCache */ public final class AccessTokenCache { // The delay after a refresh to attempt another token refresh diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index 01fa5f28cd6f..80f3b823b713 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -12,7 +12,7 @@ import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpResponse; -import com.azure.core.implementation.AccessTokenCache; +import com.azure.core.credential.AccessTokenCache; import com.azure.core.implementation.http.policy.AuthorizationChallengeParser; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/AccessTokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java similarity index 99% rename from sdk/core/azure-core/src/test/java/com/azure/core/implementation/AccessTokenCacheTests.java rename to sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java index 07b45772c13f..7339b2a5e22e 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/AccessTokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.core.implementation; +package com.azure.core.credential; import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index d1062efc7d7d..15326949c12f 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -3,7 +3,7 @@ package com.azure.core.credential; -import com.azure.core.implementation.AccessTokenCache; +import com.azure.core.credential.AccessTokenCache; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; From 8cff0095067958f5f1a8a6db49bd7ea4cd96998d Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Thu, 2 Jul 2026 11:00:46 -0700 Subject: [PATCH 02/12] Add CHANGELOG entry for AccessTokenCache promotion to public API --- sdk/core/azure-core/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index a65de8736a2b..c32472fb384b 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Promoted `AccessTokenCache` to a public API in the `com.azure.core.credential` package. This class provides a thread-safe, proactively refreshing token cache that wraps a `TokenCredential`, supporting both synchronous and asynchronous token retrieval. ([#TODO](https://github.com/Azure/azure-sdk-for-java/pull/TODO)) + ### Breaking Changes ### Bugs Fixed From 18a072c02ce1737fc55135bb26d35df5dae5c05b Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Thu, 2 Jul 2026 11:07:52 -0700 Subject: [PATCH 03/12] Add CHANGELOG entry for AccessTokenCache promotion to public API --- sdk/core/azure-core/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index c32472fb384b..41f07fe5cf61 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features Added -- Promoted `AccessTokenCache` to a public API in the `com.azure.core.credential` package. This class provides a thread-safe, proactively refreshing token cache that wraps a `TokenCredential`, supporting both synchronous and asynchronous token retrieval. ([#TODO](https://github.com/Azure/azure-sdk-for-java/pull/TODO)) +- Promoted `AccessTokenCache` to a public API in the `com.azure.core.credential` package. This class provides a thread-safe, proactively refreshing token cache that wraps a `TokenCredential`, supporting both synchronous and asynchronous token retrieval. ### Breaking Changes From 327339b66a4d1b541b8708fe76787ac7473555ac Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 11:33:08 -0700 Subject: [PATCH 04/12] Fix AccessTokenCache public API: rename parameter, improve Javadoc, fix imports - Rename 'checkToForceFetchToken' to 'refreshOnContextChange' in public methods of AccessTokenCache, and propagate the rename to BearerTokenAuthenticationPolicy and PopTokenAuthenticationPolicy private helpers - Fix getTokenSync @return Javadoc (was incorrectly saying 'Publisher') - Add @throws IllegalArgumentException to both public getToken methods - Add thread-safety guarantee and single-context limitation to class Javadoc - Clarify refreshOnContextChange semantics and CAE use-case in class/param Javadoc - Fix constructor null-check error message to follow SDK convention - Fix internal null-check error messages to follow SDK convention - Add AccessTokenCacheJavadocCodeSnippets.java for the src_embed marker - Remove redundant same-package imports from AccessTokenCacheTests - Fix stale constructor Javadoc and broken import in PopTokenAuthenticationPolicy --- .../core/credential/AccessTokenCache.java | 66 ++++++++++++------- .../BearerTokenAuthenticationPolicy.java | 8 +-- .../AccessTokenCacheJavadocCodeSnippets.java | 27 ++++++++ .../credential/AccessTokenCacheTests.java | 7 +- .../PopTokenAuthenticationPolicy.java | 10 +-- 5 files changed, 81 insertions(+), 37 deletions(-) create mode 100644 sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index b68e9e90d8eb..6e666c4b1c09 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -37,9 +37,21 @@ *

* *

- * When the {@code checkToForceFetchToken} flag is {@code true}, the cache compares the incoming + * When the {@code refreshOnContextChange} flag is {@code true}, the cache compares the incoming * {@link TokenRequestContext} (scopes, tenant ID, claims) against the context used to acquire the current cached - * token. A mismatch causes an immediate token refresh regardless of expiry. + * token. A mismatch causes an immediate token refresh regardless of expiry. This is the mechanism used to support + * Continuous Access Evaluation (CAE) claims challenges. + *

+ * + *

+ * Note: Each instance caches exactly one {@link AccessToken} associated with one + * {@link TokenRequestContext}. Do not share a single {@code AccessTokenCache} instance across calls that require + * different scopes or tenants simultaneously, as each new context will evict the previously cached token. + *

+ * + *

+ * This class is thread-safe. Multiple threads may call {@link #getToken(TokenRequestContext, boolean)} and + * {@link #getTokenSync(TokenRequestContext, boolean)} concurrently. *

* *

@@ -84,12 +96,12 @@ public final class AccessTokenCache { private final Lock lock; /** - * Creates an instance of RefreshableTokenCredential with default scheme "Bearer". + * Creates an instance of {@code AccessTokenCache} that wraps the given {@link TokenCredential}. * * @param tokenCredential the token credential to be used to acquire the token. */ public AccessTokenCache(TokenCredential tokenCredential) { - Objects.requireNonNull(tokenCredential, "The token credential cannot be null"); + Objects.requireNonNull(tokenCredential, "'tokenCredential' cannot be null."); this.wip = new AtomicReference<>(); this.tokenCredential = tokenCredential; this.cacheInfo = new AtomicReference<>(new AccessTokenCacheInfo(null, OffsetDateTime.now())); @@ -106,11 +118,15 @@ public AccessTokenCache(TokenCredential tokenCredential) { * Asynchronously get a token from either the cache or replenish the cache with a new token. * * @param tokenRequestContext The request context for token acquisition. - * @param checkToForceFetchToken The flag indicating whether to force fetch a new token or not. - * @return The Publisher that emits an AccessToken + * @param refreshOnContextChange When {@code true}, compares the incoming {@link TokenRequestContext} against the + * one used to acquire the current cached token. If the scopes, tenant ID, or claims differ, a fresh token is + * fetched immediately regardless of expiry. Pass {@code false} to always use the cached token when it is + * still valid. + * @return a {@link Mono} that emits the cached or newly acquired {@link AccessToken}. + * @throws IllegalArgumentException if {@code tokenRequestContext} is {@code null}. */ - public Mono getToken(TokenRequestContext tokenRequestContext, boolean checkToForceFetchToken) { - return Mono.defer(retrieveToken(tokenRequestContext, checkToForceFetchToken)) + public Mono getToken(TokenRequestContext tokenRequestContext, boolean refreshOnContextChange) { + return Mono.defer(retrieveToken(tokenRequestContext, refreshOnContextChange)) // Keep resubscribing as long as Mono.defer [token acquisition] emits empty(). .repeatWhenEmpty((Flux longFlux) -> longFlux .concatMap(ignored -> Flux.just(true).delayElements(Duration.ofMillis(500)))); @@ -120,25 +136,29 @@ public Mono getToken(TokenRequestContext tokenRequestContext, boole * Synchronously get a token from either the cache or replenish the cache with a new token. * * @param tokenRequestContext The request context for token acquisition. - * @param checkToForceFetchToken The flag indicating whether to force fetch a new token or not. - * @return The Publisher that emits an AccessToken + * @param refreshOnContextChange When {@code true}, compares the incoming {@link TokenRequestContext} against the + * one used to acquire the current cached token. If the scopes, tenant ID, or claims differ, a fresh token is + * fetched immediately regardless of expiry. Pass {@code false} to always use the cached token when it is + * still valid. + * @return the cached or newly acquired {@link AccessToken}. + * @throws IllegalArgumentException if {@code tokenRequestContext} is {@code null}. */ - public AccessToken getTokenSync(TokenRequestContext tokenRequestContext, boolean checkToForceFetchToken) { + public AccessToken getTokenSync(TokenRequestContext tokenRequestContext, boolean refreshOnContextChange) { lock.lock(); try { - return retrieveTokenSync(tokenRequestContext, checkToForceFetchToken).get(); + return retrieveTokenSync(tokenRequestContext, refreshOnContextChange).get(); } finally { lock.unlock(); } } private Supplier> retrieveToken(TokenRequestContext tokenRequestContext, - boolean checkToForceFetchToken) { + boolean refreshOnContextChange) { return () -> { try { if (tokenRequestContext == null) { return Mono.error(LOGGER.logExceptionAsError( - new IllegalArgumentException("The token request context input cannot be null."))); + new IllegalArgumentException("'tokenRequestContext' cannot be null."))); } AccessTokenCacheInfo cache = this.cacheInfo.get(); @@ -151,9 +171,9 @@ private Supplier> retrieveToken(TokenRequestContext Mono fallback; // Check if the incoming token request context is different from the cached one. A different - // token request context, requires to fetch a new token as the cached one won't work for the + // token request context requires fetching a new token as the cached one won't work for the // passed in token request context. - boolean forceRefresh = (checkToForceFetchToken && checkIfForceRefreshRequired(tokenRequestContext)) + boolean forceRefresh = (refreshOnContextChange && checkIfForceRefreshRequired(tokenRequestContext)) || this.tokenRequestContext == null; if (forceRefresh) { @@ -189,12 +209,12 @@ private Supplier> retrieveToken(TokenRequestContext .flatMap(processTokenRefreshResult(sinksOne, now, fallback)) .doOnError(sinksOne::tryEmitError), w -> w.set(null)); - } else if (cachedToken != null && !cachedToken.isExpired() && !checkToForceFetchToken) { + } else if (cachedToken != null && !cachedToken.isExpired() && !refreshOnContextChange) { // another thread might be refreshing the token proactively, but the current token is still valid return Mono.just(cachedToken); } else { - // if a force refresh is possible, then exit and retry. - if (checkToForceFetchToken) { + // if a context-change refresh is pending, exit and retry. + if (refreshOnContextChange) { return Mono.empty(); } // another thread is definitely refreshing the expired token @@ -214,11 +234,11 @@ private Supplier> retrieveToken(TokenRequestContext } private Supplier retrieveTokenSync(TokenRequestContext tokenRequestContext, - boolean checkToForceFetchToken) { + boolean refreshOnContextChange) { return () -> { if (tokenRequestContext == null) { throw LOGGER.logExceptionAsError( - new IllegalArgumentException("The token request context input cannot be null.")); + new IllegalArgumentException("'tokenRequestContext' cannot be null.")); } AccessTokenCacheInfo cache = this.cacheInfo.get(); AccessToken cachedToken = cache.getCachedAccessToken(); @@ -228,9 +248,9 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest AccessToken fallback; // Check if the incoming token request context is different from the cached one. A different - // token request context, requires to fetch a new token as the cached one won't work for the + // token request context requires fetching a new token as the cached one won't work for the // passed in token request context. - boolean forceRefresh = (checkToForceFetchToken && checkIfForceRefreshRequired(tokenRequestContext)) + boolean forceRefresh = (refreshOnContextChange && checkIfForceRefreshRequired(tokenRequestContext)) || this.tokenRequestContext == null; if (forceRefresh) { diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java index 80f3b823b713..549cc18faa89 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/BearerTokenAuthenticationPolicy.java @@ -208,16 +208,16 @@ public void setAuthorizationHeaderSync(HttpPipelineCallContext context, TokenReq } private Mono setAuthorizationHeaderHelper(HttpPipelineCallContext context, - TokenRequestContext tokenRequestContext, boolean checkToForceFetchToken) { - return cache.getToken(tokenRequestContext, checkToForceFetchToken).flatMap(token -> { + TokenRequestContext tokenRequestContext, boolean refreshOnContextChange) { + return cache.getToken(tokenRequestContext, refreshOnContextChange).flatMap(token -> { setAuthorizationHeader(context.getHttpRequest().getHeaders(), token.getToken()); return Mono.empty(); }); } private void setAuthorizationHeaderHelperSync(HttpPipelineCallContext context, - TokenRequestContext tokenRequestContext, boolean checkToForceFetchToken) { - AccessToken token = cache.getTokenSync(tokenRequestContext, checkToForceFetchToken); + TokenRequestContext tokenRequestContext, boolean refreshOnContextChange) { + AccessToken token = cache.getTokenSync(tokenRequestContext, refreshOnContextChange); setAuthorizationHeader(context.getHttpRequest().getHeaders(), token.getToken()); } diff --git a/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java b/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java new file mode 100644 index 000000000000..41c251f70e36 --- /dev/null +++ b/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.credential; + +import reactor.core.publisher.Mono; + +import java.time.OffsetDateTime; + +/** + * Codesnippets for {@link AccessTokenCache}. + */ +public class AccessTokenCacheJavadocCodeSnippets { + + public void accessTokenCacheSnippet() { + // BEGIN: com.azure.core.credential.accessTokenCache + TokenCredential credential = new BasicAuthenticationCredential("username", "password"); + AccessTokenCache tokenCache = new AccessTokenCache(credential); + TokenRequestContext requestContext = new TokenRequestContext().addScopes("https://management.azure.com/.default"); + // Async usage + Mono tokenMono = tokenCache.getToken(requestContext, false); + // Sync usage + AccessToken token = tokenCache.getTokenSync(requestContext, false); + // END: com.azure.core.credential.accessTokenCache + } + +} diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java index 7339b2a5e22e..a673a9ee3ae2 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/AccessTokenCacheTests.java @@ -3,9 +3,6 @@ package com.azure.core.credential; -import com.azure.core.credential.AccessToken; -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -83,7 +80,7 @@ public void testTenantIdChangeTriggersForceRefresh() { assertEquals(1, refreshCount.get()); - // Second request with same tenant should not trigger refresh (checkToForceFetchToken=false) + // Second request with same tenant should not trigger refresh (refreshOnContextChange=false) StepVerifier.create(cache.getToken(context1, false)).assertNext(token -> { assertNotNull(token); assertEquals("token-1", token.getToken()); // Same token @@ -91,7 +88,7 @@ public void testTenantIdChangeTriggersForceRefresh() { assertEquals(1, refreshCount.get()); // No additional refresh - // Third request with different tenant should trigger force refresh with checkToForceFetchToken=true + // Third request with different tenant should trigger force refresh with refreshOnContextChange=true TokenRequestContext context2 = new TokenRequestContext().addScopes(SCOPE).setTenantId(TENANT_ID_2); StepVerifier.create(cache.getToken(context2, true)).assertNext(token -> { diff --git a/sdk/identity/azure-identity-broker/src/test/java/com/azure/identity/broker/shr/resources/PopTokenAuthenticationPolicy.java b/sdk/identity/azure-identity-broker/src/test/java/com/azure/identity/broker/shr/resources/PopTokenAuthenticationPolicy.java index 87f55ea0f004..5376a6cb148e 100644 --- a/sdk/identity/azure-identity-broker/src/test/java/com/azure/identity/broker/shr/resources/PopTokenAuthenticationPolicy.java +++ b/sdk/identity/azure-identity-broker/src/test/java/com/azure/identity/broker/shr/resources/PopTokenAuthenticationPolicy.java @@ -14,7 +14,7 @@ import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.core.implementation.AccessTokenCache; +import com.azure.core.credential.AccessTokenCache; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; @@ -169,7 +169,7 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex } } - private Mono setAuthorizationHeaderHelper(HttpPipelineCallContext context, boolean checkToForceFetchToken) { + private Mono setAuthorizationHeaderHelper(HttpPipelineCallContext context, boolean refreshOnContextChange) { if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { throw LOGGER.logExceptionAsError(new RuntimeException( "Proof of possession token authentication is not permitted for non TLS-protected (HTTPS) endpoints.")); @@ -181,7 +181,7 @@ private Mono setAuthorizationHeaderHelper(HttpPipelineCallContext context, .setRequestUrl(context.getHttpRequest().getUrl())); if (!CoreUtils.isNullOrEmpty(popNonce)) { - return this.cache.getToken(popTokenRequestContext, checkToForceFetchToken).flatMap((token) -> { + return this.cache.getToken(popTokenRequestContext, refreshOnContextChange).flatMap((token) -> { setAuthorizationHeader(context.getHttpRequest().getHeaders(), token.getToken()); return Mono.empty(); }); @@ -189,7 +189,7 @@ private Mono setAuthorizationHeaderHelper(HttpPipelineCallContext context, return Mono.empty(); } - private void setAuthorizationHeaderHelperSync(HttpPipelineCallContext context, boolean checkToForceFetchToken) { + private void setAuthorizationHeaderHelperSync(HttpPipelineCallContext context, boolean refreshOnContextChange) { if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { throw LOGGER.logExceptionAsError(new RuntimeException( "Proof of possession token authentication is not permitted for non TLS-protected (HTTPS) endpoints.")); @@ -199,7 +199,7 @@ private void setAuthorizationHeaderHelperSync(HttpPipelineCallContext context, b .setRequestMethod(context.getHttpRequest().getHttpMethod()) .setRequestUrl(context.getHttpRequest().getUrl())); - AccessToken token = this.cache.getTokenSync(popTokenRequestContext, checkToForceFetchToken); + AccessToken token = this.cache.getTokenSync(popTokenRequestContext, refreshOnContextChange); setAuthorizationHeader(context.getHttpRequest().getHeaders(), token.getToken()); } From 324fc0120aa9f8444a98f0975e7563f5fb2e4715 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 12:11:29 -0700 Subject: [PATCH 05/12] Fix azure-identity-broker CI: build azure-core from source for PopTokenAuthenticationPolicy PopTokenAuthenticationPolicy uses com.azure.core.credential.AccessTokenCache which is new in azure-core 1.59.0-beta.1. The azure-identity CI was building against the published azure-core 1.58.1 (which doesn't have the public class), causing a compile error. - Add azure-core to AdditionalModules in sdk/identity/ci.yml so the identity CI builds it from source before compiling azure-identity-broker - Add azure-core 1.59.0-beta.1 as a direct test dependency in azure-identity-broker pom so Maven resolves the locally built version over the transitive published one --- eng/versioning/version_client.txt | 1 + sdk/identity/azure-identity/pom.xml | 2 +- sdk/identity/ci.yml | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 1a206ffb4585..3d192ffd3ec9 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -567,6 +567,7 @@ io.clientcore:optional-dependency-tests;1.0.0-beta.1;1.0.0-beta.1 # In the pom, the version update tag after the version should name the unreleased package and the dependency version: # +unreleased_com.azure:azure-core;1.59.0-beta.1 unreleased_com.azure.v2:azure-core;2.0.0-beta.1 unreleased_com.azure.v2:azure-identity;2.0.0-beta.1 unreleased_com.azure.v2:azure-data-appconfiguration;2.0.0-beta.1 diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 343865302e0d..9a49005d3214 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -35,7 +35,7 @@ com.azure azure-core - 1.58.1 + 1.59.0-beta.1 diff --git a/sdk/identity/ci.yml b/sdk/identity/ci.yml index 9dc5816b6e01..9fc5fe9bcd1d 100644 --- a/sdk/identity/ci.yml +++ b/sdk/identity/ci.yml @@ -74,6 +74,9 @@ extends: # required by the above perf library - name: perf-test-core groupId: com.azure + # required by azure-identity-broker tests (PopTokenAuthenticationPolicy uses public AccessTokenCache) + - name: azure-core + groupId: com.azure LiveTestStages: - template: /eng/pipelines/templates/stages/archetype-sdk-tests-isolated.yml parameters: From 3fc29c83a5b509b1a0ef5dfb09a29a90a0cbf885 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 12:28:42 -0700 Subject: [PATCH 06/12] Remove redundant same-package import of AccessTokenCache in TokenCacheTests --- .../src/test/java/com/azure/core/credential/TokenCacheTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java index 15326949c12f..1e6238ee476d 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/credential/TokenCacheTests.java @@ -3,7 +3,6 @@ package com.azure.core.credential; -import com.azure.core.credential.AccessTokenCache; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; From 40715202c80f9bf231dcf25625c13b3b646c9c46 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 12:30:32 -0700 Subject: [PATCH 07/12] Update azure-identity-perf to use unreleased azure-core 1.59.0-beta.1 --- sdk/identity/azure-identity-perf/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity-perf/pom.xml b/sdk/identity/azure-identity-perf/pom.xml index f50f3bdb06c4..7e71e6ae4c8c 100644 --- a/sdk/identity/azure-identity-perf/pom.xml +++ b/sdk/identity/azure-identity-perf/pom.xml @@ -36,7 +36,7 @@ com.azure azure-core - 1.58.1 + 1.59.0-beta.1 From 6b68986e30d9871510220fdc79f05a6142e45e75 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 13:16:55 -0700 Subject: [PATCH 08/12] Fix Spotless formatting violations in AccessTokenCache --- .../java/com/azure/core/credential/AccessTokenCache.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index 6e666c4b1c09..5c5e775dc179 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -157,8 +157,8 @@ private Supplier> retrieveToken(TokenRequestContext return () -> { try { if (tokenRequestContext == null) { - return Mono.error(LOGGER.logExceptionAsError( - new IllegalArgumentException("'tokenRequestContext' cannot be null."))); + return Mono.error(LOGGER + .logExceptionAsError(new IllegalArgumentException("'tokenRequestContext' cannot be null."))); } AccessTokenCacheInfo cache = this.cacheInfo.get(); @@ -237,8 +237,7 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest boolean refreshOnContextChange) { return () -> { if (tokenRequestContext == null) { - throw LOGGER.logExceptionAsError( - new IllegalArgumentException("'tokenRequestContext' cannot be null.")); + throw LOGGER.logExceptionAsError(new IllegalArgumentException("'tokenRequestContext' cannot be null.")); } AccessTokenCacheInfo cache = this.cacheInfo.get(); AccessToken cachedToken = cache.getCachedAccessToken(); From 579c9a12b16e1f5598a4ec43db0c4d7bbd2f3f8f Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 14:53:10 -0700 Subject: [PATCH 09/12] Fix Checkstyle linting violations in AccessTokenCache and snippets - Use LOGGER.logThrowableAsError() instead of bare throw in retrieveTokenSync (ThrowFromClientLoggerCheck violation) - Remove unused java.time.OffsetDateTime import from AccessTokenCacheJavadocCodeSnippets (UnusedImportsCheck violation) --- .../main/java/com/azure/core/credential/AccessTokenCache.java | 2 +- .../core/credential/AccessTokenCacheJavadocCodeSnippets.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index 5c5e775dc179..4ec5e6246836 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -300,7 +300,7 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest if (fallback != null) { return fallback; } - throw error; + throw LOGGER.logThrowableAsError(error); } }; } diff --git a/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java b/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java index 41c251f70e36..bbb20abf70e6 100644 --- a/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java +++ b/sdk/core/azure-core/src/samples/java/com/azure/core/credential/AccessTokenCacheJavadocCodeSnippets.java @@ -5,8 +5,6 @@ import reactor.core.publisher.Mono; -import java.time.OffsetDateTime; - /** * Codesnippets for {@link AccessTokenCache}. */ From 03a5062e2d900a99fee0ac9bf91db0487dca5570 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 15:09:24 -0700 Subject: [PATCH 10/12] Fix compilation error: narrow Throwable catch to RuntimeException in retrieveTokenSync logThrowableAsError returns Throwable which cannot be thrown from a Supplier lambda. Supplier.get() only throws RuntimeException, so narrowing the catch is correct. Use logExceptionAsError which returns RuntimeException and compiles cleanly. --- .../main/java/com/azure/core/credential/AccessTokenCache.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index 4ec5e6246836..6ceb65105250 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -291,7 +291,7 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest } else { return fallback; } - } catch (Throwable error) { + } catch (RuntimeException error) { buildTokenRefreshLog(LogLevel.ERROR, cachedToken, now).log("Failed to acquire a new access token.", error); OffsetDateTime nextTokenRefreshTime = OffsetDateTime.now(); @@ -300,7 +300,7 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest if (fallback != null) { return fallback; } - throw LOGGER.logThrowableAsError(error); + throw LOGGER.logExceptionAsError(error); } }; } From c7ec5febb9c36a96629d29f641fabc804ff7ea2b Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 15:20:04 -0700 Subject: [PATCH 11/12] Catch Throwable in retrieveTokenSync, wrap non-RuntimeException before rethrowing Error subclasses (OutOfMemoryError etc.) should also be caught. Since the Supplier lambda can only propagate RuntimeException, non-RuntimeException Throwables are wrapped in RuntimeException before being thrown through LOGGER.logExceptionAsError. --- .../java/com/azure/core/credential/AccessTokenCache.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index 6ceb65105250..5006c1a31918 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -291,7 +291,7 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest } else { return fallback; } - } catch (RuntimeException error) { + } catch (Throwable error) { buildTokenRefreshLog(LogLevel.ERROR, cachedToken, now).log("Failed to acquire a new access token.", error); OffsetDateTime nextTokenRefreshTime = OffsetDateTime.now(); @@ -300,7 +300,10 @@ private Supplier retrieveTokenSync(TokenRequestContext tokenRequest if (fallback != null) { return fallback; } - throw LOGGER.logExceptionAsError(error); + if (error instanceof RuntimeException) { + throw LOGGER.logExceptionAsError((RuntimeException) error); + } + throw LOGGER.logExceptionAsError(new RuntimeException(error)); } }; } From ca75b1dcc9f0e4a12a2792b1b01149b610115e62 Mon Sep 17 00:00:00 2001 From: Vinay Gera Date: Fri, 17 Jul 2026 15:39:48 -0700 Subject: [PATCH 12/12] Fix codesnippet: escape URL slashes in accessTokenCache Javadoc snippet --- .../main/java/com/azure/core/credential/AccessTokenCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java index 5006c1a31918..d1589ec260d2 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/credential/AccessTokenCache.java @@ -62,7 +62,7 @@ *

  * TokenCredential credential = new BasicAuthenticationCredential("username", "password");
  * AccessTokenCache tokenCache = new AccessTokenCache(credential);
- * TokenRequestContext requestContext = new TokenRequestContext().addScopes("https://management.azure.com/.default");
+ * TokenRequestContext requestContext = new TokenRequestContext().addScopes("https://management.azure.com/.default");
  * // Async usage
  * Mono<AccessToken> tokenMono = tokenCache.getToken(requestContext, false);
  * // Sync usage