Skip to content

[Engineering task] Support forwarding MSAL client metadata headers through IMDS (classic) to ESTS #899

@gladjohn

Description

@gladjohn

Issue Title: Forward MSAL client metadata headers through IMDS to ESTS

Problem

IMDSv1 managed identity token requests only send Metadata: true. IMDS cannot forward client metadata to ESTS because the headers are never sent. This was fixed in MSAL.NET (PR #5912) and needs to be replicated across all MSAL SDKs.

What to do

Add three HTTP headers to every IMDSv1 managed identity token request:

Header Value Purpose
x-client-SKU SDK product name Identifies the MSAL SDK
x-client-Ver SDK version string Identifies the SDK version
x-ms-client-request-id Correlation GUID Traces the request end-to-end

CRITICAL: Use x-ms-client-request-idNOT client-request-id. IMDS requires the x-ms- prefixed header.

Reference implementation (.NET)

// ImdsManagedIdentitySource.cs — CreateRequestAsync()
request.Headers.Add("Metadata", "true");
request.Headers.Add("x-client-SKU", platformProxy.GetProductName());   // "MSAL.NetCore"
request.Headers.Add("x-client-Ver", MsalIdHelper.GetMsalVersion());   // e.g. "4.82.1"
request.Headers.Add("x-ms-client-request-id", correlationId.ToString());

Implementation per language


MSAL Python (AzureAD/microsoft-authentication-library-for-python)

Source file: msal/managed_identity.py

  • Find _obtain_token_on_azure_vm() where http_client.get(...) is called.
  • Change headers={"Metadata": "true"} to include the three new headers.
  • SKU value: "MSAL.Python" (from msal/sku.pySKU)
  • Version: from msal/sku.py__version__
  • Correlation ID: generate str(uuid.uuid4()) or use the existing correlation ID from the managed identity flow.
from .sku import SKU, __version__
import uuid

headers = {
    "Metadata": "true",
    "x-client-SKU": SKU,
    "x-client-Ver": __version__,
    "x-ms-client-request-id": str(uuid.uuid4()),
}

Test file: tests/test_mi.py

  • Update existing IMDS test assertions to expect the new headers.
  • Use unittest.mock.ANY for version and correlation ID in assert_called_with.
  • Additionally validate the correlation ID is a valid UUID.
  • Test the SKU against a hardcoded constant "MSAL.Python", not by importing SKU from the product.
EXPECTED_SKU = "MSAL.Python"  # hardcoded constant, not imported from product

mock_get.assert_called_with(
    "http://169.254.169.254/metadata/identity/oauth2/token",
    params={"api-version": "2018-02-01", "resource": RESOURCE},
    headers={
        "Metadata": "true",
        "x-client-SKU": EXPECTED_SKU,
        "x-client-Ver": unittest.mock.ANY,
        "x-ms-client-request-id": unittest.mock.ANY,
    },
)

# Validate correlation ID is a valid UUID
corr_id = mock_get.call_args.kwargs["headers"]["x-ms-client-request-id"]
uuid.UUID(corr_id)

MSAL JavaScript (AzureAD/microsoft-authentication-library-for-js)

Source file: lib/msal-node/src/client/ManagedIdentitySources/Imds.ts

  • Find createRequest() where request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true" is set.
  • Add the three new headers after it.
  • SKU value: "@azure/msal-node" (from lib/msal-node/src/packageMetadata.tsname)
  • Version: from packageMetadata.tsversion
  • Correlation ID: from the request context (check ManagedIdentityApplication.ts which generates GUIDs via this.cryptoProvider.createNewGuid()).
import { name, version } from "../../packageMetadata.js";

request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true";
request.headers["x-client-SKU"] = name;
request.headers["x-client-Ver"] = version;
request.headers["x-ms-client-request-id"] = this.correlationId;

Test file: lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts

  • Add header assertions to existing IMDS tests using the sendGetRequestAsyncSpy.
  • Assert SKU against hardcoded "@azure/msal-node" constant.
  • Validate correlation ID matches UUID regex.
const EXPECTED_SKU = "@azure/msal-node";

const headers = sendGetRequestAsyncSpy.mock.lastCall?.[1];
expect(headers["x-client-SKU"]).toBe(EXPECTED_SKU);
expect(headers["x-client-Ver"]).toBeDefined();
expect(headers["x-ms-client-request-id"]).toMatch(
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
);

MSAL Go (AzureAD/microsoft-authentication-library-for-go)

Source file: apps/managedidentity/managedidentity.go

  • Find createIMDSAuthRequest() where req.Header.Set(metaHTTPHeaderName, "true") is set.
  • Add the three new headers after it.
  • SKU value: "MSAL.Go" (hardcoded in apps/internal/oauth/ops/internal/comm/comm.go)
  • Version: from apps/internal/version/version.goversion.Version
  • Correlation ID: thread it through from the caller or generate via uuid.New().String().
import (
    "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version"
    "github.com/google/uuid"
)

req.Header.Set(metaHTTPHeaderName, "true")
req.Header.Set("x-client-SKU", "MSAL.Go")
req.Header.Set("x-client-Ver", version.Version)
req.Header.Set("x-ms-client-request-id", uuid.New().String())

Test file: apps/managedidentity/managedidentity_test.go

  • Add header assertions inside the existing IMDS test callback using WithCallback(func(req *http.Request){...}).
  • Assert SKU against hardcoded "MSAL.Go" constant.
const expectedSKU = "MSAL.Go"

mock.WithCallback(func(req *http.Request) {
    // existing query param assertions...

    if got := req.Header.Get("x-client-SKU"); got != expectedSKU {
        t.Errorf("x-client-SKU = %q, want %q", got, expectedSKU)
    }
    if got := req.Header.Get("x-client-Ver"); got == "" {
        t.Error("x-client-Ver header is empty")
    }
    corrID := req.Header.Get("x-ms-client-request-id")
    if _, err := uuid.Parse(corrID); err != nil {
        t.Errorf("x-ms-client-request-id not a valid UUID: %q", corrID)
    }
})

MSAL Java (AzureAD/microsoft-authentication-library-for-java)

Source file: msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IMDSManagedIdentitySource.java

  • Find where managedIdentityRequest.headers.put("Metadata", "true") is set.
  • Add the three new headers after it.
  • SKU: use HttpHeaders.PRODUCT_HEADER_NAME / HttpHeaders.PRODUCT_HEADER_VALUE ("MSAL.Java")
  • Version: use HttpHeaders.PRODUCT_VERSION_HEADER_NAME / HttpHeaders.PRODUCT_VERSION_HEADER_VALUE
  • Correlation ID: from the request context.
managedIdentityRequest.headers.put("Metadata", "true");
managedIdentityRequest.headers.put(
    HttpHeaders.PRODUCT_HEADER_NAME, HttpHeaders.PRODUCT_HEADER_VALUE);
managedIdentityRequest.headers.put(
    HttpHeaders.PRODUCT_VERSION_HEADER_NAME, HttpHeaders.PRODUCT_VERSION_HEADER_VALUE);
managedIdentityRequest.headers.put(
    "x-ms-client-request-id", requestContext.correlationId());

Note: HttpHeaders.CORRELATION_ID_HEADER_NAME is "client-request-id"do not use it. Use the string "x-ms-client-request-id" directly, or add a new constant.

Test file: msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/ManagedIdentityTests.java

  • Add header assertions to existing IMDS tests using Mockito ArgumentCaptor.
  • Assert SKU against hardcoded "MSAL.Java" constant.
private static final String EXPECTED_SKU = "MSAL.Java";

ArgumentCaptor<HttpRequest> captor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClientMock).send(captor.capture());
HttpRequest req = captor.getValue();
assertEquals(EXPECTED_SKU, req.headers().get("x-client-SKU"));
assertNotNull(req.headers().get("x-client-Ver"));
assertDoesNotThrow(() -> UUID.fromString(req.headers().get("x-ms-client-request-id")));

Rules (from .NET PR review)

  1. Use x-ms-client-request-idnot client-request-id for IMDS.
  2. Test SKU against a hardcoded string constant — never import the product value into tests.
  3. Validate correlation ID is a valid UUID in tests.
  4. Add assertions to existing IMDS tests — do not create new standalone test methods.
  5. Use existing SDK constants for header names and values where available.
  6. No changes to CloudShell, App Service, Azure Arc, or any other managed identity source — IMDS only.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions