Skip to content

Commit 520513d

Browse files
mrm9084Copilot
andauthored
App Config - Audience policy (Azure#47224)
* AudiencePolicy * Updated usage + tests * fixing code owners * Update sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java Co-authored-by: Copilot <[email protected]> * linting changes * Revert "linting changes" This reverts commit c7f2d12. * text update + suppression * Trying to fix tests * Update AudiencePolicy.java --------- Co-authored-by: Copilot <[email protected]>
1 parent 65fb85e commit 520513d

File tree

6 files changed

+262
-12
lines changed

6 files changed

+262
-12
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
# ServiceOwners: @miaojiang
108108

109109
# PRLabel: %App Configuration
110-
/sdk/appconfiguration/ @alzimmermsft @Azure/azure-java-sdk
110+
/sdk/appconfiguration/ @mrm9084 @rossgrambo @avanigupta @alzimmermsft @Azure/azure-java-sdk
111111

112112
# ServiceLabel: %App Configuration
113113
# AzureSdkOwners: @mrm9084

sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## 1.9.0-beta.1 (Unreleased)
44

55
### Features Added
6+
67
- Added a pipeline policy to handle query parameters to make sure the keys are always in lower case and in alphabetical order.
8+
- Added audience policy to provide more meaningful error messages for Azure Active Directory authentication failures. The policy detects AAD audience-related errors and provides clear guidance on audience configuration issues.
79

810
### Breaking Changes
911

sdk/appconfiguration/azure-data-appconfiguration/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
<suppressions>
66
<suppress files="com.azure.data.appconfiguration.implementation.ConfigurationSettingDeserializationHelper.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
7+
<suppress files="com.azure.data.appconfiguration.implementation.AudiencePolicy.java" checks="io.clientcore.linting.extensions.checkstyle.checks.HttpPipelinePolicyCheck" />
78
<suppress files="com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy.java" checks="io.clientcore.linting.extensions.checkstyle.checks.HttpPipelinePolicyCheck" />
89
<suppress files="com.azure.data.appconfiguration.implementation.SyncToken.java" checks="io.clientcore.linting.extensions.checkstyle.checks.UseCaughtExceptionCauseCheck" />
910
</suppressions>

sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33

44
package com.azure.data.appconfiguration;
55

6+
import java.net.MalformedURLException;
7+
import java.net.URL;
8+
import java.time.temporal.ChronoUnit;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Objects;
13+
614
import com.azure.core.annotation.ServiceClientBuilder;
715
import com.azure.core.client.traits.ConfigurationTrait;
816
import com.azure.core.client.traits.ConnectionStringTrait;
@@ -33,30 +41,22 @@
3341
import com.azure.core.util.ClientOptions;
3442
import com.azure.core.util.Configuration;
3543
import com.azure.core.util.CoreUtils;
44+
import static com.azure.core.util.CoreUtils.getApplicationId;
3645
import com.azure.core.util.HttpClientOptions;
3746
import com.azure.core.util.TracingOptions;
3847
import com.azure.core.util.builder.ClientBuilderUtil;
3948
import com.azure.core.util.logging.ClientLogger;
4049
import com.azure.core.util.tracing.Tracer;
4150
import com.azure.core.util.tracing.TracerProvider;
51+
import com.azure.data.appconfiguration.implementation.AudiencePolicy;
4252
import com.azure.data.appconfiguration.implementation.AzureAppConfigurationImpl;
53+
import static com.azure.data.appconfiguration.implementation.ClientConstants.APP_CONFIG_TRACING_NAMESPACE_VALUE;
4354
import com.azure.data.appconfiguration.implementation.ConfigurationClientCredentials;
4455
import com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy;
4556
import com.azure.data.appconfiguration.implementation.QueryParamPolicy;
4657
import com.azure.data.appconfiguration.implementation.SyncTokenPolicy;
4758
import com.azure.data.appconfiguration.models.ConfigurationAudience;
4859

49-
import java.net.MalformedURLException;
50-
import java.net.URL;
51-
import java.time.temporal.ChronoUnit;
52-
import java.util.ArrayList;
53-
import java.util.List;
54-
import java.util.Map;
55-
import java.util.Objects;
56-
57-
import static com.azure.core.util.CoreUtils.getApplicationId;
58-
import static com.azure.data.appconfiguration.implementation.ClientConstants.APP_CONFIG_TRACING_NAMESPACE_VALUE;
59-
6060
/**
6161
* This class provides a fluent builder API to help aid the configuration and instantiation of
6262
* {@link ConfigurationClient ConfigurationClients} and {@link ConfigurationAsyncClient ConfigurationAsyncClients}, call
@@ -289,6 +289,9 @@ private HttpPipeline createDefaultHttpPipeline(SyncTokenPolicy syncTokenPolicy,
289289
policies.add(syncTokenPolicy);
290290
policies.addAll(perRetryPolicies);
291291

292+
// Add policy to provide better error messages for AAD audience authentication failures
293+
policies.add(new AudiencePolicy(audience));
294+
292295
List<HttpHeader> httpHeaderList = new ArrayList<>();
293296
localClientOptions.getHeaders()
294297
.forEach(header -> httpHeaderList.add(new HttpHeader(header.getName(), header.getValue())));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.data.appconfiguration.implementation;
4+
5+
import com.azure.core.exception.HttpResponseException;
6+
import com.azure.core.http.HttpPipelineCallContext;
7+
import com.azure.core.http.HttpPipelineNextPolicy;
8+
import com.azure.core.http.HttpPipelineNextSyncPolicy;
9+
import com.azure.core.http.HttpResponse;
10+
import com.azure.core.http.policy.HttpPipelinePolicy;
11+
import com.azure.core.util.logging.ClientLogger;
12+
import com.azure.data.appconfiguration.models.ConfigurationAudience;
13+
14+
import reactor.core.publisher.Mono;
15+
16+
/**
17+
* HTTP pipeline policy that handles Azure Active Directory audience-related authentication errors.
18+
* This policy intercepts HTTP responses and provides more meaningful error messages when
19+
* audience configuration issues occur during authentication.
20+
*/
21+
public class AudiencePolicy implements HttpPipelinePolicy {
22+
23+
private static final ClientLogger LOGGER = new ClientLogger(AudiencePolicy.class);
24+
25+
private static final String NO_AUDIENCE_ERROR_MESSAGE
26+
= "Unable to authenticate to Azure App Configuration. No authentication token audience was provided. "
27+
+ "Please set an Audience in your ConfigurationClientBuilder for the target cloud. "
28+
+ "For details on how to configure the authentication token audience visit "
29+
+ "https://aka.ms/appconfig/client-token-audience.";
30+
31+
private static final String INCORRECT_AUDIENCE_ERROR_MESSAGE
32+
= "Unable to authenticate to Azure App Configuration. An incorrect token audience was provided. "
33+
+ "Please set the Audience in your ConfigurationClientBuilder to the appropriate audience for this cloud. "
34+
+ "For details on how to configure the authentication token audience visit "
35+
+ "https://aka.ms/appconfig/client-token-audience.";
36+
37+
private static final String AAD_AUDIENCE_ERROR_CODE = "AADSTS500011";
38+
39+
private final ConfigurationAudience audience;
40+
41+
/**
42+
* Creates a new instance of AudiencePolicy.
43+
*
44+
* @param audience The configuration audience to use for validation. May be null.
45+
*/
46+
public AudiencePolicy(ConfigurationAudience audience) {
47+
this.audience = audience;
48+
}
49+
50+
@Override
51+
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
52+
return next.process().onErrorMap(HttpResponseException.class, this::handleAudienceException);
53+
}
54+
55+
@Override
56+
public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) {
57+
try {
58+
return next.processSync();
59+
} catch (HttpResponseException ex) {
60+
throw LOGGER.logExceptionAsError(handleAudienceException(ex));
61+
}
62+
}
63+
64+
/**
65+
* Handles audience-related authentication exceptions by providing more meaningful error messages.
66+
*
67+
* @param ex The original HttpResponseException
68+
* @return A new HttpResponseException with improved error message if audience-related, otherwise the original exception
69+
*/
70+
private HttpResponseException handleAudienceException(HttpResponseException ex) {
71+
if (ex.getMessage() != null && ex.getMessage().contains(AAD_AUDIENCE_ERROR_CODE)) {
72+
String message = audience == null ? NO_AUDIENCE_ERROR_MESSAGE : INCORRECT_AUDIENCE_ERROR_MESSAGE;
73+
return new HttpResponseException(message, ex.getResponse(), ex);
74+
}
75+
return ex;
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.data.appconfiguration.implementation;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import com.azure.core.exception.HttpResponseException;
12+
import com.azure.core.http.HttpMethod;
13+
import com.azure.core.http.HttpPipeline;
14+
import com.azure.core.http.HttpPipelineBuilder;
15+
import com.azure.core.http.HttpRequest;
16+
import com.azure.core.http.HttpResponse;
17+
import com.azure.core.http.policy.HttpPipelinePolicy;
18+
import com.azure.core.test.SyncAsyncExtension;
19+
import com.azure.core.test.annotation.SyncAsyncTest;
20+
import com.azure.core.test.http.MockHttpResponse;
21+
import com.azure.core.test.http.NoOpHttpClient;
22+
import com.azure.core.util.Context;
23+
import com.azure.data.appconfiguration.models.ConfigurationAudience;
24+
25+
import reactor.core.publisher.Mono;
26+
import reactor.test.StepVerifier;
27+
28+
/**
29+
* Unit tests for AudiencePolicy
30+
*/
31+
public class AudiencePolicyTest {
32+
private static final String LOCAL_HOST = "http://localhost";
33+
private static final String AAD_AUDIENCE_ERROR_CODE = "AADSTS500011";
34+
private static final String NO_AUDIENCE_ERROR_MESSAGE
35+
= "Unable to authenticate to Azure App Configuration. No authentication token audience was provided. "
36+
+ "Please set an Audience in your ConfigurationClientBuilder for the target cloud. "
37+
+ "For details on how to configure the authentication token audience visit "
38+
+ "https://aka.ms/appconfig/client-token-audience.";
39+
40+
private static final String INCORRECT_AUDIENCE_ERROR_MESSAGE
41+
= "Unable to authenticate to Azure App Configuration. An incorrect token audience was provided. "
42+
+ "Please set the Audience in your ConfigurationClientBuilder to the appropriate audience for this cloud. "
43+
+ "For details on how to configure the authentication token audience visit "
44+
+ "https://aka.ms/appconfig/client-token-audience.";
45+
46+
@SyncAsyncTest
47+
public void processWithoutException() {
48+
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);
49+
50+
HttpPipelinePolicy testPolicy = (context, next) -> {
51+
return next.process();
52+
};
53+
54+
final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient() {
55+
@Override
56+
public Mono<HttpResponse> send(HttpRequest request) {
57+
return Mono.just(new MockHttpResponse(request, 200));
58+
}
59+
}).policies(audiencePolicy, testPolicy).build();
60+
61+
SyncAsyncExtension.execute(() -> sendRequestSync(pipeline), () -> sendRequest(pipeline));
62+
}
63+
64+
@Test
65+
public void processWithNonAudienceException() {
66+
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);
67+
68+
HttpPipelinePolicy exceptionPolicy = (context, next) -> {
69+
HttpResponseException ex
70+
= new HttpResponseException("Some other error", new MockHttpResponse(context.getHttpRequest(), 401));
71+
return Mono.error(ex);
72+
};
73+
74+
final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
75+
.policies(audiencePolicy, exceptionPolicy)
76+
.build();
77+
78+
StepVerifier.create(sendRequest(pipeline))
79+
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
80+
&& throwable.getMessage().equals("Some other error"))
81+
.verify();
82+
83+
// Test sync version
84+
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
85+
assertEquals("Some other error", thrown.getMessage());
86+
}
87+
88+
@Test
89+
public void processWithAudienceExceptionAndNullAudience() {
90+
AudiencePolicy audiencePolicy = new AudiencePolicy(null);
91+
92+
HttpPipelinePolicy exceptionPolicy = (context, next) -> {
93+
HttpResponseException ex = new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
94+
new MockHttpResponse(context.getHttpRequest(), 401));
95+
return Mono.error(ex);
96+
};
97+
98+
final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
99+
.policies(audiencePolicy, exceptionPolicy)
100+
.build();
101+
102+
StepVerifier.create(sendRequest(pipeline))
103+
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
104+
&& throwable.getMessage().equals(NO_AUDIENCE_ERROR_MESSAGE))
105+
.verify();
106+
107+
// Test sync version
108+
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
109+
assertEquals(NO_AUDIENCE_ERROR_MESSAGE, thrown.getMessage());
110+
}
111+
112+
@Test
113+
public void processWithAudienceExceptionAndConfiguredAudience() {
114+
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);
115+
116+
HttpPipelinePolicy exceptionPolicy = (context, next) -> {
117+
HttpResponseException ex = new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
118+
new MockHttpResponse(context.getHttpRequest(), 401));
119+
return Mono.error(ex);
120+
};
121+
122+
final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
123+
.policies(audiencePolicy, exceptionPolicy)
124+
.build();
125+
126+
StepVerifier.create(sendRequest(pipeline))
127+
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
128+
&& throwable.getMessage().equals(INCORRECT_AUDIENCE_ERROR_MESSAGE))
129+
.verify();
130+
131+
// Test sync version
132+
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
133+
assertEquals(INCORRECT_AUDIENCE_ERROR_MESSAGE, thrown.getMessage());
134+
}
135+
136+
@Test
137+
public void processWithNullMessageException() {
138+
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);
139+
140+
HttpPipelinePolicy exceptionPolicy = (context, next) -> {
141+
HttpResponseException ex
142+
= new HttpResponseException(null, new MockHttpResponse(context.getHttpRequest(), 401));
143+
return Mono.error(ex);
144+
};
145+
146+
final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
147+
.policies(audiencePolicy, exceptionPolicy)
148+
.build();
149+
150+
StepVerifier.create(sendRequest(pipeline))
151+
.expectErrorMatches(
152+
throwable -> throwable instanceof HttpResponseException && throwable.getMessage() == null)
153+
.verify();
154+
155+
// Test sync version
156+
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
157+
assertEquals(null, thrown.getMessage(), "Should return original exception when message is null");
158+
}
159+
160+
private Mono<HttpResponse> sendRequest(HttpPipeline pipeline) {
161+
return pipeline.send(new HttpRequest(HttpMethod.GET, LOCAL_HOST));
162+
}
163+
164+
private HttpResponse sendRequestSync(HttpPipeline pipeline) {
165+
return pipeline.sendSync(new HttpRequest(HttpMethod.GET, LOCAL_HOST), Context.NONE);
166+
}
167+
}

0 commit comments

Comments
 (0)