From ffc76ed86c95199b7fb8f01cdf297071e2b24f39 Mon Sep 17 00:00:00 2001 From: Hariharan Sethuraman Date: Fri, 30 Oct 2026 11:16:10 +0000 Subject: [PATCH] Honor AZURE_CLIENT_ID in DefaultAzureCredential managed identity Use the environment client ID for managed identity selection in the default credential chain while preserving standalone ManagedIdentityCredential behavior. Add selector regression tests and document the compatibility change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 11d8863b-ee0a-4f8e-94c4-e63edc82546b Signed-off-by: Hariharan Sethuraman --- sdk/identity/azure-identity/CHANGELOG.md | 6 ++ sdk/identity/azure-identity/README.md | 12 +++ .../identity/default_azure_credential.hpp | 5 + .../src/default_azure_credential.cpp | 4 +- .../test/ut/default_azure_credential_test.cpp | 100 +++++++++++++++++- 5 files changed, 123 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 31e761f270..2bb9628e72 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,8 +4,14 @@ ### Features Added +- `DefaultAzureCredential` uses `AZURE_CLIENT_ID` to select a user-assigned managed identity. + ### Breaking Changes +- When `AZURE_CLIENT_ID` is set, the managed identity credential in `DefaultAzureCredential` + now selects that identity instead of sending no identity selector. Standalone + `ManagedIdentityCredential` constructors are unchanged. + ### Bugs Fixed ### Other Changes diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 5f69162ed7..ba5b939512 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -98,6 +98,18 @@ The [Managed identity authentication](https://learn.microsoft.com/entra/identity * [Azure Arc](https://learn.microsoft.com/azure/azure-arc/servers/managed-identity-authentication) * [Azure Virtual Machines](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token) +### Specify a user-assigned managed identity with `DefaultAzureCredential` + +Set `AZURE_CLIENT_ID` to the client ID of a user-assigned managed identity +attached to the Azure host. `DefaultAzureCredential` passes this value to the +managed identity credential in its chain. An unset or empty value preserves +the existing behavior of sending no identity selector. + +This also applies when `AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential`. +It does not change credential ordering or configure credentials constructed +directly with `ManagedIdentityCredential`; those still require explicit +identity selection as shown below. + ### Specify a user-assigned managed identity with `ManagedIdentityCredential` Many Azure hosts allow the assignment of a user-assigned managed identity. The following examples demonstrate configuring `ManagedIdentityCredential` to authenticate a user-assigned managed identity when deployed to an Azure host. The sample code uses the credential to authenticate a `BlobClient` from the [Azure::Storage::Blobs][blobs_client_library] client library. It also demonstrates how you can specify a user-assigned managed identity either by a client ID, resource ID, or an object ID. diff --git a/sdk/identity/azure-identity/inc/azure/identity/default_azure_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/default_azure_credential.hpp index 778e19fbe5..39655ab34e 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/default_azure_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/default_azure_credential.hpp @@ -34,6 +34,11 @@ namespace Azure { namespace Identity { * though the credentials being used and their order is documented, it may be changed in the * future versions of the SDK, potentially introducing breaking changes in its behavior. * + * @details The managed identity credential in the chain uses the client ID from the + * `AZURE_CLIENT_ID` environment variable when it is non-empty. Otherwise, it does not specify + * an identity selector. This does not change standalone + * #Azure::Identity::ManagedIdentityCredential constructors. + * * @note This credential is intended to be used at the early stages of development, to allow the * developer some time to work with the other aspects of the SDK, and later to replace this * credential with the exact credential that is the best fit for the application. It is not diff --git a/sdk/identity/azure-identity/src/default_azure_credential.cpp b/sdk/identity/azure-identity/src/default_azure_credential.cpp index 00e2a6360a..be777c8c41 100644 --- a/sdk/identity/azure-identity/src/default_azure_credential.cpp +++ b/sdk/identity/azure-identity/src/default_azure_credential.cpp @@ -98,8 +98,8 @@ DefaultAzureCredential::DefaultAzureCredential( true, "ManagedIdentityCredential", [&specificCred](auto options) { - return std::shared_ptr( - new ManagedIdentityCredential({}, !specificCred, options)); + return std::shared_ptr(new ManagedIdentityCredential( + Environment::GetVariable("AZURE_CLIENT_ID"), !specificCred, options)); }}, CredentialInfo{ false, diff --git a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp index 5043fd9ea4..f825e1c99b 100644 --- a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp @@ -2,9 +2,11 @@ // Licensed under the MIT License. #include "azure/identity/default_azure_credential.hpp" +#include "azure/identity/managed_identity_credential.hpp" #include "credential_test_helper.hpp" #include +#include #include <../src/private/chained_token_credential_impl.hpp> #include @@ -274,7 +276,8 @@ TEST_P(LogMessages, ) EXPECT_EQ( log.at(i).second, "Identity: ManagedIdentityCredential will be created " - "with Azure Instance Metadata Service source." + "with Azure Instance Metadata Service source " + "and Client ID 'fedcba98-7654-3210-0123-456789abcdef'." "\nSuccessful creation does not guarantee further successful token retrieval."); { @@ -488,7 +491,8 @@ TEST_P(LogMessagesForSpecificCredential, ) EXPECT_EQ( log.at(i).second, "Identity: ManagedIdentityCredential will be created " - "with Azure Instance Metadata Service source." + "with Azure Instance Metadata Service source " + "and Client ID 'fedcba98-7654-3210-0123-456789abcdef'." "\nSuccessful creation does not guarantee further successful token retrieval."); } else if (specificCredentialInfo.CredentialName == "AzureCliCredential") @@ -529,6 +533,98 @@ TEST(DefaultAzureCredential, RequireCredentialSpecifierEnvVarValue) static_cast(std::make_unique(true)), AuthenticationException); } +TEST(DefaultAzureCredential, ManagedIdentityClientIdFromEnvironment) +{ + for (std::string const credentialSpecifier : {"", "prod", "ManagedIdentityCredential"}) + { + for (std::string const clientId : {"", "fedcba98-7654-3210-0123-456789abcdef"}) + { + SCOPED_TRACE(credentialSpecifier + ": " + clientId); + CredentialTestHelper::EnvironmentOverride const env({ + {"AZURE_TOKEN_CREDENTIALS", credentialSpecifier}, + {"AZURE_CLIENT_ID", clientId}, + {"AZURE_TENANT_ID", ""}, + {"AZURE_CLIENT_SECRET", ""}, + {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, + {"AZURE_FEDERATED_TOKEN_FILE", ""}, + {"AZURE_AUTHORITY_HOST", ""}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IMDS_ENDPOINT", ""}, + }); + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + return std::make_unique(options); + }, + {{"https://storage.azure.com/.default"}}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN\"}"}); + + ASSERT_EQ(actual.Requests.size(), 1U); + auto const query = Azure::Core::Url(actual.Requests.at(0).AbsoluteUrl).GetQueryParameters(); + if (clientId.empty()) + { + EXPECT_EQ(query.count("client_id"), 0U); + } + else + { + ASSERT_EQ(query.count("client_id"), 1U); + EXPECT_EQ(query.at("client_id"), clientId); + } + ASSERT_EQ(actual.Responses.size(), 1U); + EXPECT_EQ(actual.Responses.at(0).AccessToken.Token, "ACCESSTOKEN"); + } + } +} + +TEST(DefaultAzureCredential, StandaloneManagedIdentityIgnoresClientIdEnvironment) +{ + CredentialTestHelper::EnvironmentOverride const env({ + {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IMDS_ENDPOINT", ""}, + }); + + for (std::string const explicitClientId : {"", "01234567-89ab-cdef-fedc-ba8976543210"}) + { + SCOPED_TRACE(explicitClientId); + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [&](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + if (explicitClientId.empty()) + { + return std::make_unique(options); + } + return std::make_unique( + explicitClientId, options); + }, + {{"https://storage.azure.com/.default"}}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN\"}"}); + + ASSERT_EQ(actual.Requests.size(), 1U); + auto const query = Azure::Core::Url(actual.Requests.at(0).AbsoluteUrl).GetQueryParameters(); + if (explicitClientId.empty()) + { + EXPECT_EQ(query.count("client_id"), 0U); + } + else + { + ASSERT_EQ(query.count("client_id"), 1U); + EXPECT_EQ(query.at("client_id"), explicitClientId); + } + } +} + TEST(DefaultAzureCredential, ImdsProbe) { using Azure::Core::Http::HttpStatusCode;