Skip to content

Commit

Permalink
Region auto enable on env variable (#7354)
Browse files Browse the repository at this point in the history
On creation of a ConfidentialClientApplication, MSAL shall detect env
variable MSAL_FORCE_REGION, which will be set to a specific region (e.g.
westus1)

If this env variable is set, MSAL shall opt-in to ESTS-R with the value
of this variable.

This feature request is located
[here](#7328).
  • Loading branch information
Robbie-Microsoft authored Oct 31, 2024
1 parent 933f21d commit 098a957
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Region auto enable on env variable #7354",
"packageName": "@azure/msal-node",
"email": "[email protected]",
"dependentChangeType": "patch"
}
20 changes: 19 additions & 1 deletion lib/msal-node/src/client/ConfidentialClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Constants as NodeConstants,
ApiId,
REGION_ENVIRONMENT_VARIABLE,
MSAL_FORCE_REGION,
} from "../utils/Constants.js";
import {
CommonClientCredentialRequest,
Expand All @@ -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";
Expand Down Expand Up @@ -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],
};

Expand Down
1 change: 1 addition & 0 deletions lib/msal-node/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 130 additions & 11 deletions lib/msal-node/test/client/ConfidentialClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
DEFAULT_OPENID_CONFIG_RESPONSE,
ID_TOKEN_CLAIMS,
TEST_CONSTANTS,
} from "../utils/TestConstants";
} from "../utils/TestConstants.js";
import {
ConfidentialClientApplication,
OnBehalfOfRequest,
Expand All @@ -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");

Expand All @@ -53,15 +57,17 @@ describe("ConfidentialClientApplication", () => {
jest.spyOn(jwt, <any>"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
);
});

Expand Down Expand Up @@ -340,6 +346,119 @@ describe("ConfidentialClientApplication", () => {
);
});

describe("region is determined correctly", () => {
const checkRegion = (
endpointFromSpy: string,
expectedRegion: string
) => {
const endpoint: string = endpointFromSpy;
const regionMatch: Array<string> | 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,
<any>"acquireTokenByClientCredential"
);

buildOauthClientConfigurationSpy = jest.spyOn(
ConfidentialClientApplication.prototype,
<any>"buildOauthClientConfiguration"
);

sendPostRequestAsyncSpy = jest.spyOn(
networkClient,
<any>"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<string> | 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,
Expand Down

0 comments on commit 098a957

Please sign in to comment.