diff --git a/change/@azure-msal-node-b449273d-2a9c-4b34-a210-42f3b3ac9868.json b/change/@azure-msal-node-b449273d-2a9c-4b34-a210-42f3b3ac9868.json new file mode 100644 index 0000000000..791cf11f69 --- /dev/null +++ b/change/@azure-msal-node-b449273d-2a9c-4b34-a210-42f3b3ac9868.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Region auto enable on env variable #7354", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-node/src/client/ConfidentialClientApplication.ts b/lib/msal-node/src/client/ConfidentialClientApplication.ts index 02998e6d0b..19e687215b 100644 --- a/lib/msal-node/src/client/ConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/ConfidentialClientApplication.ts @@ -12,6 +12,7 @@ import { Constants as NodeConstants, ApiId, REGION_ENVIRONMENT_VARIABLE, + MSAL_FORCE_REGION, } from "../utils/Constants.js"; import { CommonClientCredentialRequest, @@ -27,6 +28,7 @@ import { ClientAuthErrorCodes, ClientAssertion as ClientAssertionType, getClientAssertion, + AzureRegion, } from "@azure/msal-common/node"; import { IConfidentialClientApplication } from "./IConfidentialClientApplication.js"; import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest.js"; @@ -136,8 +138,24 @@ export class ConfidentialClientApplication ); } + /* + * if this env variable is set, and the developer provided region isn't defined and isn't "DisableMsalForceRegion", + * MSAL shall opt-in to ESTS-R with the value of this variable + */ + const ENV_MSAL_FORCE_REGION: AzureRegion | undefined = + process.env[MSAL_FORCE_REGION]; + + let region: AzureRegion | undefined; + if (validRequest.azureRegion !== "DisableMsalForceRegion") { + if (!validRequest.azureRegion && ENV_MSAL_FORCE_REGION) { + region = ENV_MSAL_FORCE_REGION; + } else { + region = validRequest.azureRegion; + } + } + const azureRegionConfiguration: AzureRegionConfiguration = { - azureRegion: validRequest.azureRegion, + azureRegion: region, environmentRegion: process.env[REGION_ENVIRONMENT_VARIABLE], }; diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index 0b28a947ee..efbffa267b 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -82,6 +82,7 @@ export type ProxyStatus = (typeof ProxyStatus)[keyof typeof ProxyStatus]; * Constants used for region discovery */ export const REGION_ENVIRONMENT_VARIABLE = "REGION_NAME"; +export const MSAL_FORCE_REGION = "MSAL_FORCE_REGION"; /** * Constant used for PKCE diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index e476a42302..fd93da9748 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -20,7 +20,7 @@ import { DEFAULT_OPENID_CONFIG_RESPONSE, ID_TOKEN_CLAIMS, TEST_CONSTANTS, -} from "../utils/TestConstants"; +} from "../utils/TestConstants.js"; import { ConfidentialClientApplication, OnBehalfOfRequest, @@ -32,19 +32,23 @@ import { RefreshTokenRequest, SilentFlowRequest, ClientApplication, -} from "../../src"; +} from "../../src/index.js"; import { CAE_CONSTANTS, CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT, TEST_CONFIG, TEST_TOKENS, -} from "../test_kit/StringConstants"; -import { mockNetworkClient } from "../utils/MockNetworkClient"; -import { ClientTestUtils, getClientAssertionCallback } from "./ClientTestUtils"; +} from "../test_kit/StringConstants.js"; +import { mockNetworkClient } from "../utils/MockNetworkClient.js"; +import { + ClientTestUtils, + getClientAssertionCallback, +} from "./ClientTestUtils.js"; import { buildAccountFromIdTokenClaims } from "msal-test-utils"; -import { Constants } from "../../src/utils/Constants"; +import { Constants, MSAL_FORCE_REGION } from "../../src/utils/Constants.js"; import jwt from "jsonwebtoken"; -import { NodeAuthError } from "../../src/error/NodeAuthError"; +import { NodeAuthError } from "../../src/error/NodeAuthError.js"; +import { INetworkModule } from "../../../msal-common/lib/types/exports-common.js"; jest.mock("jsonwebtoken"); @@ -53,15 +57,17 @@ describe("ConfidentialClientApplication", () => { jest.spyOn(jwt, "sign").mockReturnValue("fake_jwt_string"); }); + const networkClient: INetworkModule = mockNetworkClient( + DEFAULT_OPENID_CONFIG_RESPONSE.body, + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT + ); + let config: Configuration; beforeEach(async () => { config = await ClientTestUtils.createTestConfidentialClientConfiguration( undefined, - mockNetworkClient( - DEFAULT_OPENID_CONFIG_RESPONSE.body, - CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT - ) + networkClient ); }); @@ -340,6 +346,119 @@ describe("ConfidentialClientApplication", () => { ); }); + describe("region is determined correctly", () => { + const checkRegion = ( + endpointFromSpy: string, + expectedRegion: string + ) => { + const endpoint: string = endpointFromSpy; + const regionMatch: Array | null = endpoint.match( + "https://(.*).login.microsoft.com/tenantid/oauth2/v2.0/token/" + ); + expect(regionMatch && regionMatch.length).toEqual(2); + expect(regionMatch && regionMatch[1]).toEqual(expectedRegion); + }; + + let acquireTokenByClientCredentialSpy: jest.SpyInstance; + let buildOauthClientConfigurationSpy: jest.SpyInstance; + let sendPostRequestAsyncSpy: jest.SpyInstance; + let client: ConfidentialClientApplication; + let request: ClientCredentialRequest; + beforeEach(() => { + acquireTokenByClientCredentialSpy = jest.spyOn( + ConfidentialClientApplication.prototype, + "acquireTokenByClientCredential" + ); + + buildOauthClientConfigurationSpy = jest.spyOn( + ConfidentialClientApplication.prototype, + "buildOauthClientConfiguration" + ); + + sendPostRequestAsyncSpy = jest.spyOn( + networkClient, + "sendPostRequestAsync" + ); + + client = new ConfidentialClientApplication(config); + + request = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + skipCache: false, + }; + + process.env[MSAL_FORCE_REGION] = "eastus"; + }); + + afterEach(() => { + delete process.env[MSAL_FORCE_REGION]; + }); + + test("region is not passed in through the request, the MSAL_FORCE_REGION environment variable is used", async () => { + const authResult = (await client.acquireTokenByClientCredential( + request + )) as AuthenticationResult; + expect(authResult.accessToken).toEqual( + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token + ); + expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( + 1 + ); + expect( + buildOauthClientConfigurationSpy.mock.lastCall[4] + .azureRegion + ).toEqual(process.env[MSAL_FORCE_REGION]); + + checkRegion( + sendPostRequestAsyncSpy.mock.lastCall[0], + process.env[MSAL_FORCE_REGION] as string + ); + }); + + test("region is passed in through the request, the MSAL_FORCE_REGION environment variable is not used", async () => { + const region = "westus"; + + const authResult = (await client.acquireTokenByClientCredential( + { ...request, azureRegion: region } + )) as AuthenticationResult; + expect(authResult.accessToken).toEqual( + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token + ); + expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( + 1 + ); + expect( + buildOauthClientConfigurationSpy.mock.lastCall[4] + .azureRegion + ).toEqual(region); + + checkRegion(sendPostRequestAsyncSpy.mock.lastCall[0], region); + }); + + test('region is not passed in through the request, the MSAL_FORCE_REGION environment variable is set to "DisableMsalForceRegion"', async () => { + const authResult = (await client.acquireTokenByClientCredential( + { ...request, azureRegion: "DisableMsalForceRegion" } + )) as AuthenticationResult; + expect(authResult.accessToken).toEqual( + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token + ); + expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( + 1 + ); + expect( + buildOauthClientConfigurationSpy.mock.lastCall[4] + .azureRegion + ).toBeUndefined(); + + const endpoint: string = + sendPostRequestAsyncSpy.mock.lastCall[0]; + const regionMatch: Array | null = endpoint.match( + "https://(.*).login.microsoft.com/tenantid/oauth2/v2.0/token/" + ); + expect(regionMatch).toBeNull(); + }); + }); + test("acquireTokenByClientCredential request does not contain OIDC scopes", async () => { const request: ClientCredentialRequest = { scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE,