diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 2da8ec3ec8f1..a8c4e0ab0c78 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- Disabled MSAL's internal retry for Confidential Client, Managed Identity and Public Client Applications. + ### Other Changes ## 1.19.0-beta.2 (2026-02-25) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java index d8c3bd0a8103..0832a47f804b 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClientBase.java @@ -238,7 +238,8 @@ ConfidentialClientApplication getConfidentialClient(boolean enableCae) { try { applicationBuilder = applicationBuilder.logPii(options.isUnsafeSupportLoggingEnabled()) .authority(authorityUrl) - .instanceDiscovery(options.isInstanceDiscoveryEnabled()); + .instanceDiscovery(options.isInstanceDiscoveryEnabled()) + .disableInternalRetries(); if (!options.isInstanceDiscoveryEnabled()) { LOGGER.log(LogLevel.VERBOSE, () -> "Instance discovery and authority validation is disabled. In this" @@ -309,7 +310,8 @@ PublicClientApplication getPublicClient(boolean sharedTokenCacheCredential, bool try { builder = builder.logPii(options.isUnsafeSupportLoggingEnabled()) .authority(authorityUrl) - .instanceDiscovery(options.isInstanceDiscoveryEnabled()); + .instanceDiscovery(options.isInstanceDiscoveryEnabled()) + .disableInternalRetries(); if (!options.isInstanceDiscoveryEnabled()) { LOGGER.log(LogLevel.VERBOSE, () -> "Instance discovery and authority validation is disabled. In this" @@ -408,8 +410,9 @@ ManagedIdentityApplication getManagedIdentityMsalApplication() { managedIdentityId = ManagedIdentityId.systemAssigned(); } - ManagedIdentityApplication.Builder miBuilder - = ManagedIdentityApplication.builder(managedIdentityId).logPii(options.isUnsafeSupportLoggingEnabled()); + ManagedIdentityApplication.Builder miBuilder = ManagedIdentityApplication.builder(managedIdentityId) + .logPii(options.isUnsafeSupportLoggingEnabled()) + .disableInternalRetries(); ManagedIdentitySourceType managedIdentitySourceType = ManagedIdentityApplication.getManagedIdentitySource(); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java index 072e4c2d80f8..b16ad87f22c6 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java @@ -4,8 +4,18 @@ package com.azure.identity.implementation; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.exception.ClientAuthenticationException; +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.FixedDelayOptions; +import com.azure.core.http.policy.RetryOptions; +import com.azure.core.http.policy.RetryPolicy; +import com.azure.core.http.policy.FixedDelay; +import com.azure.core.test.http.MockHttpResponse; import com.azure.core.test.utils.TestConfigurationSource; import com.azure.core.util.Configuration; +import com.azure.core.util.Context; import com.azure.identity.DefaultAzureCredential; import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.identity.implementation.util.CertificateUtil; @@ -29,6 +39,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.exceptions.misusing.InvalidUseOfMatchersException; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import javax.net.ssl.HttpsURLConnection; @@ -41,10 +52,12 @@ import java.nio.charset.Charset; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.time.Duration; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -981,4 +994,115 @@ public void testManagedCredentialSkipsImdsProbing() { } } + @Test + public void testConfidentialClientRetryCountWithDisabledMsalInternalRetries() { + AtomicInteger requestCount = new AtomicInteger(0); + final int RETRY_COUNT = 2; + + HttpClient mockHttpClient = new HttpClient() { + @Override + public HttpResponse sendSync(HttpRequest request, Context context) { + requestCount.incrementAndGet(); + return new MockHttpResponse(request, 500); + } + + @Override + public Mono send(HttpRequest request) { + requestCount.incrementAndGet(); + return Mono.just(new MockHttpResponse(request, 500)); + } + }; + + IdentityClientOptions options = new IdentityClientOptions() + .setHttpClient(mockHttpClient) + .disableInstanceDiscovery() + .setRetryPolicy(new RetryPolicy(new FixedDelay(RETRY_COUNT, Duration.ofMillis(1)))); + + IdentityClient client = new IdentityClientBuilder().tenantId(TENANT_ID) + .clientId(CLIENT_ID) + .clientSecret("test-secret") + .identityClientOptions(options) + .build(); + + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); + + StepVerifier.create(client.authenticateWithConfidentialClient(request)) + .expectErrorMatches(e -> e instanceof MsalServiceException).verify(); + + assertEquals(RETRY_COUNT + 1, requestCount.get(), "With maxRetries=" + RETRY_COUNT + ", total requests should be " + (RETRY_COUNT + 1) + " (1 initial + " + RETRY_COUNT + " retries)"); + } + + @Test + public void testPublicClientRetryCountWithDisabledMsalInternalRetries() { + AtomicInteger requestCount = new AtomicInteger(0); + final int RETRY_COUNT = 2; + + HttpClient mockHttpClient = new HttpClient() { + @Override + public HttpResponse sendSync(HttpRequest request, Context context) { + requestCount.incrementAndGet(); + return new MockHttpResponse(request, 503); + } + + @Override + public Mono send(HttpRequest request) { + requestCount.incrementAndGet(); + return Mono.just(new MockHttpResponse(request, 503)); + } + }; + + IdentityClientOptions options = new IdentityClientOptions().setHttpClient(mockHttpClient) + .disableInstanceDiscovery() + .setRetryOptions(new RetryOptions(new FixedDelayOptions(RETRY_COUNT, Duration.ofMillis(1)))); + + IdentityClient client = new IdentityClientBuilder().tenantId(TENANT_ID) + .clientId(CLIENT_ID) + .identityClientOptions(options) + .build(); + + TokenRequestContext request = new TokenRequestContext().addScopes("https://management.azure.com/.default"); + + StepVerifier.create(client.authenticateWithUsernamePassword(request, "test-username", "test-password")) + .expectErrorMatches(e -> e instanceof ClientAuthenticationException).verify(); + + assertEquals(RETRY_COUNT + 1, requestCount.get(), "With maxRetries=" + RETRY_COUNT + ", total requests should be " + (RETRY_COUNT + 1) + " (1 initial + " + RETRY_COUNT + " retries)"); + } + + @Test + public void testManagedIdentityClientRetryCountWithDisabledMsalInternalRetries() { + AtomicInteger requestCount = new AtomicInteger(0); + final int RETRY_COUNT = 2; + + HttpClient mockHttpClient = new HttpClient() { + @Override + public HttpResponse sendSync(HttpRequest request, Context context) { + requestCount.incrementAndGet(); + return new MockHttpResponse(request, 500); + } + + @Override + public Mono send(HttpRequest request) { + requestCount.incrementAndGet(); + return Mono.just(new MockHttpResponse(request, 500)); + } + }; + + RetryPolicy retryPolicy = new RetryPolicy(new FixedDelay(RETRY_COUNT, Duration.ofMillis(1))); + + IdentityClientOptions options = new IdentityClientOptions() + .setHttpClient(mockHttpClient) + .setRetryPolicy(retryPolicy); + + IdentityClient client = new IdentityClientBuilder() + .identityClientOptions(options) + .build(); + + StepVerifier.create(client.authenticateWithManagedIdentityMsalClient(new TokenRequestContext().addScopes("https://management.azure.com/.default"))) + .expectErrorMatches(e -> e instanceof ClientAuthenticationException) + .verify(); + + assertEquals(RETRY_COUNT + 1, requestCount.get(), + "With maxRetries=" + RETRY_COUNT + ", total requests should be " + (RETRY_COUNT + 1) + " (1 initial + " + RETRY_COUNT + " retries)"); + } + }