Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Migrate region auto-discovery to the IMDS /compute JSON endpoint (api-version 2021-02-01), reading the location field [#8660](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8660)",
"packageName": "@azure/msal-common",
"email": "rginsburg@microsoft.com",
"dependentChangeType": "patch"
}
4 changes: 2 additions & 2 deletions lib/msal-common/apiReview/msal-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1793,13 +1793,13 @@ export interface ILoggerCallback {
}

// @public (undocumented)
const IMDS_ENDPOINT = "http://169.254.169.254/metadata/instance/compute/location";
const IMDS_ENDPOINT = "http://169.254.169.254/metadata/instance/compute";

// @public (undocumented)
const IMDS_TIMEOUT = 2000;

// @public (undocumented)
const IMDS_VERSION = "2020-06-01";
const IMDS_VERSION = "2021-02-01";

// @public (undocumented)
export interface INativeBrokerPlugin {
Expand Down
26 changes: 16 additions & 10 deletions lib/msal-common/src/authority/RegionDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { INetworkModule } from "../network/INetworkModule.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { IMDSBadResponse } from "../response/IMDSBadResponse.js";
import { ImdsComputeResponse } from "../response/ImdsComputeResponse.js";
import * as Constants from "../utils/Constants.js";
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata.js";
import { ImdsOptions } from "./ImdsOptions.js";
Expand Down Expand Up @@ -69,9 +70,12 @@ export class RegionDiscovery {
if (
localIMDSVersionResponse.status === Constants.HTTP_SUCCESS
) {
autodetectedRegionName = localIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
autodetectedRegionName =
localIMDSVersionResponse.body?.location;
if (autodetectedRegionName) {
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
}
}

// If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry.
Expand Down Expand Up @@ -104,9 +108,11 @@ export class RegionDiscovery {
Constants.HTTP_SUCCESS
) {
autodetectedRegionName =
currentIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
currentIMDSVersionResponse.body?.location;
if (autodetectedRegionName) {
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
}
}
}
} catch (e) {
Expand All @@ -132,14 +138,14 @@ export class RegionDiscovery {
* Make the call to the IMDS endpoint
*
* @param imdsEndpointUrl
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
* @returns Promise<NetworkResponse<string>>
* @returns Promise<NetworkResponse<ImdsComputeResponse>>
*/
Comment thread
Robbie-Microsoft marked this conversation as resolved.
private async getRegionFromIMDS(
version: string,
options: ImdsOptions
): Promise<NetworkResponse<string>> {
return this.networkInterface.sendGetRequestAsync<string>(
`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`,
): Promise<NetworkResponse<ImdsComputeResponse>> {
return this.networkInterface.sendGetRequestAsync<ImdsComputeResponse>(
`${Constants.IMDS_ENDPOINT}?api-version=${version}`,
options,
Constants.IMDS_TIMEOUT
);
Expand Down
13 changes: 13 additions & 0 deletions lib/msal-common/src/response/ImdsComputeResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Response body returned by the IMDS compute metadata endpoint
* (http://169.254.169.254/metadata/instance/compute). Only the `location`
* field is used for region auto-discovery.
*/
export type ImdsComputeResponse = {
location: string;
};
Comment thread
Robbie-Microsoft marked this conversation as resolved.
5 changes: 2 additions & 3 deletions lib/msal-common/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ export const AUTHORIZATION_PENDING = "authorization_pending";
export const NOT_APPLICABLE = "N/A";
export const NOT_AVAILABLE = "Not Available";
export const FORWARD_SLASH = "/";
export const IMDS_ENDPOINT =
"http://169.254.169.254/metadata/instance/compute/location";
export const IMDS_VERSION = "2020-06-01";
export const IMDS_ENDPOINT = "http://169.254.169.254/metadata/instance/compute";
export const IMDS_VERSION = "2021-02-01";
export const IMDS_TIMEOUT = 2000;
export const AZURE_REGION_AUTO_DISCOVER_FLAG = "TryAutoDetect";
export const REGIONAL_AUTH_PUBLIC_CLOUD_SUFFIX = "login.microsoft.com";
Expand Down
183 changes: 183 additions & 0 deletions lib/msal-common/test/authority/Authority.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
UrlString,
} from "../../src/index.js";
import { RegionDiscovery } from "../../src/authority/RegionDiscovery.js";
import { RegionDiscoveryMetadata } from "../../src/authority/RegionDiscoveryMetadata.js";
import { InstanceDiscoveryMetadata } from "../../src/authority/AuthorityMetadata.js";
import * as authorityMetadata from "../../src/authority/AuthorityMetadata.js";
import { getDefaultErrorMessage } from "../../src/error/AuthError.js";
Expand Down Expand Up @@ -925,6 +926,188 @@ describe("Authority.ts Class Unit Tests", () => {
});
});

describe("RegionDiscovery IMDS compute endpoint", () => {
const correlationId = TEST_CONFIG.CORRELATION_ID;

const buildNetworkInterface = (
getImpl: (url: string, options?: NetworkRequestOptions) => unknown
): INetworkModule => ({
sendGetRequestAsync: <T>(
url: string,
options?: NetworkRequestOptions
): Promise<T> => Promise.resolve(getImpl(url, options) as T),
sendPostRequestAsync: <T>(): Promise<T> => Promise.resolve({} as T),
});

it("calls the IMDS /compute JSON endpoint and reads the location field", async () => {
let requestedUrl = "";
const networkInterface = buildNetworkInterface((url) => {
requestedUrl = url;
return {
status: Constants.HTTP_SUCCESS,
body: { location: "centralus" },
};
});

const regionDiscovery = new RegionDiscovery(
networkInterface,
logger,
new StubPerformanceClient(),
correlationId
);
const regionDiscoveryMetadata: RegionDiscoveryMetadata = {};

const region = await regionDiscovery.detectRegion(
undefined,
regionDiscoveryMetadata
);

expect(region).toBe("centralus");
expect(requestedUrl).toBe(
`${Constants.IMDS_ENDPOINT}?api-version=${Constants.IMDS_VERSION}`
);
expect(requestedUrl).toContain(
"metadata/instance/compute?api-version=2021-02-01"
);
expect(requestedUrl).not.toContain("format=text");
expect(requestedUrl).not.toContain("/compute/location");
expect(regionDiscoveryMetadata.region_source).toBe(
Constants.RegionDiscoverySources.IMDS
);
});

it("falls back to FAILED_AUTO_DETECTION when the location field is missing", async () => {
const networkInterface = buildNetworkInterface(() => ({
status: Constants.HTTP_SUCCESS,
body: { vmId: "11111111-1111-1111-1111-111111111111" },
}));

const regionDiscovery = new RegionDiscovery(
networkInterface,
logger,
new StubPerformanceClient(),
correlationId
);
const regionDiscoveryMetadata: RegionDiscoveryMetadata = {};

const region = await regionDiscovery.detectRegion(
undefined,
regionDiscoveryMetadata
);

expect(region).toBeNull();
expect(regionDiscoveryMetadata.region_source).toBe(
Constants.RegionDiscoverySources.FAILED_AUTO_DETECTION
);
});

it("falls back to FAILED_AUTO_DETECTION when the location field is null", async () => {
const networkInterface = buildNetworkInterface(() => ({
status: Constants.HTTP_SUCCESS,
body: { location: null },
}));

const regionDiscovery = new RegionDiscovery(
networkInterface,
logger,
new StubPerformanceClient(),
correlationId
);
const regionDiscoveryMetadata: RegionDiscoveryMetadata = {};

const region = await regionDiscovery.detectRegion(
undefined,
regionDiscoveryMetadata
);

expect(region).toBeNull();
expect(regionDiscoveryMetadata.region_source).toBe(
Constants.RegionDiscoverySources.FAILED_AUTO_DETECTION
);
});

it("falls back to FAILED_AUTO_DETECTION when the IMDS body is malformed JSON", async () => {
// The network client throws while parsing a malformed JSON body.
const networkInterface = buildNetworkInterface(() => {
throw new Error("Failed to parse response");
});

const regionDiscovery = new RegionDiscovery(
networkInterface,
logger,
new StubPerformanceClient(),
correlationId
);
const regionDiscoveryMetadata: RegionDiscoveryMetadata = {};

const region = await regionDiscovery.detectRegion(
undefined,
regionDiscoveryMetadata
);

expect(region).toBeNull();
expect(regionDiscoveryMetadata.region_source).toBe(
Constants.RegionDiscoverySources.FAILED_AUTO_DETECTION
);
});

it("negotiates a new api-version on 400 then reads location from the retry", async () => {
const requestedUrls: string[] = [];
const networkInterface = buildNetworkInterface((url) => {
requestedUrls.push(url);

// Probe for supported versions (no api-version param).
if (url === `${Constants.IMDS_ENDPOINT}?format=json`) {
return {
status: Constants.HTTP_BAD_REQUEST,
body: {
error: "invalid",
"newest-versions": ["2020-10-01"],
},
};
}

// First call with the default api-version is rejected.
if (
url ===
`${Constants.IMDS_ENDPOINT}?api-version=${Constants.IMDS_VERSION}`
) {
return { status: Constants.HTTP_BAD_REQUEST, body: {} };
}

// Retry with the negotiated api-version succeeds.
return {
status: Constants.HTTP_SUCCESS,
body: { location: "centralus" },
};
});

const regionDiscovery = new RegionDiscovery(
networkInterface,
logger,
new StubPerformanceClient(),
correlationId
);
const regionDiscoveryMetadata: RegionDiscoveryMetadata = {};

const region = await regionDiscovery.detectRegion(
undefined,
regionDiscoveryMetadata
);

expect(region).toBe("centralus");
expect(regionDiscoveryMetadata.region_source).toBe(
Constants.RegionDiscoverySources.IMDS
);
expect(requestedUrls).toContain(
`${Constants.IMDS_ENDPOINT}?api-version=2020-10-01`
);
requestedUrls.forEach((url) => {
expect(url).not.toContain("format=text");
});
});
});

describe("Endpoint discovery", () => {
const networkInterface: INetworkModule = {
sendGetRequestAsync<T>(
Expand Down
Loading