Skip to content

Commit f0b51fb

Browse files
[App Configuration] - Add audience error handling policy (#53834)
* add Audience error handling policy * update * not introduce Azure.Identity * update * update * update * add test * Revert "add test" This reverts commit 414f6c4. * add unit test * update * update
1 parent 5ba077b commit f0b51fb

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Azure.Core;
7+
using Azure.Core.Pipeline;
8+
9+
namespace Azure.Data.AppConfiguration
10+
{
11+
/// <summary>
12+
/// Pipeline policy that provides more helpful errors when Entra ID audience misconfiguration is detected.
13+
/// </summary>
14+
internal class AudienceErrorHandlingPolicy : HttpPipelinePolicy
15+
{
16+
private readonly bool _isAudienceConfigured;
17+
private const string AadAudienceErrorCode = "AADSTS500011";
18+
private const string NoAudienceErrorMessage = $"Unable to authenticate to Azure App Configuration. No authentication token audience was provided. Please set {nameof(ConfigurationClientOptions)}.{nameof(ConfigurationClientOptions.Audience)} to the appropriate audience for the target cloud. For details on how to configure the authentication token audience visit https://aka.ms/appconfig/client-token-audience.";
19+
private const string WrongAudienceErrorMessage = $"Unable to authenticate to Azure App Configuration. An incorrect token audience was provided. Please set {nameof(ConfigurationClientOptions)}.{nameof(ConfigurationClientOptions.Audience)} to the appropriate audience for the target cloud. For details on how to configure the authentication token audience visit https://aka.ms/appconfig/client-token-audience.";
20+
21+
public AudienceErrorHandlingPolicy(bool isAudienceConfigured)
22+
{
23+
_isAudienceConfigured = isAudienceConfigured;
24+
}
25+
26+
public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
27+
{
28+
try
29+
{
30+
ProcessNext(message, pipeline);
31+
}
32+
catch (Exception ex) when (ex.Message.Contains(AadAudienceErrorCode))
33+
{
34+
string errorMessage = _isAudienceConfigured ? WrongAudienceErrorMessage : NoAudienceErrorMessage;
35+
throw new RequestFailedException(errorMessage, ex);
36+
}
37+
}
38+
39+
public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
40+
{
41+
try
42+
{
43+
await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
44+
}
45+
catch (Exception ex) when (ex.Message.Contains(AadAudienceErrorCode))
46+
{
47+
string errorMessage = _isAudienceConfigured ? WrongAudienceErrorMessage : NoAudienceErrorMessage;
48+
throw new RequestFailedException(errorMessage, ex);
49+
}
50+
}
51+
}
52+
}

sdk/appconfiguration/Azure.Data.AppConfiguration/src/ConfigurationClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ private static HttpPipeline CreatePipeline(ConfigurationClientOptions options, H
200200
{
201201
return HttpPipelineBuilder.Build(options,
202202
new HttpPipelinePolicy[] { new CustomHeadersPolicy(), new QueryParamPolicy() },
203-
new HttpPipelinePolicy[] { authenticationPolicy, syncTokenPolicy },
203+
new HttpPipelinePolicy[] { new AudienceErrorHandlingPolicy(options.Audience != null), authenticationPolicy, syncTokenPolicy },
204204
new ResponseClassifier());
205205
}
206206

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Azure.Core;
8+
using Azure.Core.Pipeline;
9+
using Azure.Core.TestFramework;
10+
using NUnit.Framework;
11+
12+
namespace Azure.Data.AppConfiguration.Tests
13+
{
14+
[TestFixture(true)]
15+
[TestFixture(false)]
16+
public class AudienceErrorHandlingPolicyTests : SyncAsyncPolicyTestBase
17+
{
18+
private const string AadAudienceErrorCode = "AADSTS500011"; // Must match code in AudienceErrorHandlingPolicy
19+
20+
public AudienceErrorHandlingPolicyTests(bool isAsync) : base(isAsync)
21+
{
22+
}
23+
24+
private static string ExpectedErrorMessage(bool isAudienceConfigured)
25+
{
26+
string leading = "Unable to authenticate to Azure App Configuration.";
27+
string detail = isAudienceConfigured
28+
? " An incorrect token audience was provided."
29+
: " No authentication token audience was provided.";
30+
string guidance = $" Please set {nameof(ConfigurationClientOptions)}.{nameof(ConfigurationClientOptions.Audience)} to the appropriate audience for the target cloud. For details on how to configure the authentication token audience visit https://aka.ms/appconfig/client-token-audience.";
31+
return leading + detail + guidance;
32+
}
33+
34+
private class ThrowingPolicy : HttpPipelinePolicy
35+
{
36+
private readonly string _message;
37+
public ThrowingPolicy(string message) => _message = message;
38+
public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
39+
{
40+
throw new Exception(_message);
41+
}
42+
public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
43+
{
44+
throw new Exception(_message);
45+
}
46+
}
47+
48+
[Test]
49+
public void WrapsError_NoAudienceConfigured() => AssertWrapsError(isAudienceConfigured: false);
50+
51+
[Test]
52+
public void WrapsError_WrongAudienceConfigured() => AssertWrapsError(isAudienceConfigured: true);
53+
54+
[Test]
55+
public void NonAudienceError_PassesThrough()
56+
{
57+
var transport = new MockTransport(new MockResponse(200));
58+
var pipeline = new HttpPipeline(
59+
transport,
60+
new HttpPipelinePolicy[]
61+
{
62+
new AudienceErrorHandlingPolicy(isAudienceConfigured: true), // value irrelevant since code won't match
63+
new ThrowingPolicy("Simulated failure WITHOUT code")
64+
},
65+
responseClassifier: null);
66+
67+
var requestUri = new Uri("http://example.com");
68+
69+
Exception ex = Assert.ThrowsAsync<Exception>(async () =>
70+
{
71+
if (IsAsync)
72+
{
73+
var message = pipeline.CreateMessage();
74+
message.Request.Method = RequestMethod.Get;
75+
message.Request.Uri.Reset(requestUri);
76+
await pipeline.SendAsync(message, CancellationToken.None);
77+
}
78+
else
79+
{
80+
var message = pipeline.CreateMessage();
81+
message.Request.Method = RequestMethod.Get;
82+
message.Request.Uri.Reset(requestUri);
83+
pipeline.Send(message, CancellationToken.None);
84+
}
85+
});
86+
87+
Assert.IsNotInstanceOf<RequestFailedException>(ex); // Should not be wrapped
88+
Assert.AreEqual("Simulated failure WITHOUT code", ex.Message);
89+
}
90+
91+
private void AssertWrapsError(bool isAudienceConfigured)
92+
{
93+
var transport = new MockTransport(new MockResponse(200)); // Transport won't be reached because throwing policy throws first.
94+
var pipeline = new HttpPipeline(
95+
transport,
96+
new HttpPipelinePolicy[]
97+
{
98+
new AudienceErrorHandlingPolicy(isAudienceConfigured),
99+
new ThrowingPolicy($"Simulated authentication failure {AadAudienceErrorCode}: Resource principal not found")
100+
},
101+
responseClassifier: null);
102+
103+
var requestUri = new Uri("http://example.com");
104+
RequestFailedException ex = Assert.ThrowsAsync<RequestFailedException>(async () =>
105+
{
106+
if (IsAsync)
107+
{
108+
var message = pipeline.CreateMessage();
109+
message.Request.Method = RequestMethod.Get;
110+
message.Request.Uri.Reset(requestUri);
111+
await pipeline.SendAsync(message, CancellationToken.None);
112+
}
113+
else
114+
{
115+
var message = pipeline.CreateMessage();
116+
message.Request.Method = RequestMethod.Get;
117+
message.Request.Uri.Reset(requestUri);
118+
pipeline.Send(message, CancellationToken.None);
119+
}
120+
});
121+
122+
Assert.NotNull(ex);
123+
StringAssert.Contains(ExpectedErrorMessage(isAudienceConfigured), ex.Message);
124+
Assert.NotNull(ex.InnerException);
125+
StringAssert.Contains(AadAudienceErrorCode, ex.InnerException.Message);
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)