Repository navigation
[Engineering task] Support forwarding MSAL client metadata headers through IMDS (classic) to ESTS #899
Description
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-id — NOT 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()wherehttp_client.get(...)is called. - Change
headers={"Metadata": "true"}to include the three new headers. - SKU value:
"MSAL.Python"(frommsal/sku.py→SKU) - 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.ANYfor version and correlation ID inassert_called_with. - Additionally validate the correlation ID is a valid UUID.
- Test the SKU against a hardcoded constant
"MSAL.Python", not by importingSKUfrom 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()whererequest.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"is set. - Add the three new headers after it.
- SKU value:
"@azure/msal-node"(fromlib/msal-node/src/packageMetadata.ts→name) - Version: from
packageMetadata.ts→version - Correlation ID: from the request context (check
ManagedIdentityApplication.tswhich generates GUIDs viathis.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()wherereq.Header.Set(metaHTTPHeaderName, "true")is set. - Add the three new headers after it.
- SKU value:
"MSAL.Go"(hardcoded inapps/internal/oauth/ops/internal/comm/comm.go) - Version: from
apps/internal/version/version.go→version.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)
- Use
x-ms-client-request-id— notclient-request-idfor IMDS. - Test SKU against a hardcoded string constant — never import the product value into tests.
- Validate correlation ID is a valid UUID in tests.
- Add assertions to existing IMDS tests — do not create new standalone test methods.
- Use existing SDK constants for header names and values where available.
- No changes to CloudShell, App Service, Azure Arc, or any other managed identity source — IMDS only.
Activity