diff --git a/change/@azure-msal-common-3040b4da-993d-4047-91f8-0405382492ce.json b/change/@azure-msal-common-3040b4da-993d-4047-91f8-0405382492ce.json new file mode 100644 index 0000000000..a2d41c0788 --- /dev/null +++ b/change/@azure-msal-common-3040b4da-993d-4047-91f8-0405382492ce.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Reformatted ManagedIdentityTokenResponse + adjusted unit tests #7167", + "packageName": "@azure/msal-common", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-4b35e25c-7acf-468c-8323-9830342ad06c.json b/change/@azure-msal-node-4b35e25c-7acf-468c-8323-9830342ad06c.json new file mode 100644 index 0000000000..5b51541fd2 --- /dev/null +++ b/change/@azure-msal-node-4b35e25c-7acf-468c-8323-9830342ad06c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Reformatted ManagedIdentityTokenResponse + adjusted unit tests #7167", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index aa8c046958..c3a707b9dd 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -1650,6 +1650,7 @@ export const Constants: { NOT_DEFINED: string; EMPTY_STRING: string; NOT_APPLICABLE: string; + NOT_AVAILABLE: string; FORWARD_SLASH: string; IMDS_ENDPOINT: string; IMDS_VERSION: string; @@ -3618,8 +3619,9 @@ export type ServerDeviceCodeResponse = { // // @public export class ServerError extends AuthError { - constructor(errorCode?: string, errorMessage?: string, subError?: string, errorNo?: string); + constructor(errorCode?: string, errorMessage?: string, subError?: string, errorNo?: string, status?: number); readonly errorNo?: string; + readonly status?: number; } // Warning: (ae-missing-release-tag) "ServerResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -4261,9 +4263,9 @@ const X_MS_LIB_CAPABILITY = "x-ms-lib-capability"; // src/request/AuthenticationHeaderParser.ts:74:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/request/ScopeSet.ts:72:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/request/ScopeSet.ts:73:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// src/response/ResponseHandler.ts:419:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:420:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:421:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:430:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:431:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:432:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:886:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:886:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/telemetry/performance/PerformanceClient.ts:898:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen diff --git a/lib/msal-common/src/error/ServerError.ts b/lib/msal-common/src/error/ServerError.ts index 0a0b4ecb5f..cb7eaba2a4 100644 --- a/lib/msal-common/src/error/ServerError.ts +++ b/lib/msal-common/src/error/ServerError.ts @@ -14,15 +14,22 @@ export class ServerError extends AuthError { */ readonly errorNo?: string; + /** + * Http status number; + */ + readonly status?: number; + constructor( errorCode?: string, errorMessage?: string, subError?: string, - errorNo?: string + errorNo?: string, + status?: number ) { super(errorCode, errorMessage, subError); this.name = "ServerError"; this.errorNo = errorNo; + this.status = status; Object.setPrototypeOf(this, ServerError.prototype); } diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 28231e6618..7534fc190e 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -197,7 +197,17 @@ export class ResponseHandler { serverResponse.error_description || serverResponse.suberror ) { - const errString = `${serverResponse.error_codes} - [${serverResponse.timestamp}]: ${serverResponse.error_description} - Correlation ID: ${serverResponse.correlation_id} - Trace ID: ${serverResponse.trace_id}`; + const errString = `Error(s): ${ + serverResponse.error_codes || Constants.NOT_AVAILABLE + } - Timestamp: ${ + serverResponse.timestamp || Constants.NOT_AVAILABLE + } - Description: ${ + serverResponse.error_description || Constants.NOT_AVAILABLE + } - Correlation ID: ${ + serverResponse.correlation_id || Constants.NOT_AVAILABLE + } - Trace ID: ${ + serverResponse.trace_id || Constants.NOT_AVAILABLE + }`; const serverErrorNo = serverResponse.error_codes?.length ? serverResponse.error_codes[0] : undefined; @@ -205,7 +215,8 @@ export class ResponseHandler { serverResponse.error, errString, serverResponse.suberror, - serverErrorNo + serverErrorNo, + serverResponse.status ); // check if 500 error diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index 8b5a35e1a1..24ae44a31c 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -45,6 +45,7 @@ export const Constants = { NOT_DEFINED: "not_defined", EMPTY_STRING: "", NOT_APPLICABLE: "N/A", + NOT_AVAILABLE: "Not Available", FORWARD_SLASH: "/", IMDS_ENDPOINT: "http://169.254.169.254/metadata/instance/compute/location", IMDS_VERSION: "2020-06-01", diff --git a/lib/msal-common/test/error/ServerError.spec.ts b/lib/msal-common/test/error/ServerError.spec.ts index 5a138677f2..c35060c449 100644 --- a/lib/msal-common/test/error/ServerError.spec.ts +++ b/lib/msal-common/test/error/ServerError.spec.ts @@ -1,13 +1,18 @@ import { ServerError } from "../../src/error/ServerError"; import { AuthError } from "../../src/error/AuthError"; +import { Constants, HttpStatus } from "../../src"; describe("ServerError.ts Class Unit Tests", () => { it("ServerError object can be created", () => { const TEST_ERROR_CODE: string = "test"; const TEST_ERROR_MSG: string = "This is a test error"; + const TEST_ERROR_STATUS: number = HttpStatus.BAD_REQUEST; const err: ServerError = new ServerError( TEST_ERROR_CODE, - TEST_ERROR_MSG + TEST_ERROR_MSG, + undefined, + undefined, + TEST_ERROR_STATUS ); expect(err instanceof ServerError).toBe(true); @@ -18,5 +23,21 @@ describe("ServerError.ts Class Unit Tests", () => { expect(err.message).toBe(`${TEST_ERROR_CODE}: ${TEST_ERROR_MSG}`); expect(err.name).toBe("ServerError"); expect(err.stack?.includes("ServerError.spec.ts")).toBe(true); + expect(err.status).toBe(TEST_ERROR_STATUS); + }); + + it("Values are set as expected when no info was provided to ServerError", () => { + const err: ServerError = new ServerError(); + + expect(err instanceof ServerError).toBe(true); + expect(err instanceof AuthError).toBe(true); + expect(err instanceof Error).toBe(true); + expect(err.errorCode).toBe(Constants.EMPTY_STRING); + expect(err.errorMessage).toBe(Constants.EMPTY_STRING); + expect(err.errorNo).toBeUndefined(); + expect(err.message).toBe(Constants.EMPTY_STRING); + expect(err.name).toBe("ServerError"); + expect(err.stack?.includes("ServerError.spec.ts")).toBe(true); + expect(err.status).toBeUndefined(); }); }); diff --git a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts index eb88b23a7e..f19de8d635 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -102,8 +102,20 @@ export abstract class BaseManagedIdentitySource { refresh_in: refreshIn, // error - error: response.body.message, - correlation_id: response.body.correlationId, + correlation_id: + response.body.correlation_id || response.body.correlationId, + error: + typeof response.body.error === "string" + ? response.body.error + : response.body.error?.code, + error_description: + response.body.message || + (typeof response.body.error === "string" + ? response.body.error_description + : response.body.error?.message), + error_codes: response.body.error_codes, + timestamp: response.body.timestamp, + trace_id: response.body.trace_id, }; return serverTokenResponse; diff --git a/lib/msal-node/src/response/ManagedIdentityTokenResponse.ts b/lib/msal-node/src/response/ManagedIdentityTokenResponse.ts index 662d2bfda9..122157ef91 100644 --- a/lib/msal-node/src/response/ManagedIdentityTokenResponse.ts +++ b/lib/msal-node/src/response/ManagedIdentityTokenResponse.ts @@ -28,6 +28,28 @@ export type ManagedIdentityTokenResponse = { token_type?: AuthenticationScheme; // error - message?: string; // will be converted to error - correlationId?: string; // will be converted to correlation_id + + /* + * (Web/Function) App Service + * 500 errors can return this from all MI sources as well + */ + message?: string; + correlationId?: string; + + // IMDS, Azure Arc, Service Fabric (unconfirmed) + error?: string | ErrorObject; + error_description?: string; + error_codes?: Array; + correlation_id?: string; + timestamp?: string; + trace_id?: string; +}; + +/* + * This is the only error property that exists for Cloud Shell + * It can also be the only thing App Service will return + */ +export type ErrorObject = { + code: string; + message: string; }; diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index 9fbbdf50ec..a765bfcb37 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -7,6 +7,7 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityA import { DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, } from "../../test_kit/StringConstants"; @@ -14,8 +15,14 @@ import { userAssignedClientIdConfig, managedIdentityRequestParams, systemAssignedConfig, + networkClient, + ManagedIdentityNetworkErrorClient, } from "../../test_kit/ManagedIdentityTestUtils"; -import { AuthenticationResult } from "@azure/msal-common"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient"; import { ManagedIdentityEnvironmentVariableNames, @@ -106,4 +113,49 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ); }); }); + + describe("Errors", () => { + test("ensures that the error format is correct", async () => { + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(systemAssignedConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.APP_SERVICE + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR.message as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR.correlation_id as string + ) + ).toBe(true); + + jest.restoreAllMocks(); + }); + }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts index d6f190c535..0a5107dc17 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts @@ -6,22 +6,26 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication"; import { DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_AZURE_ARC_WWW_AUTHENTICATE_HEADER, MANAGED_IDENTITY_CONTENT_TYPE_HEADER, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_BASE, - MANAGED_IDENTITY_RESOURCE_ID, TEST_TOKENS, } from "../../test_kit/StringConstants"; import { - ManagedIdentityNetworkClient, ManagedIdentityNetworkErrorClient, systemAssignedConfig, managedIdentityRequestParams, userAssignedClientIdConfig, + networkClient, } from "../../test_kit/ManagedIdentityTestUtils"; -import { AuthenticationResult, HttpStatus } from "@azure/msal-common"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient"; import { ManagedIdentityErrorCodes, @@ -73,6 +77,15 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = delete ManagedIdentityApplication["nodeStorage"]; }); + const managedIdentityNetworkErrorClient401 = + new ManagedIdentityNetworkErrorClient( + {}, // 401 error response. Will be ignored because only the www-authenticate header is relevant when Azure Arc returns a 401 error + { + "www-authenticate": `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.key`, + }, + HttpStatus.UNAUTHORIZED + ); + // Azure Arc Managed Identities can only be system assigned describe("System Assigned", () => { let managedIdentityApplication: ManagedIdentityApplication; @@ -119,30 +132,12 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("attempts to acquire a token, a 401 and www-authenticate header are returned from the azure arc managed identity, then retries the network request with the www-authenticate header", async () => { - const networkClient: ManagedIdentityNetworkClient = - new ManagedIdentityNetworkClient(MANAGED_IDENTITY_RESOURCE_ID); - - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient, - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); - - const networkErrorClient: ManagedIdentityNetworkErrorClient = - new ManagedIdentityNetworkErrorClient(); const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") // override the networkClient's sendGetRequestAsync method to return a 401 // and the WWW-Authentication header the first time the network request is executed .mockReturnValueOnce( - networkErrorClient.getSendGetRequestAsyncReturnObject( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.key` // Linux - ) + managedIdentityNetworkErrorClient401.sendGetRequestAsync() ); const statSyncSpy: jest.SpyInstance = jest @@ -191,15 +186,25 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); describe("Errors", () => { - test("throws an error if a user assigned managed identity is used", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication(userAssignedClientIdConfig); + let managedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + managedIdentityApplication = new ManagedIdentityApplication( + systemAssignedConfig + ); expect(managedIdentityApplication.getManagedIdentitySource()).toBe( ManagedIdentitySourceNames.AZURE_ARC ); + }); + + test("throws an error if a user assigned managed identity is used", async () => { + const userAssignedManagedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedClientIdConfig); + expect( + userAssignedManagedIdentityApplication.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.AZURE_ARC); await expect( - managedIdentityApplication.acquireToken( + userAssignedManagedIdentityApplication.acquireToken( managedIdentityRequestParams ) ).rejects.toMatchObject( @@ -210,18 +215,21 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header has been returned from the azure arc managed identity, but the file in the file path is not a .key file", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.txt` // Linux - ), - // managedIdentityIdParams will be omitted for system assigned + const managedIdentityNetworkErrorClient401FileNotFound = + new ManagedIdentityNetworkErrorClient( + {}, // 401 error response. Will be ignored because only the www-authenticate header is relevant when Azure Arc returns a 401 error, + { + "www-authenticate": `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.txt`, // Linux }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + HttpStatus.UNAUTHORIZED + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401FileNotFound.sendGetRequestAsync() + ); await expect( managedIdentityApplication.acquireToken( @@ -237,18 +245,12 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header has been returned from the azure arc managed identity, but the managed identity application is not being run on Windows or Linux", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.key` // Linux - ), - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401.sendGetRequestAsync() + ); Object.defineProperty(process, "platform", { value: "darwin", @@ -271,18 +273,21 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header has been returned from the azure arc managed identity, but the path of the secret file from the www-authenticate header is not in the expected Windows or Linux formats", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}this_will_throw_because_file_path_must_match_exactly/AzureArcSecret.key` // Linux - ), - // managedIdentityIdParams will be omitted for system assigned + const managedIdentityNetworkErrorClient401BadFilePath = + new ManagedIdentityNetworkErrorClient( + {}, // 401 error response. Will be ignored because only the www-authenticate header is relevant when Azure Arc returns a 401 error, + { + "www-authenticate": `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}this_will_throw_because_file_path_must_match_exactly/AzureArcSecret.key`, // Linux }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + HttpStatus.UNAUTHORIZED + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401BadFilePath.sendGetRequestAsync() + ); await expect( managedIdentityApplication.acquireToken( @@ -298,18 +303,12 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header has been returned from the azure arc managed identity, but the size of the secret file from the www-authenticate header is greater than 4096 bytes", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.key` // Linux - ), - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401.sendGetRequestAsync() + ); jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 4097, @@ -329,19 +328,19 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header is missing", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - undefined, - HttpStatus.UNAUTHORIZED - ), - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + const managedIdentityNetworkErrorClient401HeaderMissing = + new ManagedIdentityNetworkErrorClient( + {}, // 401 error response. Will be ignored because only the www-authenticate header is relevant when Azure Arc returns a 401 error, + {}, // www-authenticate header missing + HttpStatus.UNAUTHORIZED + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401HeaderMissing.sendGetRequestAsync() + ); await expect( managedIdentityApplication.acquireToken( @@ -355,18 +354,21 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the www-authenticate header is in an unsupported format", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - "unsupported_format" - ), - // managedIdentityIdParams will be omitted for system assigned + const managedIdentityNetworkErrorClient401HeaderBadFormat = + new ManagedIdentityNetworkErrorClient( + {}, // 401 error response. Will be ignored because only the www-authenticate header is relevant when Azure Arc returns a 401 error, + { + "www-authenticate": "unsupported_format", }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + HttpStatus.UNAUTHORIZED + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401HeaderBadFormat.sendGetRequestAsync() + ); await expect( managedIdentityApplication.acquireToken( @@ -380,18 +382,12 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = }); test("throws an error if the secret file cannot be found/read", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: new ManagedIdentityNetworkErrorClient( - `Basic realm=${SUPPORTED_AZURE_ARC_PLATFORMS.linux}AzureArcSecret.key` // Linux - ), - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.AZURE_ARC - ); + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401.sendGetRequestAsync() + ); jest.spyOn(fs, "statSync").mockImplementationOnce(() => { throw new Error(); @@ -407,6 +403,13 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = ) ); + jest.spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 401 + // and the WWW-Authentication header the first time the network request is executed + .mockReturnValueOnce( + managedIdentityNetworkErrorClient401.sendGetRequestAsync() + ); + jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 4000, } as fs.Stats); @@ -427,5 +430,64 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = jest.restoreAllMocks(); }); + + test("ensures that the error format is correct", async () => { + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.error as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.error_description as string + ) + ).toBe(true); + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.error_codes?.forEach( + (errorCode) => { + expect(serverError.errorMessage.includes(errorCode)).toBe( + true + ); + } + ); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.timestamp as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.trace_id as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR.correlation_id as string + ) + ).toBe(true); + + jest.restoreAllMocks(); + }); }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/CloudShell.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/CloudShell.spec.ts index 4085dd6a1c..64d9b57353 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/CloudShell.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/CloudShell.spec.ts @@ -6,15 +6,24 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication"; import { DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_CLOUD_SHELL_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, } from "../../test_kit/StringConstants"; import { userAssignedClientIdConfig, managedIdentityRequestParams, systemAssignedConfig, + ManagedIdentityNetworkErrorClient, + networkClient, } from "../../test_kit/ManagedIdentityTestUtils"; -import { AuthenticationResult } from "@azure/msal-common"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient"; import { ManagedIdentityEnvironmentVariableNames, @@ -106,5 +115,46 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ) ); }); + + test("ensures that the error format is correct", async () => { + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_CLOUD_SHELL_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + jest.spyOn(networkClient, "sendPostRequestAsync") + // permanently override the networkClient's sendPostRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendPostRequestAsync() + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(systemAssignedConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.CLOUD_SHELL + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorCode.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE as string + ) + ).toBe(true); + }); }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts index c145d89248..b3b1e3d315 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts @@ -8,11 +8,12 @@ import { ManagedIdentityConfiguration } from "../../../src/config/Configuration" import { DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_BASE, MANAGED_IDENTITY_RESOURCE_ID, MANAGED_IDENTITY_RESOURCE_ID_2, - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, THREE_SECONDS_IN_MILLI, getCacheKey, } from "../../test_kit/StringConstants"; @@ -51,7 +52,6 @@ import { ClientCredentialClient, NodeStorage, } from "../../../src"; -import { mockAuthenticationResult } from "../../utils/TestConstants"; // NodeJS 16+ provides a built-in version of setTimeout that is promise-based import { setTimeout } from "timers/promises"; @@ -63,8 +63,14 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { delete ManagedIdentityApplication["nodeStorage"]; }); - const managedIdentityNetworkErrorClient = + const managedIdentityNetworkErrorClientDefault500 = new ManagedIdentityNetworkErrorClient(); + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); const userAssignedObjectIdConfig: ManagedIdentityConfiguration = { system: { @@ -200,7 +206,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { // after this override, original functionality will be restored // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); const networkManagedIdentityResult: AuthenticationResult = @@ -221,7 +227,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 500 .mockReturnValue( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); let serverError: ServerError = new ServerError(); @@ -233,9 +239,12 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { serverError = e as ServerError; } - expect(serverError.errorCode).toEqual( - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR - ); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries jest.restoreAllMocks(); @@ -260,7 +269,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { // after this override, original functionality will be restored // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); @@ -290,15 +299,16 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { const headers: Record = { "Retry-After": "3", // 3 seconds }; + const managedIdentityNetworkErrorClient = + new ManagedIdentityNetworkErrorClient(undefined, headers); + const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") // override the networkClient's sendGetRequestAsync method to return a 500. // after this override, original functionality will be restored // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync( - headers - ) + managedIdentityNetworkErrorClient.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); @@ -332,15 +342,16 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { const headers: Record = { "Retry-After": retryAfterHttpDate.toString(), }; + const managedIdentityNetworkErrorClient = + new ManagedIdentityNetworkErrorClient(undefined, headers); + const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") // override the networkClient's sendGetRequestAsync method to return a 500. // after this override, original functionality will be restored // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync( - headers - ) + managedIdentityNetworkErrorClient.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); @@ -371,7 +382,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 500 .mockReturnValue( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); let serverError: ServerError = new ServerError(); @@ -383,9 +394,11 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { serverError = e as ServerError; } - expect(serverError.errorCode).toEqual( - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR - ); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries jest.restoreAllMocks(); @@ -396,7 +409,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 500 .mockReturnValue( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); try { @@ -431,20 +444,25 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 400 .mockReturnValue( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync( - undefined, - HttpStatus.BAD_REQUEST - ) + managedIdentityNetworkErrorClient400.sendGetRequestAsync() ); + let serverError: ServerError = new ServerError(); try { await managedIdentityApplication.acquireToken( managedIdentityRequestParams ); } catch (e) { - expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(1); + serverError = e as ServerError; } + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(1); + jest.restoreAllMocks(); }); @@ -464,7 +482,7 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 500 .mockReturnValue( - managedIdentityNetworkErrorClient.sendGetRequestForRetryAsync() + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() ); let serverError: ServerError = new ServerError(); @@ -476,9 +494,11 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { serverError = e as ServerError; } - expect(serverError.errorCode).toEqual( - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR - ); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(1); jest.restoreAllMocks(); @@ -784,13 +804,16 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { }); describe("Errors", () => { - test("throws an error when an invalid resource is provided", async () => { - const systemAssignedManagedIdentityApplication: ManagedIdentityApplication = + let systemAssignedManagedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + systemAssignedManagedIdentityApplication = new ManagedIdentityApplication(systemAssignedConfig); expect( systemAssignedManagedIdentityApplication.getManagedIdentitySource() ).toBe(ManagedIdentitySourceNames.DEFAULT_TO_IMDS); + }); + test("throws an error when an invalid resource is provided", async () => { await expect( systemAssignedManagedIdentityApplication.acquireToken({ resource: "", @@ -823,36 +846,56 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ); }); - test("managed identity token response contains an error message and correlation id when an error is returned from the managed identity", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication({ - system: { - networkClient: managedIdentityNetworkErrorClient, - // managedIdentityIdParams will be omitted for system assigned - }, - }); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.DEFAULT_TO_IMDS - ); + test("ensures that the error format is correct", async () => { + jest.spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); let serverError: ServerError = new ServerError(); try { - await managedIdentityApplication.acquireToken( + await systemAssignedManagedIdentityApplication.acquireToken( managedIdentityRequestParams ); } catch (e) { serverError = e as ServerError; } - expect(serverError.errorCode).toEqual( - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.error as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.error_description as string + ) + ).toBe(true); + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.error_codes?.forEach( + (errorCode) => { + expect(serverError.errorMessage.includes(errorCode)).toBe( + true + ); + } ); - - const correlationIdCheck: boolean = + expect( serverError.errorMessage.includes( - mockAuthenticationResult.correlationId - ); - expect(correlationIdCheck).toBe(true); + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.timestamp as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.trace_id as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR.correlation_id as string + ) + ).toBe(true); + + jest.restoreAllMocks(); }); }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index 81e5bccb28..1cc5a0e84f 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts @@ -8,14 +8,21 @@ import { DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_RESOURCE, + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, } from "../../test_kit/StringConstants"; import { userAssignedClientIdConfig, managedIdentityRequestParams, systemAssignedConfig, + ManagedIdentityNetworkErrorClient, + networkClient, } from "../../test_kit/ManagedIdentityTestUtils"; -import { AuthenticationResult } from "@azure/msal-common"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient"; import { ManagedIdentityEnvironmentVariableNames, @@ -112,4 +119,71 @@ describe("Acquires a token successfully via an App Service Managed Identity", () ); }); }); + + describe("Errors", () => { + test("ensures that the error format is correct", async () => { + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + jest.spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(systemAssignedConfig); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.SERVICE_FABRIC + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error_description as string + ) + ).toBe(true); + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error_codes?.forEach( + (errorCode) => { + expect(serverError.errorMessage.includes(errorCode)).toBe( + true + ); + } + ); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.timestamp as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.trace_id as string + ) + ).toBe(true); + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.correlation_id as string + ) + ).toBe(true); + + jest.restoreAllMocks(); + }); + }); }); diff --git a/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts b/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts index 85ed8fb302..977b0eae61 100644 --- a/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts +++ b/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts @@ -7,21 +7,19 @@ import { AuthenticationScheme, HttpStatus, INetworkModule, - NetworkRequestOptions, NetworkResponse, TimeUtils, } from "@azure/msal-common"; import { + MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_ID, - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, TEST_TOKENS, TEST_TOKEN_LIFETIMES, } from "./StringConstants"; import { ManagedIdentityTokenResponse } from "../../src/response/ManagedIdentityTokenResponse"; import { ManagedIdentityRequestParams } from "../../src"; import { ManagedIdentityConfiguration } from "../../src/config/Configuration"; -import { mockAuthenticationResult } from "../utils/TestConstants"; const EMPTY_HEADERS: Record = {}; @@ -33,11 +31,8 @@ export class ManagedIdentityNetworkClient implements INetworkModule { this.clientId = clientId; } - sendGetRequestAsync( - _url: string, - _options?: NetworkRequestOptions, - _timeout?: number - ): Promise> { + // App Service, Azure Arc, Imds, Service Fabric + sendGetRequestAsync(): Promise> { return new Promise>((resolve, _reject) => { resolve({ status: HttpStatus.SUCCESS, @@ -58,10 +53,8 @@ export class ManagedIdentityNetworkClient implements INetworkModule { }); } - sendPostRequestAsync( - _url: string, - _options?: NetworkRequestOptions - ): Promise> { + // Cloud Shell + sendPostRequestAsync(): Promise> { return new Promise>((resolve, _reject) => { resolve({ status: HttpStatus.SUCCESS, @@ -83,80 +76,39 @@ export class ManagedIdentityNetworkClient implements INetworkModule { } export class ManagedIdentityNetworkErrorClient implements INetworkModule { - private wwwAuthenticateHeader: string | undefined; - private status: number | undefined; + private errorResponse: ManagedIdentityTokenResponse; + private headers: Record; + private status: number; - constructor(wwwAuthenticateHeader?: string, status?: number) { - this.wwwAuthenticateHeader = wwwAuthenticateHeader || undefined; - this.status = status || undefined; - } - - getSendGetRequestAsyncReturnObject( - wwwAuthenticateHeader: string | undefined, - status?: number | undefined - ): NetworkResponse { - const headers: Record = {}; - if (wwwAuthenticateHeader) { - headers["www-authenticate"] = wwwAuthenticateHeader; - } - - return { - status: - status || - (wwwAuthenticateHeader - ? HttpStatus.UNAUTHORIZED - : HttpStatus.BAD_REQUEST), - body: { - message: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, - correlationId: mockAuthenticationResult.correlationId, - } as ManagedIdentityTokenResponse, - headers, - } as NetworkResponse; - } - - sendGetRequestAsync( - _url: string, - _options?: NetworkRequestOptions, - _timeout?: number - ): Promise> { - return new Promise>((resolve, _reject) => { - resolve( - this.getSendGetRequestAsyncReturnObject( - this.wwwAuthenticateHeader, - this.status - ) - ); - }); + constructor( + errorResponse?: ManagedIdentityTokenResponse, + headers?: Record, + status?: number + ) { + // default to 500 error + this.errorResponse = + errorResponse || MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR; + this.headers = headers || EMPTY_HEADERS; + this.status = status || HttpStatus.SERVER_ERROR; } - sendGetRequestForRetryAsync( - // optional retry-after header - headers?: Record, - httpStatusCode?: HttpStatus - ): Promise> { + // App Service, Azure Arc, Imds, Service Fabric + sendGetRequestAsync(): Promise> { return new Promise>((resolve, _reject) => { resolve({ - status: httpStatusCode || HttpStatus.SERVER_ERROR, - body: { - message: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, - correlationId: mockAuthenticationResult.correlationId, - } as ManagedIdentityTokenResponse, - headers: headers || EMPTY_HEADERS, + status: this.status, + body: this.errorResponse, + headers: this.headers, } as NetworkResponse); }); } - sendPostRequestAsync( - _url: string, - _options?: NetworkRequestOptions - ): Promise> { + // Cloud Shell + sendPostRequestAsync(): Promise> { return new Promise>((resolve, _reject) => { resolve({ - status: HttpStatus.BAD_REQUEST, - body: { - message: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, - correlationId: mockAuthenticationResult.correlationId, - } as ManagedIdentityTokenResponse, + status: this.status, + body: this.errorResponse, headers: EMPTY_HEADERS, } as NetworkResponse); }); diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index f858d6ab6f..8809572c87 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -16,6 +16,7 @@ import { DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY, DEFAULT_MANAGED_IDENTITY_ID, } from "../../src/utils/Constants"; +import { ManagedIdentityTokenResponse } from "../../src/response/ManagedIdentityTokenResponse"; // This file contains the string constants used by the test classes. @@ -378,8 +379,52 @@ export const MANAGED_IDENTITY_AZURE_ARC_WWW_AUTHENTICATE_HEADER: string = `Basic export const MANAGED_IDENTITY_CONTENT_TYPE_HEADER: string = "application/x-www-form-urlencoded;charset=utf-8"; -export const MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR: string = +export const MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE: string = "There was an error retrieving the access token from the managed identity."; +export const MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR = "fake_error"; +const MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_CORRELATION_ID = + "fake_correlation_id"; + +// Any Managed Identity Source 500 error response +export const MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR: ManagedIdentityTokenResponse = + { + message: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, + correlation_id: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_CORRELATION_ID, + }; + +// App Service 400 error response +export const MANAGED_IDENTITY_APP_SERVICE_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + ...MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, + }; +// Cloud Shell 400 error response +export const MANAGED_IDENTITY_CLOUD_SHELL_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + error: { + code: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, + message: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, + }, + }; +// Azure Arc 400 error response +export const MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + error: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR, + error_description: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, + error_codes: ["fake_error_code"], + timestamp: "fake_timestamp", + trace_id: "fake_trace_id", + correlation_id: MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_CORRELATION_ID, + }; +// Imds 400 error response +export const MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + ...MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR, + }; +// Service Fabric 400 error response +export const MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR: ManagedIdentityTokenResponse = + { + ...MANAGED_IDENTITY_AZURE_ARC_NETWORK_REQUEST_400_ERROR, + }; export const MANAGED_IDENTITY_RESOURCE_BASE: string = "https://graph.microsoft.com";