From 602a36a4f23ac3c874a4e26a02427ba36762e917 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 8 May 2026 13:31:07 +0800 Subject: [PATCH 01/14] Try to enable EventHubsKafkaBinderOAuthIT --- .../tests/eventhubs/binder/EventHubsKafkaBinderOAuthIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/eventhubs/binder/EventHubsKafkaBinderOAuthIT.java b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/eventhubs/binder/EventHubsKafkaBinderOAuthIT.java index 0654af2461b7..cfc3aaa4bc09 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/eventhubs/binder/EventHubsKafkaBinderOAuthIT.java +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/eventhubs/binder/EventHubsKafkaBinderOAuthIT.java @@ -5,7 +5,6 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,7 +28,6 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("eventhubs-kafka-binder-oauth") -@Disabled("Pipeline oauth is not enabled now") class EventHubsKafkaBinderOAuthIT { private static final Logger LOGGER = LoggerFactory.getLogger(EventHubsKafkaBinderOAuthIT.class); From a597bd98121aeaa33cdcb9acdc121b3c8313f957 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 13:29:32 +0800 Subject: [PATCH 02/14] Fix Event Hubs Kafka OAuth test compatibility --- eng/versioning/external_dependencies.txt | 1 + sdk/spring/spring-cloud-azure-integration-tests/pom.xml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index a2a2fd3d2c26..44dd1e583f68 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -316,6 +316,7 @@ springboot4_net.bytebuddy:byte-buddy-agent;1.17.8 springboot4_net.bytebuddy:byte-buddy;1.17.8 springboot4_org.apache.commons:commons-lang3;3.19.0 springboot4_org.apache.kafka:kafka-clients;4.1.2 +springboot4_kafka_compat_org.apache.kafka:kafka-clients;3.9.0 springboot4_org.apache.maven.plugins:maven-antrun-plugin;3.2.0 springboot4_org.apache.maven.plugins:maven-compiler-plugin;3.14.1 springboot4_org.apache.maven.plugins:maven-enforcer-plugin;3.6.2 diff --git a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml index 758d6560b8cd..eebfbb82f579 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml @@ -39,6 +39,14 @@ + + + org.apache.kafka + kafka-clients + 3.9.0 + com.azure.spring spring-cloud-azure-starter-servicebus From 09f66f46c29f4b7d7487052ff867ca5077193543 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 14:05:40 +0800 Subject: [PATCH 03/14] Tune Event Hubs Kafka OAuth test compatibility --- eng/versioning/external_dependencies.txt | 2 +- sdk/spring/spring-cloud-azure-integration-tests/pom.xml | 6 +++--- .../resources/application-eventhubs-kafka-binder-oauth.yml | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 44dd1e583f68..99033ba5c576 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -316,7 +316,7 @@ springboot4_net.bytebuddy:byte-buddy-agent;1.17.8 springboot4_net.bytebuddy:byte-buddy;1.17.8 springboot4_org.apache.commons:commons-lang3;3.19.0 springboot4_org.apache.kafka:kafka-clients;4.1.2 -springboot4_kafka_compat_org.apache.kafka:kafka-clients;3.9.0 +springboot4_kafka_compat_org.apache.kafka:kafka-clients;3.8.1 springboot4_org.apache.maven.plugins:maven-antrun-plugin;3.2.0 springboot4_org.apache.maven.plugins:maven-compiler-plugin;3.14.1 springboot4_org.apache.maven.plugins:maven-enforcer-plugin;3.6.2 diff --git a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml index eebfbb82f579..bc8d8ae560dc 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml @@ -39,13 +39,13 @@ - org.apache.kafka kafka-clients - 3.9.0 + 3.8.1 com.azure.spring diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml index c15630d9540d..ce0c50298bda 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml @@ -1,4 +1,9 @@ spring: + kafka: + properties: + request.timeout.ms: 60000 + metadata.max.age.ms: 180000 + connections.max.idle.ms: 180000 cloud: function: definition: consume;supply From 7ec962d55fa70cb94a80b3346928958ac1b33ccf Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 15:05:53 +0800 Subject: [PATCH 04/14] Disable Kafka binder AdminClient for Event Hubs OAuth IT Revert the kafka-clients 3.8.1 override (incompatible with spring-kafka 4.x, caused VerifyError in EventHubsKafkaBinderConnectionStringIT) and instead disable the Spring Cloud Stream Kafka binder's topic auto-creation and admin-based health check. Azure Event Hubs for Kafka does not support the Kafka AdminClient API, so the binder must not create one against the Event Hubs endpoint. --- eng/versioning/external_dependencies.txt | 1 - sdk/spring/spring-cloud-azure-integration-tests/pom.xml | 8 -------- .../application-eventhubs-kafka-binder-oauth.yml | 6 ++++++ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 99033ba5c576..a2a2fd3d2c26 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -316,7 +316,6 @@ springboot4_net.bytebuddy:byte-buddy-agent;1.17.8 springboot4_net.bytebuddy:byte-buddy;1.17.8 springboot4_org.apache.commons:commons-lang3;3.19.0 springboot4_org.apache.kafka:kafka-clients;4.1.2 -springboot4_kafka_compat_org.apache.kafka:kafka-clients;3.8.1 springboot4_org.apache.maven.plugins:maven-antrun-plugin;3.2.0 springboot4_org.apache.maven.plugins:maven-compiler-plugin;3.14.1 springboot4_org.apache.maven.plugins:maven-enforcer-plugin;3.6.2 diff --git a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml index bc8d8ae560dc..758d6560b8cd 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-tests/pom.xml @@ -39,14 +39,6 @@ - - - org.apache.kafka - kafka-clients - 3.8.1 - com.azure.spring spring-cloud-azure-starter-servicebus diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml index ce0c50298bda..b6e400219127 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml @@ -11,6 +11,12 @@ spring: kafka: binder: brokers: ${AZURE_EVENTHUBS_NAMESPACE}.servicebus.windows.net:9093 + # Azure Event Hubs for Kafka does not support the Kafka AdminClient API + # (topic management must be done via Azure portal/CLI/ARM). Disable + # binder-side topic provisioning and admin-based health checks so the + # binder does not create an AdminClient against the Event Hubs endpoint. + auto-create-topics: false + health-timeout: 0 bindings: consume-in-0: destination: aad-auth From c0cb7a58f33db2f8f70e12bc9574a7ba3dfead37 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 16:20:49 +0800 Subject: [PATCH 05/14] Use AzurePipelinesCredential for EventHubs Kafka OAuth IT Add a test-only AuthenticateCallbackHandler that uses the same federated identity (AzurePipelinesCredential via TestCredentialUtils) the rest of the integration tests use. Wire it through spring.cloud.stream.kafka.binder.configuration so the binder's producer, consumer and provisioning AdminClient all authenticate as testApplicationOid (which the bicep grants Event Hubs Data Owner to) instead of falling back to the agent's managed identity. --- .../util/TestKafkaOAuth2CallbackHandler.java | 121 ++++++++++++++++++ ...plication-eventhubs-kafka-binder-oauth.yml | 11 ++ 2 files changed, 132 insertions(+) create mode 100644 sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java new file mode 100644 index 000000000000..59faf4b90524 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.integration.tests.util; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; + +/** + * Test-only {@link AuthenticateCallbackHandler} that authenticates against Azure Event Hubs Kafka endpoint + * using the same {@link TokenCredential} chain as the rest of the integration tests + * ({@link TestCredentialUtils#getIntegrationTestTokenCredential()}). + * + *

This bypasses the production {@code KafkaOAuth2AuthenticateCallbackHandler}, which on a CI agent ends + * up using a {@code DefaultAzureCredential} that resolves to the agent's managed identity (which is not + * granted the {@code Azure Event Hubs Data Owner} role by the test bicep). The federated + * {@code AzurePipelinesCredential} used here corresponds to the {@code testApplicationOid} that the bicep + * actually grants permissions to.

+ */ +public class TestKafkaOAuth2CallbackHandler implements AuthenticateCallbackHandler { + + private static final Duration ACCESS_TOKEN_REQUEST_BLOCK_TIME = Duration.ofSeconds(30); + private static final String TOKEN_AUDIENCE_FORMAT = "%s://%s/.default"; + + private TokenCredential credential; + private TokenRequestContext tokenRequestContext; + + @Override + @SuppressWarnings("unchecked") + public void configure(Map configs, String mechanism, List jaasConfigEntries) { + List bootstrapServers = (List) configs.get(BOOTSTRAP_SERVERS_CONFIG); + if (bootstrapServers == null || bootstrapServers.isEmpty()) { + throw new IllegalArgumentException("bootstrap.servers must be configured for Azure Event Hubs."); + } + String bootstrap = bootstrapServers.get(0); + URI uri = URI.create("https://" + bootstrap); + String audience = String.format(TOKEN_AUDIENCE_FORMAT, uri.getScheme(), uri.getHost()); + + this.tokenRequestContext = new TokenRequestContext(); + this.tokenRequestContext.addScopes(audience); + this.credential = TestCredentialUtils.getIntegrationTestTokenCredential(); + } + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerTokenCallback) { + OAuthBearerTokenCallback oauthCallback = (OAuthBearerTokenCallback) callback; + try { + AccessToken accessToken = credential.getToken(tokenRequestContext).block(ACCESS_TOKEN_REQUEST_BLOCK_TIME); + if (accessToken == null) { + oauthCallback.error("invalid_grant", "Failed to acquire token from credential chain.", null); + } else { + oauthCallback.token(new SimpleOAuthBearerToken(accessToken)); + } + } catch (RuntimeException e) { + oauthCallback.error("invalid_grant", e.getMessage(), null); + } + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + @Override + public void close() { + // NOOP + } + + /** + * Minimal {@link OAuthBearerToken} that exposes only what the Kafka client needs for SASL handshake: + * the bearer token string and its expiration time. + */ + private static final class SimpleOAuthBearerToken implements OAuthBearerToken { + private final AccessToken accessToken; + + SimpleOAuthBearerToken(AccessToken accessToken) { + this.accessToken = accessToken; + } + + @Override + public String value() { + return accessToken.getToken(); + } + + @Override + public Long startTimeMs() { + return null; + } + + @Override + public long lifetimeMs() { + return accessToken.getExpiresAt().toInstant().toEpochMilli(); + } + + @Override + public Set scope() { + return null; + } + + @Override + public String principalName() { + return "azure-event-hubs-kafka-oauth-test"; + } + } +} diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml index b6e400219127..a4f0ad628407 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml @@ -17,6 +17,17 @@ spring: # binder does not create an AdminClient against the Event Hubs endpoint. auto-create-topics: false health-timeout: 0 + # Force the binder's producer/consumer/admin clients to use our test + # callback handler, which authenticates with the federated + # AzurePipelinesCredential (testApplicationOid) the bicep grants + # Event Hubs Data Owner to. Otherwise spring-cloud-azure's default + # KafkaOAuth2AuthenticateCallbackHandler falls back to + # DefaultAzureCredential -> agent managed identity, which has no role. + configuration: + security.protocol: SASL_SSL + sasl.mechanism: OAUTHBEARER + sasl.jaas.config: org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required; + sasl.login.callback.handler.class: com.azure.spring.cloud.integration.tests.util.TestKafkaOAuth2CallbackHandler bindings: consume-in-0: destination: aad-auth From 4b786101b69864b2970fa16fd7bf98cf665eb437 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 16:37:52 +0800 Subject: [PATCH 06/14] Add AzurePipelinesCredential fallback to Kafka OAuth2 handler Extend InternalCredentialResolver in KafkaOAuth2AuthenticateCallbackHandler: when neither configs nor AzureTokenCredentialResolver yield a credential, chain AzurePipelinesCredential (built from AZURESUBSCRIPTION_* + SYSTEM_ACCESSTOKEN, when all present) before DefaultAzureCredential. This lets Spring Cloud Azure Kafka OAuth2 authenticate to Event Hubs from an Azure DevOps pipeline job using a federated workload-identity service connection -- which DefaultAzureCredential's chain does not cover because AzurePipelinesCredential's service-connection-id is not auto-discoverable. With this in place, EventHubsKafkaBinderOAuthIT exercises the real production handler end-to-end (instead of relying on a test-only callback handler), so the integration test now actually validates the OAuth2 path it claims to validate. The test-only TestKafkaOAuth2CallbackHandler and the binder.configuration override in application-eventhubs-kafka-binder-oauth.yml are reverted. --- .../util/TestKafkaOAuth2CallbackHandler.java | 121 ------------------ ...plication-eventhubs-kafka-binder-oauth.yml | 11 -- .../spring-cloud-azure-service/CHANGELOG.md | 2 + ...afkaOAuth2AuthenticateCallbackHandler.java | 58 ++++++++- 4 files changed, 58 insertions(+), 134 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java deleted file mode 100644 index 59faf4b90524..000000000000 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/util/TestKafkaOAuth2CallbackHandler.java +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.cloud.integration.tests.util; - -import com.azure.core.credential.AccessToken; -import com.azure.core.credential.TokenCredential; -import com.azure.core.credential.TokenRequestContext; -import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler; -import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; -import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; - -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.AppConfigurationEntry; -import java.net.URI; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; - -/** - * Test-only {@link AuthenticateCallbackHandler} that authenticates against Azure Event Hubs Kafka endpoint - * using the same {@link TokenCredential} chain as the rest of the integration tests - * ({@link TestCredentialUtils#getIntegrationTestTokenCredential()}). - * - *

This bypasses the production {@code KafkaOAuth2AuthenticateCallbackHandler}, which on a CI agent ends - * up using a {@code DefaultAzureCredential} that resolves to the agent's managed identity (which is not - * granted the {@code Azure Event Hubs Data Owner} role by the test bicep). The federated - * {@code AzurePipelinesCredential} used here corresponds to the {@code testApplicationOid} that the bicep - * actually grants permissions to.

- */ -public class TestKafkaOAuth2CallbackHandler implements AuthenticateCallbackHandler { - - private static final Duration ACCESS_TOKEN_REQUEST_BLOCK_TIME = Duration.ofSeconds(30); - private static final String TOKEN_AUDIENCE_FORMAT = "%s://%s/.default"; - - private TokenCredential credential; - private TokenRequestContext tokenRequestContext; - - @Override - @SuppressWarnings("unchecked") - public void configure(Map configs, String mechanism, List jaasConfigEntries) { - List bootstrapServers = (List) configs.get(BOOTSTRAP_SERVERS_CONFIG); - if (bootstrapServers == null || bootstrapServers.isEmpty()) { - throw new IllegalArgumentException("bootstrap.servers must be configured for Azure Event Hubs."); - } - String bootstrap = bootstrapServers.get(0); - URI uri = URI.create("https://" + bootstrap); - String audience = String.format(TOKEN_AUDIENCE_FORMAT, uri.getScheme(), uri.getHost()); - - this.tokenRequestContext = new TokenRequestContext(); - this.tokenRequestContext.addScopes(audience); - this.credential = TestCredentialUtils.getIntegrationTestTokenCredential(); - } - - @Override - public void handle(Callback[] callbacks) throws UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof OAuthBearerTokenCallback) { - OAuthBearerTokenCallback oauthCallback = (OAuthBearerTokenCallback) callback; - try { - AccessToken accessToken = credential.getToken(tokenRequestContext).block(ACCESS_TOKEN_REQUEST_BLOCK_TIME); - if (accessToken == null) { - oauthCallback.error("invalid_grant", "Failed to acquire token from credential chain.", null); - } else { - oauthCallback.token(new SimpleOAuthBearerToken(accessToken)); - } - } catch (RuntimeException e) { - oauthCallback.error("invalid_grant", e.getMessage(), null); - } - } else { - throw new UnsupportedCallbackException(callback); - } - } - } - - @Override - public void close() { - // NOOP - } - - /** - * Minimal {@link OAuthBearerToken} that exposes only what the Kafka client needs for SASL handshake: - * the bearer token string and its expiration time. - */ - private static final class SimpleOAuthBearerToken implements OAuthBearerToken { - private final AccessToken accessToken; - - SimpleOAuthBearerToken(AccessToken accessToken) { - this.accessToken = accessToken; - } - - @Override - public String value() { - return accessToken.getToken(); - } - - @Override - public Long startTimeMs() { - return null; - } - - @Override - public long lifetimeMs() { - return accessToken.getExpiresAt().toInstant().toEpochMilli(); - } - - @Override - public Set scope() { - return null; - } - - @Override - public String principalName() { - return "azure-event-hubs-kafka-oauth-test"; - } - } -} diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml index a4f0ad628407..b6e400219127 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml @@ -17,17 +17,6 @@ spring: # binder does not create an AdminClient against the Event Hubs endpoint. auto-create-topics: false health-timeout: 0 - # Force the binder's producer/consumer/admin clients to use our test - # callback handler, which authenticates with the federated - # AzurePipelinesCredential (testApplicationOid) the bicep grants - # Event Hubs Data Owner to. Otherwise spring-cloud-azure's default - # KafkaOAuth2AuthenticateCallbackHandler falls back to - # DefaultAzureCredential -> agent managed identity, which has no role. - configuration: - security.protocol: SASL_SSL - sasl.mechanism: OAUTHBEARER - sasl.jaas.config: org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required; - sasl.login.callback.handler.class: com.azure.spring.cloud.integration.tests.util.TestKafkaOAuth2CallbackHandler bindings: consume-in-0: destination: aad-auth diff --git a/sdk/spring/spring-cloud-azure-service/CHANGELOG.md b/sdk/spring/spring-cloud-azure-service/CHANGELOG.md index 90f059c59035..82ccc0f3a35b 100644 --- a/sdk/spring/spring-cloud-azure-service/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-service/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- When `KafkaOAuth2AuthenticateCallbackHandler` cannot resolve a credential from explicit Azure properties, the fallback now chains an `AzurePipelinesCredential` (built from the standard `AZURESUBSCRIPTION_*` and `SYSTEM_ACCESSTOKEN` environment variables, when all are present) before `DefaultAzureCredential`. This enables Spring Cloud Azure Kafka OAuth2 to authenticate to Azure Event Hubs from Azure DevOps pipeline jobs that use a federated workload-identity service connection without any additional configuration. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java index 3f325e33a862..b3725afa7b33 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java @@ -4,6 +4,10 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.util.Configuration; +import com.azure.core.util.logging.ClientLogger; +import com.azure.identity.AzurePipelinesCredentialBuilder; +import com.azure.identity.ChainedTokenCredentialBuilder; import com.azure.spring.cloud.core.credential.AzureCredentialResolver; import com.azure.spring.cloud.core.implementation.credential.resolver.AzureTokenCredentialResolver; import com.azure.spring.cloud.core.implementation.factory.credential.DefaultAzureCredentialBuilderFactory; @@ -111,6 +115,8 @@ public void close() { } private static class InternalCredentialResolver implements AzureCredentialResolver { + private static final ClientLogger LOGGER = new ClientLogger(InternalCredentialResolver.class); + private final AzureCredentialResolver delegated; private final Map configs; private TokenCredential credential; @@ -128,8 +134,22 @@ public TokenCredential resolve(AzureProperties properties) { if (credential == null) { credential = delegated.resolve(properties); if (credential == null) { - // Create DefaultAzureCredential when no credential can be resolved from configs. - credential = new DefaultAzureCredentialBuilderFactory(properties).build().build(); + // No credential could be resolved from explicit configuration. Build a chained + // credential: try AzurePipelinesCredential first when the Azure DevOps federated + // workload-identity environment variables are present (DefaultAzureCredential's + // chain intentionally does not include AzurePipelinesCredential because its + // service-connection-id is not auto-discoverable); fall back to + // DefaultAzureCredential for all other environments. + TokenCredential defaultAzureCredential = new DefaultAzureCredentialBuilderFactory(properties).build().build(); + TokenCredential pipelinesCredential = tryBuildAzurePipelinesCredential(); + if (pipelinesCredential == null) { + credential = defaultAzureCredential; + } else { + credential = new ChainedTokenCredentialBuilder() + .addLast(pipelinesCredential) + .addLast(defaultAzureCredential) + .build(); + } } } } @@ -140,5 +160,39 @@ public TokenCredential resolve(AzureProperties properties) { public boolean isResolvable(AzureProperties properties) { return true; } + + /** + * Attempts to build an {@code AzurePipelinesCredential} from the Azure DevOps federated + * workload-identity environment variables. Returns {@code null} when any of the required + * variables are missing (the typical case outside an Azure DevOps job). + */ + private static TokenCredential tryBuildAzurePipelinesCredential() { + Configuration config = Configuration.getGlobalConfiguration(); + String serviceConnectionId = config.get("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); + String clientId = config.get("AZURESUBSCRIPTION_CLIENT_ID"); + String tenantId = config.get("AZURESUBSCRIPTION_TENANT_ID"); + String systemAccessToken = config.get("SYSTEM_ACCESSTOKEN"); + if (isNullOrEmpty(serviceConnectionId) + || isNullOrEmpty(clientId) + || isNullOrEmpty(tenantId) + || isNullOrEmpty(systemAccessToken)) { + return null; + } + try { + return new AzurePipelinesCredentialBuilder() + .systemAccessToken(systemAccessToken) + .clientId(clientId) + .tenantId(tenantId) + .serviceConnectionId(serviceConnectionId) + .build(); + } catch (RuntimeException e) { + LOGGER.verbose("Failed to build AzurePipelinesCredential, will fall back to DefaultAzureCredential.", e); + return null; + } + } + + private static boolean isNullOrEmpty(String value) { + return value == null || value.isEmpty(); + } } } From 85e17a6d0b02264f0734abbf90a01eb07d436dc1 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 16:41:40 +0800 Subject: [PATCH 07/14] Move Kafka OAuth2 credential fallback entry to consolidated spring CHANGELOG --- sdk/spring/CHANGELOG.md | 8 ++++++++ sdk/spring/spring-cloud-azure-service/CHANGELOG.md | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 41ad0d1cc57d..351b6cba9d9a 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -26,6 +26,14 @@ This section includes changes in `spring-cloud-azure-stream-binder-servicebus` m - Add support for injecting a custom `RetryTemplate` from Spring context for advanced retry scenarios. [#47135](https://github.com/Azure/azure-sdk-for-java/issues/47135). +### Spring Cloud Azure Service + +This section includes changes in `spring-cloud-azure-service` module. + +#### Features Added + +- When `KafkaOAuth2AuthenticateCallbackHandler` cannot resolve a credential from explicit Azure properties, the fallback now chains an `AzurePipelinesCredential` (built from the standard `AZURESUBSCRIPTION_*` and `SYSTEM_ACCESSTOKEN` environment variables, when all are present) before `DefaultAzureCredential`. This enables Spring Cloud Azure Kafka OAuth2 to authenticate to Azure Event Hubs from Azure DevOps pipeline jobs that use a federated workload-identity service connection without any additional configuration. + ## 6.3.0 (2026-04-29) - This release is compatible with Spring Boot 3.5.0-3.5.14. (Note: 3.5.x (x>14) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2025.0.0-2025.0.2. (Note: 2025.0.x (x>2) should be supported, but they aren't tested with this release.) diff --git a/sdk/spring/spring-cloud-azure-service/CHANGELOG.md b/sdk/spring/spring-cloud-azure-service/CHANGELOG.md index 82ccc0f3a35b..90f059c59035 100644 --- a/sdk/spring/spring-cloud-azure-service/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-service/CHANGELOG.md @@ -4,8 +4,6 @@ ### Features Added -- When `KafkaOAuth2AuthenticateCallbackHandler` cannot resolve a credential from explicit Azure properties, the fallback now chains an `AzurePipelinesCredential` (built from the standard `AZURESUBSCRIPTION_*` and `SYSTEM_ACCESSTOKEN` environment variables, when all are present) before `DefaultAzureCredential`. This enables Spring Cloud Azure Kafka OAuth2 to authenticate to Azure Event Hubs from Azure DevOps pipeline jobs that use a federated workload-identity service connection without any additional configuration. - ### Breaking Changes ### Bugs Fixed From 84612ce76a5cb1717bbf7ac4f35b3db1a263fc4f Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 16:43:39 +0800 Subject: [PATCH 08/14] Revert workarounds in application-eventhubs-kafka-binder-oauth.yml The auto-create-topics/health-timeout overrides and the increased timeouts were only needed to dampen the failure-retry loop caused by the underlying OAuth authentication failure. Now that KafkaOAuth2AuthenticateCallbackHandler resolves the right credential on ADO pipelines, these workarounds are no longer necessary. --- .../application-eventhubs-kafka-binder-oauth.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml index b6e400219127..c15630d9540d 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-eventhubs-kafka-binder-oauth.yml @@ -1,9 +1,4 @@ spring: - kafka: - properties: - request.timeout.ms: 60000 - metadata.max.age.ms: 180000 - connections.max.idle.ms: 180000 cloud: function: definition: consume;supply @@ -11,12 +6,6 @@ spring: kafka: binder: brokers: ${AZURE_EVENTHUBS_NAMESPACE}.servicebus.windows.net:9093 - # Azure Event Hubs for Kafka does not support the Kafka AdminClient API - # (topic management must be done via Azure portal/CLI/ARM). Disable - # binder-side topic provisioning and admin-based health checks so the - # binder does not create an AdminClient against the Event Hubs endpoint. - auto-create-topics: false - health-timeout: 0 bindings: consume-in-0: destination: aad-auth From 1b81d2f8e67d983f1ac2618d1cb0e684986aeebc Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 14 May 2026 16:45:19 +0800 Subject: [PATCH 09/14] Remove unnecessary comment in InternalCredentialResolver.resolve --- .../kafka/KafkaOAuth2AuthenticateCallbackHandler.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java index b3725afa7b33..a61e8ffc0d06 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java @@ -134,12 +134,6 @@ public TokenCredential resolve(AzureProperties properties) { if (credential == null) { credential = delegated.resolve(properties); if (credential == null) { - // No credential could be resolved from explicit configuration. Build a chained - // credential: try AzurePipelinesCredential first when the Azure DevOps federated - // workload-identity environment variables are present (DefaultAzureCredential's - // chain intentionally does not include AzurePipelinesCredential because its - // service-connection-id is not auto-discoverable); fall back to - // DefaultAzureCredential for all other environments. TokenCredential defaultAzureCredential = new DefaultAzureCredentialBuilderFactory(properties).build().build(); TokenCredential pipelinesCredential = tryBuildAzurePipelinesCredential(); if (pipelinesCredential == null) { From cb0dca5f4b6f21f01338b5e2ab68db97bf0c1198 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 May 2026 10:03:10 +0800 Subject: [PATCH 10/14] Update changelog for AzurePipelinesCredential support --- sdk/spring/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 351b6cba9d9a..b53a1a66f288 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -32,7 +32,11 @@ This section includes changes in `spring-cloud-azure-service` module. #### Features Added -- When `KafkaOAuth2AuthenticateCallbackHandler` cannot resolve a credential from explicit Azure properties, the fallback now chains an `AzurePipelinesCredential` (built from the standard `AZURESUBSCRIPTION_*` and `SYSTEM_ACCESSTOKEN` environment variables, when all are present) before `DefaultAzureCredential`. This enables Spring Cloud Azure Kafka OAuth2 to authenticate to Azure Event Hubs from Azure DevOps pipeline jobs that use a federated workload-identity service connection without any additional configuration. +- Support `AzurePipelinesCredential` in Azure Event Hubs for Kafka passwordless connection ([#49108](https://github.com/Azure/azure-sdk-for-java/pull/49108)). It only takes effect when all the following 4 environment variables exist at runtime: + - `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` + - `AZURESUBSCRIPTION_CLIENT_ID` + - `AZURESUBSCRIPTION_TENANT_ID` + - `SYSTEM_ACCESSTOKEN` ## 6.3.0 (2026-04-29) - This release is compatible with Spring Boot 3.5.0-3.5.14. (Note: 3.5.x (x>14) should be supported, but they aren't tested with this release.) From 862e2c87fe9d3d261a88e7e5afd487c5871b9ff7 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 May 2026 10:03:11 +0800 Subject: [PATCH 11/14] Apply AzureProperties authority host to AzurePipelinesCredential In sovereign clouds (Azure China, US Gov) DefaultAzureCredentialBuilderFactory configures profile.environment.activeDirectoryEndpoint, but the newly added AzurePipelinesCredentialBuilder was created without it and defaulted to the public-cloud authority. Since ChainedTokenCredential only falls through on CredentialUnavailableException, a wrong-authority failure would block the DefaultAzureCredential fallback. Pass AzureProperties into the builder and apply authorityHost when available. --- ...afkaOAuth2AuthenticateCallbackHandler.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java index a61e8ffc0d06..b15dbea2a1ca 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java @@ -135,7 +135,7 @@ public TokenCredential resolve(AzureProperties properties) { credential = delegated.resolve(properties); if (credential == null) { TokenCredential defaultAzureCredential = new DefaultAzureCredentialBuilderFactory(properties).build().build(); - TokenCredential pipelinesCredential = tryBuildAzurePipelinesCredential(); + TokenCredential pipelinesCredential = tryBuildAzurePipelinesCredential(properties); if (pipelinesCredential == null) { credential = defaultAzureCredential; } else { @@ -158,9 +158,11 @@ public boolean isResolvable(AzureProperties properties) { /** * Attempts to build an {@code AzurePipelinesCredential} from the Azure DevOps federated * workload-identity environment variables. Returns {@code null} when any of the required - * variables are missing (the typical case outside an Azure DevOps job). + * variables are missing (the typical case outside an Azure DevOps job). The authority host + * is taken from the {@link AzureProperties} profile so that the credential targets the + * correct cloud (public, China, US Gov). */ - private static TokenCredential tryBuildAzurePipelinesCredential() { + private static TokenCredential tryBuildAzurePipelinesCredential(AzureProperties properties) { Configuration config = Configuration.getGlobalConfiguration(); String serviceConnectionId = config.get("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); String clientId = config.get("AZURESUBSCRIPTION_CLIENT_ID"); @@ -173,18 +175,29 @@ private static TokenCredential tryBuildAzurePipelinesCredential() { return null; } try { - return new AzurePipelinesCredentialBuilder() + AzurePipelinesCredentialBuilder builder = new AzurePipelinesCredentialBuilder() .systemAccessToken(systemAccessToken) .clientId(clientId) .tenantId(tenantId) - .serviceConnectionId(serviceConnectionId) - .build(); + .serviceConnectionId(serviceConnectionId); + String authorityHost = resolveAuthorityHost(properties); + if (!isNullOrEmpty(authorityHost)) { + builder.authorityHost(authorityHost); + } + return builder.build(); } catch (RuntimeException e) { LOGGER.verbose("Failed to build AzurePipelinesCredential, will fall back to DefaultAzureCredential.", e); return null; } } + private static String resolveAuthorityHost(AzureProperties properties) { + if (properties == null || properties.getProfile() == null || properties.getProfile().getEnvironment() == null) { + return null; + } + return properties.getProfile().getEnvironment().getActiveDirectoryEndpoint(); + } + private static boolean isNullOrEmpty(String value) { return value == null || value.isEmpty(); } From 43e784b4baeca76df839cebee11842c585ed6180 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 May 2026 10:08:32 +0800 Subject: [PATCH 12/14] Add unit test for AzurePipelinesCredential fallback --- ...OAuth2AuthenticateCallbackHandlerTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java index 26e4317da6dc..2c26a18a6e9d 100644 --- a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java +++ b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java @@ -5,6 +5,7 @@ import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.ChainedTokenCredential; import com.azure.identity.DefaultAzureCredential; import com.azure.identity.ManagedIdentityCredential; import com.azure.spring.cloud.core.credential.AzureCredentialResolver; @@ -82,6 +83,32 @@ void testCreateDefaultTokenCredential() { assertTrue(azureTokenCredentialResolver.resolve(properties) instanceof DefaultAzureCredential); } + @Test + void testCreateChainedTokenCredentialWhenAzurePipelinesEnvVarsPresent() { + System.setProperty("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID", "test-service-connection-id"); + System.setProperty("AZURESUBSCRIPTION_CLIENT_ID", "00000000-0000-0000-0000-000000000000"); + System.setProperty("AZURESUBSCRIPTION_TENANT_ID", "11111111-1111-1111-1111-111111111111"); + System.setProperty("SYSTEM_ACCESSTOKEN", "test-system-access-token"); + try { + Map configs = new HashMap<>(); + configs.put(BOOTSTRAP_SERVERS_CONFIG, KAFKA_BOOTSTRAP_SERVER); + KafkaOAuth2AuthenticateCallbackHandler handler = new KafkaOAuth2AuthenticateCallbackHandler(); + handler.configure(configs, null, null); + + AzurePasswordlessProperties properties = (AzurePasswordlessProperties) ReflectionTestUtils + .getField(handler, AZURE_THIRD_PARTY_SERVICE_PROPERTIES_FIELD_NAME); + @SuppressWarnings("unchecked") AzureCredentialResolver azureTokenCredentialResolver = + (AzureCredentialResolver) ReflectionTestUtils.getField(handler, TOKEN_CREDENTIAL_RESOLVER_FIELD_NAME); + assertNotNull(azureTokenCredentialResolver); + assertTrue(azureTokenCredentialResolver.resolve(properties) instanceof ChainedTokenCredential); + } finally { + System.clearProperty("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); + System.clearProperty("AZURESUBSCRIPTION_CLIENT_ID"); + System.clearProperty("AZURESUBSCRIPTION_TENANT_ID"); + System.clearProperty("SYSTEM_ACCESSTOKEN"); + } + } + @Test void testCreateTokenCredentialByResolver() { Map configs = new HashMap<>(); From efe72ab27611083802db4743267568dc55a1eefd Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 May 2026 10:36:45 +0800 Subject: [PATCH 13/14] Set SYSTEM_OIDCREQUESTURI and isolate global Configuration in chained credential test --- ...OAuth2AuthenticateCallbackHandlerTest.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java index 2c26a18a6e9d..12e23f938aa2 100644 --- a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java +++ b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java @@ -5,6 +5,7 @@ import com.azure.core.credential.AccessToken; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.util.Configuration; import com.azure.identity.ChainedTokenCredential; import com.azure.identity.DefaultAzureCredential; import com.azure.identity.ManagedIdentityCredential; @@ -15,6 +16,8 @@ import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule; import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; import reactor.core.publisher.Mono; @@ -84,11 +87,19 @@ void testCreateDefaultTokenCredential() { } @Test + @ResourceLock(value = Resources.GLOBAL, mode = org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE) + @SuppressWarnings("deprecation") void testCreateChainedTokenCredentialWhenAzurePipelinesEnvVarsPresent() { - System.setProperty("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID", "test-service-connection-id"); - System.setProperty("AZURESUBSCRIPTION_CLIENT_ID", "00000000-0000-0000-0000-000000000000"); - System.setProperty("AZURESUBSCRIPTION_TENANT_ID", "11111111-1111-1111-1111-111111111111"); - System.setProperty("SYSTEM_ACCESSTOKEN", "test-system-access-token"); + // Use Configuration.put rather than System.setProperty because Configuration internally + // caches system-property/env-var lookups; only the explicitConfigurations map (populated + // by put) is cleared by remove, giving us proper per-test isolation. + Configuration globalConfiguration = Configuration.getGlobalConfiguration(); + globalConfiguration.put("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID", "test-service-connection-id"); + globalConfiguration.put("AZURESUBSCRIPTION_CLIENT_ID", "00000000-0000-0000-0000-000000000000"); + globalConfiguration.put("AZURESUBSCRIPTION_TENANT_ID", "11111111-1111-1111-1111-111111111111"); + globalConfiguration.put("SYSTEM_ACCESSTOKEN", "test-system-access-token"); + // AzurePipelinesCredentialBuilder.build() also validates SYSTEM_OIDCREQUESTURI. + globalConfiguration.put("SYSTEM_OIDCREQUESTURI", "https://example.test/oidc"); try { Map configs = new HashMap<>(); configs.put(BOOTSTRAP_SERVERS_CONFIG, KAFKA_BOOTSTRAP_SERVER); @@ -102,10 +113,11 @@ void testCreateChainedTokenCredentialWhenAzurePipelinesEnvVarsPresent() { assertNotNull(azureTokenCredentialResolver); assertTrue(azureTokenCredentialResolver.resolve(properties) instanceof ChainedTokenCredential); } finally { - System.clearProperty("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); - System.clearProperty("AZURESUBSCRIPTION_CLIENT_ID"); - System.clearProperty("AZURESUBSCRIPTION_TENANT_ID"); - System.clearProperty("SYSTEM_ACCESSTOKEN"); + globalConfiguration.remove("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"); + globalConfiguration.remove("AZURESUBSCRIPTION_CLIENT_ID"); + globalConfiguration.remove("AZURESUBSCRIPTION_TENANT_ID"); + globalConfiguration.remove("SYSTEM_ACCESSTOKEN"); + globalConfiguration.remove("SYSTEM_OIDCREQUESTURI"); } } From 2efe847a76aa98e09a44f4f215720d32b2f356b9 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 15 May 2026 10:41:26 +0800 Subject: [PATCH 14/14] Clarify tryBuildAzurePipelinesCredential Javadoc about build() failures --- .../kafka/KafkaOAuth2AuthenticateCallbackHandler.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java index b15dbea2a1ca..72db1274eb32 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandler.java @@ -157,10 +157,13 @@ public boolean isResolvable(AzureProperties properties) { /** * Attempts to build an {@code AzurePipelinesCredential} from the Azure DevOps federated - * workload-identity environment variables. Returns {@code null} when any of the required - * variables are missing (the typical case outside an Azure DevOps job). The authority host - * is taken from the {@link AzureProperties} profile so that the credential targets the - * correct cloud (public, China, US Gov). + * workload-identity environment variables. Returns {@code null} when any of the four + * caller-provided variables ({@code AZURESUBSCRIPTION_SERVICE_CONNECTION_ID}, + * {@code AZURESUBSCRIPTION_CLIENT_ID}, {@code AZURESUBSCRIPTION_TENANT_ID}, + * {@code SYSTEM_ACCESSTOKEN}) are missing, or when {@code AzurePipelinesCredentialBuilder#build()} + * itself fails (e.g. {@code SYSTEM_OIDCREQUESTURI} is unavailable outside an Azure DevOps + * job). The authority host is taken from the {@link AzureProperties} profile so that the + * credential targets the correct cloud (public, China, US Gov). */ private static TokenCredential tryBuildAzurePipelinesCredential(AzureProperties properties) { Configuration config = Configuration.getGlobalConfiguration();