Skip to content

Commit 8ae427d

Browse files
[App Configuration] - Add audience error handling policy (#36573)
### Packages impacted by this PR @azure/app-configuration ### Issues associated with this PR ### Describe the problem that is addressed by this PR Users running in clouds other than the public cloud must correctly configure ConfigurationClientOptions.Audience when using the ConfigurationClient from the Azure SDK. There are two main ways users can misconfigure They do not specify audience when they are running in non-public cloud They specify audience, and the audience they specify is the wrong one for the cloud is using In both cases we will get an error when trying to get an Entra ID token that looks like: "Microsoft.Identity.Client.MsalServiceException: AADSTS500011: The resource principal named https://appconfig.azure.com/ was not found in the tenant named msazurecloud. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant." We should handle this error and surface up an improved error message Audience not provided If we get this error and audience is not provided we should surface an error message that says audience should be configured and link to our public doc that documents the appropriate audience for each cloud Audience provided and incorrect If we get this error and the audience is provided but is wrong, we should surface an error message that the configured audience is wrong and link to our public doc that documents the appropriate audience for each cloud. ### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? ### Are there test cases added in this PR? _(If not, why?)_ ### Provide a list of related PRs _(if any)_ ### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ ### Checklists - [ ] Added impacted package name to the issue description - [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ - [ ] Added a changelog (if necessary)
1 parent de01a8e commit 8ae427d

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import type { PagedAsyncIterableIterator, PagedResult } from "@azure/core-paging
5656
import { getPagedAsyncIterator } from "@azure/core-paging";
5757
import type { PipelinePolicy, RestError } from "@azure/core-rest-pipeline";
5858
import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
59+
import { audienceErrorHandlingPolicy } from "./internal/audienceErrorHandlingPolicy.js";
5960
import { SyncTokens, syncTokenPolicy } from "./internal/syncTokenPolicy.js";
6061
import { queryParamPolicy } from "./internal/queryParamPolicy.js";
6162
import type { TokenCredential } from "@azure/core-auth";
@@ -196,6 +197,13 @@ export class AppConfigurationClient {
196197
options?.apiVersion ?? appConfigurationApiVersion,
197198
internalClientPipelineOptions,
198199
);
200+
this.client.pipeline.addPolicy(
201+
audienceErrorHandlingPolicy(appConfigOptions?.audience !== undefined),
202+
{
203+
phase: "Sign",
204+
beforePolicies: [authPolicy.name],
205+
},
206+
);
199207
this.client.pipeline.addPolicy(authPolicy, { phase: "Sign" });
200208
this.client.pipeline.addPolicy(queryParamPolicy());
201209
this.client.pipeline.addPolicy(syncTokenPolicy(this._syncTokens), { afterPhase: "Retry" });
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
RestError,
6+
type PipelinePolicy,
7+
type PipelineRequest,
8+
type PipelineResponse,
9+
type SendRequest,
10+
} from "@azure/core-rest-pipeline";
11+
12+
const AadAudienceErrorCode = "AADSTS500011";
13+
const NoAudienceErrorMessage =
14+
"Unable to authenticate to Azure App Configuration. No authentication token audience was provided. Please set AppConfigurationClientOptions.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.";
15+
const WrongAudienceErrorMessage =
16+
"Unable to authenticate to Azure App Configuration. An incorrect token audience was provided. Please set AppConfigurationClientOptions.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.";
17+
18+
/**
19+
* Creates a PipelinePolicy that provides more helpful errors when Entra ID audience misconfiguration is detected.
20+
*/
21+
export function audienceErrorHandlingPolicy(isAudienceConfigured: boolean): PipelinePolicy {
22+
return {
23+
name: "audienceErrorHandlingPolicy",
24+
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
25+
try {
26+
return await next(request);
27+
} catch (error: any) {
28+
if (typeof error.message === "string" && error.message.includes(AadAudienceErrorCode)) {
29+
if (isAudienceConfigured) {
30+
throw new RestError(WrongAudienceErrorMessage);
31+
} else {
32+
throw new RestError(NoAudienceErrorMessage);
33+
}
34+
}
35+
throw error;
36+
}
37+
},
38+
};
39+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { describe, it, expect } from "vitest";
5+
import {
6+
createEmptyPipeline,
7+
createPipelineRequest,
8+
createDefaultHttpClient,
9+
type PipelinePolicy,
10+
} from "@azure/core-rest-pipeline";
11+
import { audienceErrorHandlingPolicy } from "../../src/internal/audienceErrorHandlingPolicy.js";
12+
13+
describe("audienceErrorHandlingPolicy", () => {
14+
function buildPipelineWithThrowingPolicy(isAudienceConfigured: boolean, thrownError: Error) {
15+
const pipeline = createEmptyPipeline();
16+
17+
const throwingPolicy: PipelinePolicy = {
18+
name: "throwingPolicy",
19+
async sendRequest() {
20+
throw thrownError;
21+
},
22+
};
23+
24+
// audienceErrorHandlingPolicy should wrap errors coming from later policies
25+
pipeline.addPolicy(audienceErrorHandlingPolicy(isAudienceConfigured));
26+
pipeline.addPolicy(throwingPolicy);
27+
28+
return pipeline;
29+
}
30+
31+
it("throws helpful RestError when AAD audience error occurs and audience is not configured", async () => {
32+
const originalError = new Error(
33+
"Some auth failure including AADSTS500011: No resource matches the audience",
34+
);
35+
36+
const pipeline = buildPipelineWithThrowingPolicy(false, originalError);
37+
const httpClient = createDefaultHttpClient();
38+
const request = createPipelineRequest({ url: "https://example.com" });
39+
40+
await expect(pipeline.sendRequest(httpClient, request)).rejects.toMatchObject({
41+
message: expect.stringMatching(/No authentication token audience was provided/),
42+
name: "RestError",
43+
});
44+
});
45+
46+
it("throws helpful RestError when AAD audience error occurs and audience is configured", async () => {
47+
const originalError = new Error(
48+
"Some auth failure including AADSTS500011: Invalid resource for this tenant",
49+
);
50+
51+
const pipeline = buildPipelineWithThrowingPolicy(true, originalError);
52+
const httpClient = createDefaultHttpClient();
53+
const request = createPipelineRequest({ url: "https://example.com" });
54+
55+
await expect(pipeline.sendRequest(httpClient, request)).rejects.toMatchObject({
56+
message: expect.stringMatching(/An incorrect token audience was provided/),
57+
name: "RestError",
58+
});
59+
});
60+
61+
it("rethrows non-AAD audience errors unchanged", async () => {
62+
const originalError = new Error("Network timeout, no AADSTS code here");
63+
64+
const pipeline = buildPipelineWithThrowingPolicy(true, originalError);
65+
const httpClient = createDefaultHttpClient();
66+
const request = createPipelineRequest({ url: "https://example.com" });
67+
68+
await expect(pipeline.sendRequest(httpClient, request)).rejects.toBe(originalError);
69+
});
70+
});

0 commit comments

Comments
 (0)