diff --git a/sdk/appconfiguration/Azure.Data.AppConfiguration/src/AudienceErrorHandlingPolicy.cs b/sdk/appconfiguration/Azure.Data.AppConfiguration/src/AudienceErrorHandlingPolicy.cs new file mode 100644 index 000000000000..f43e29612763 --- /dev/null +++ b/sdk/appconfiguration/Azure.Data.AppConfiguration/src/AudienceErrorHandlingPolicy.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Data.AppConfiguration +{ + /// + /// Pipeline policy that provides more helpful errors when Entra ID audience misconfiguration is detected. + /// + internal class AudienceErrorHandlingPolicy : HttpPipelinePolicy + { + private readonly bool _isAudienceConfigured; + private const string AadAudienceErrorCode = "AADSTS500011"; + 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."; + 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."; + + public AudienceErrorHandlingPolicy(bool isAudienceConfigured) + { + _isAudienceConfigured = isAudienceConfigured; + } + + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + try + { + ProcessNext(message, pipeline); + } + catch (Exception ex) when (ex.Message.Contains(AadAudienceErrorCode)) + { + string errorMessage = _isAudienceConfigured ? WrongAudienceErrorMessage : NoAudienceErrorMessage; + throw new RequestFailedException(errorMessage, ex); + } + } + + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + try + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + catch (Exception ex) when (ex.Message.Contains(AadAudienceErrorCode)) + { + string errorMessage = _isAudienceConfigured ? WrongAudienceErrorMessage : NoAudienceErrorMessage; + throw new RequestFailedException(errorMessage, ex); + } + } + } +} diff --git a/sdk/appconfiguration/Azure.Data.AppConfiguration/src/ConfigurationClient.cs b/sdk/appconfiguration/Azure.Data.AppConfiguration/src/ConfigurationClient.cs index 7b63921eb5b1..be24b889c12a 100644 --- a/sdk/appconfiguration/Azure.Data.AppConfiguration/src/ConfigurationClient.cs +++ b/sdk/appconfiguration/Azure.Data.AppConfiguration/src/ConfigurationClient.cs @@ -200,7 +200,7 @@ private static HttpPipeline CreatePipeline(ConfigurationClientOptions options, H { return HttpPipelineBuilder.Build(options, new HttpPipelinePolicy[] { new CustomHeadersPolicy(), new QueryParamPolicy() }, - new HttpPipelinePolicy[] { authenticationPolicy, syncTokenPolicy }, + new HttpPipelinePolicy[] { new AudienceErrorHandlingPolicy(options.Audience != null), authenticationPolicy, syncTokenPolicy }, new ResponseClassifier()); } diff --git a/sdk/appconfiguration/Azure.Data.AppConfiguration/tests/AudienceErrorHandlingPolicyTests.cs b/sdk/appconfiguration/Azure.Data.AppConfiguration/tests/AudienceErrorHandlingPolicyTests.cs new file mode 100644 index 000000000000..978c3ff82239 --- /dev/null +++ b/sdk/appconfiguration/Azure.Data.AppConfiguration/tests/AudienceErrorHandlingPolicyTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Data.AppConfiguration.Tests +{ + [TestFixture(true)] + [TestFixture(false)] + public class AudienceErrorHandlingPolicyTests : SyncAsyncPolicyTestBase + { + private const string AadAudienceErrorCode = "AADSTS500011"; // Must match code in AudienceErrorHandlingPolicy + + public AudienceErrorHandlingPolicyTests(bool isAsync) : base(isAsync) + { + } + + private static string ExpectedErrorMessage(bool isAudienceConfigured) + { + string leading = "Unable to authenticate to Azure App Configuration."; + string detail = isAudienceConfigured + ? " An incorrect token audience was provided." + : " No authentication token audience was provided."; + 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."; + return leading + detail + guidance; + } + + private class ThrowingPolicy : HttpPipelinePolicy + { + private readonly string _message; + public ThrowingPolicy(string message) => _message = message; + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + throw new Exception(_message); + } + public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + throw new Exception(_message); + } + } + + [Test] + public void WrapsError_NoAudienceConfigured() => AssertWrapsError(isAudienceConfigured: false); + + [Test] + public void WrapsError_WrongAudienceConfigured() => AssertWrapsError(isAudienceConfigured: true); + + [Test] + public void NonAudienceError_PassesThrough() + { + var transport = new MockTransport(new MockResponse(200)); + var pipeline = new HttpPipeline( + transport, + new HttpPipelinePolicy[] + { + new AudienceErrorHandlingPolicy(isAudienceConfigured: true), // value irrelevant since code won't match + new ThrowingPolicy("Simulated failure WITHOUT code") + }, + responseClassifier: null); + + var requestUri = new Uri("http://example.com"); + + Exception ex = Assert.ThrowsAsync(async () => + { + if (IsAsync) + { + var message = pipeline.CreateMessage(); + message.Request.Method = RequestMethod.Get; + message.Request.Uri.Reset(requestUri); + await pipeline.SendAsync(message, CancellationToken.None); + } + else + { + var message = pipeline.CreateMessage(); + message.Request.Method = RequestMethod.Get; + message.Request.Uri.Reset(requestUri); + pipeline.Send(message, CancellationToken.None); + } + }); + + Assert.IsNotInstanceOf(ex); // Should not be wrapped + Assert.AreEqual("Simulated failure WITHOUT code", ex.Message); + } + + private void AssertWrapsError(bool isAudienceConfigured) + { + var transport = new MockTransport(new MockResponse(200)); // Transport won't be reached because throwing policy throws first. + var pipeline = new HttpPipeline( + transport, + new HttpPipelinePolicy[] + { + new AudienceErrorHandlingPolicy(isAudienceConfigured), + new ThrowingPolicy($"Simulated authentication failure {AadAudienceErrorCode}: Resource principal not found") + }, + responseClassifier: null); + + var requestUri = new Uri("http://example.com"); + RequestFailedException ex = Assert.ThrowsAsync(async () => + { + if (IsAsync) + { + var message = pipeline.CreateMessage(); + message.Request.Method = RequestMethod.Get; + message.Request.Uri.Reset(requestUri); + await pipeline.SendAsync(message, CancellationToken.None); + } + else + { + var message = pipeline.CreateMessage(); + message.Request.Method = RequestMethod.Get; + message.Request.Uri.Reset(requestUri); + pipeline.Send(message, CancellationToken.None); + } + }); + + Assert.NotNull(ex); + StringAssert.Contains(ExpectedErrorMessage(isAudienceConfigured), ex.Message); + Assert.NotNull(ex.InnerException); + StringAssert.Contains(AadAudienceErrorCode, ex.InnerException.Message); + } + } +}