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;