Identity: 'AZURE_TOKEN_CREDENTIALS' env var support for specific credential names (#6634)

* Identity: 'AZURE_TOKEN_CREDENTIALS' env var support for specific credential names

* No need to use index-based loop at that place

* Clang-format

* Require 'Credential' suffix for env var values

* Update sdk/identity/azure-identity/CHANGELOG.md

Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>

---------

Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com>
Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>
This commit is contained in:
Anton Kolesnyk 2025-07-10 12:10:03 -07:00 committed by GitHub
parent 970b619b28
commit 65508720fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 299 additions and 25 deletions

View File

@ -4,6 +4,8 @@
### Features Added
- Added support for more `AZURE_TOKEN_CREDENTIALS` environment variable values to specify a single credential type to use in `DefaultAzureCredential`. In addition to `dev` and `prod`, possible values now include `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, and `AzureCliCredential` - each for the corresponding credential type.
### Breaking Changes
### Bugs Fixed

View File

@ -13,6 +13,10 @@
#include <azure/core/internal/environment.hpp>
#include <azure/core/internal/strings.hpp>
#include <array>
#include <functional>
#include <type_traits>
using namespace Azure::Identity;
using namespace Azure::Core::Credentials;
@ -45,39 +49,139 @@ DefaultAzureCredential::DefaultAzureCredential(
// Creating credentials in order to ensure the order of log messages.
ChainedTokenCredential::Sources credentialChain;
{
credentialChain.emplace_back(std::make_shared<EnvironmentCredential>(options));
credentialChain.emplace_back(std::make_shared<WorkloadIdentityCredential>(options));
credentialChain.emplace_back(std::make_shared<ManagedIdentityCredential>(options));
struct CredentialInfo
{
bool IsProd;
std::string CredentialName;
std::function<std::shared_ptr<Core::Credentials::TokenCredential>(
const Core::Credentials::TokenCredentialOptions&)>
Create;
};
static const std::array<CredentialInfo, 4> credentials = {
CredentialInfo{
true,
"EnvironmentCredential",
[](auto options) { return std::make_shared<EnvironmentCredential>(options); }},
CredentialInfo{
true,
"WorkloadIdentityCredential",
[](auto options) { return std::make_shared<WorkloadIdentityCredential>(options); }},
CredentialInfo{
true,
"ManagedIdentityCredential",
[](auto options) { return std::make_shared<ManagedIdentityCredential>(options); }},
CredentialInfo{
false,
"AzureCliCredential",
[](auto options) { return std::make_shared<AzureCliCredential>(options); }},
};
constexpr auto envVarName = "AZURE_TOKEN_CREDENTIALS";
const auto envVarValue = Environment::GetVariable(envVarName);
const auto trimmedEnvVarValue = StringExtensions::Trim(envVarValue);
const auto isProd
= StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "prod");
const auto logMsg = GetCredentialName() + ": '" + envVarName + "' environment variable is "
+ (envVarValue.empty() ? "not set" : ("set to '" + envVarValue + "'"))
+ ", therefore AzureCliCredential will " + (isProd ? "NOT " : "")
+ "be included in the credential chain.";
if (isProd)
bool specificCred = false;
if (!trimmedEnvVarValue.empty())
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
for (const auto& cred : credentials)
{
if (StringExtensions::LocaleInvariantCaseInsensitiveEqual(
trimmedEnvVarValue, cred.CredentialName))
{
specificCred = true;
IdentityLog::Write(
IdentityLog::Level::Verbose,
GetCredentialName() + ": '" + envVarName + "' environment variable is set to '"
+ envVarValue
+ "', therefore credential chain will only contain single credential: "
+ cred.CredentialName + '.');
credentialChain.emplace_back(cred.Create(options));
break;
}
}
}
else if (
trimmedEnvVarValue.empty()
|| StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev"))
if (!specificCred)
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
credentialChain.emplace_back(std::make_shared<AzureCliCredential>(options));
}
else
{
throw AuthenticationException(
GetCredentialName() + ": Invalid value '" + envVarValue + "' for the '" + envVarName
+ "' environment variable. Allowed values are 'dev' and 'prod' (case insensitive). "
"It is also valid to not have the environment variable defined.");
for (const auto& cred : credentials)
{
if (cred.IsProd)
{
credentialChain.emplace_back(cred.Create(options));
}
}
const auto isProd
= StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "prod");
static const auto devCredCount = std::count_if(
credentials.begin(), credentials.end(), [](auto& cred) { return !cred.IsProd; });
std::string devCredNames;
{
std::remove_const<decltype(devCredCount)>::type devCredNum = 0;
for (const auto& cred : credentials)
{
if (!cred.IsProd)
{
if (!devCredNames.empty())
{
if (devCredCount == 2)
{
devCredNames += " and ";
}
else
{
++devCredNum;
devCredNames += (devCredNum < devCredCount) ? ", " : ", and ";
}
}
devCredNames += cred.CredentialName;
}
}
}
const auto logMsg = GetCredentialName() + ": '" + envVarName + "' environment variable is "
+ (envVarValue.empty() ? "not set" : ("set to '" + envVarValue + "'"))
+ ((devCredCount > 0)
? (", therefore " + devCredNames + " will " + (isProd ? "NOT " : "")
+ "be included in the credential chain.")
: ".");
if (isProd)
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
}
else if (
trimmedEnvVarValue.empty()
|| StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev"))
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
for (const auto& cred : credentials)
{
if (!cred.IsProd)
{
credentialChain.emplace_back(cred.Create(options));
}
}
}
else
{
std::string allowedCredNames;
for (std::size_t i = 0; i < credentials.size(); ++i)
{
allowedCredNames += ((i < credentials.size() - 1) ? ", '" : ", and '")
+ credentials[i].CredentialName + '\'';
}
throw AuthenticationException(
GetCredentialName() + ": Invalid value '" + envVarValue + "' for the '" + envVarName
+ "' environment variable. Allowed values are 'dev', 'prod'" + allowedCredNames
+ " (case insensitive). "
"It is also valid to not have the environment variable defined.");
}
}
}

View File

@ -354,3 +354,171 @@ TEST_P(LogMessages, )
Logger::SetListener(nullptr);
}
}
struct SpecificCredentialInfo
{
std::string CredentialName;
std::string EnvVarValue;
size_t ExpectedLogMsgCount;
};
class LogMessagesForSpecificCredential : public ::testing::TestWithParam<SpecificCredentialInfo> {
};
INSTANTIATE_TEST_SUITE_P(
DefaultAzureCredential,
LogMessagesForSpecificCredential,
::testing::Values(
SpecificCredentialInfo{"EnvironmentCredential", "eNvIrOnMeNtCrEdEnTiAl", 5},
SpecificCredentialInfo{"WorkloadIdentityCredential", "workloadidentitycredential", 4},
SpecificCredentialInfo{"ManagedIdentityCredential", "MANAGEDIDENTITYCREDENTIAL", 8},
SpecificCredentialInfo{"AzureCliCredential", " AzureCLICredential ", 4}));
TEST_P(LogMessagesForSpecificCredential, )
{
const auto specificCredentialInfo = GetParam();
using LogMsgVec = std::vector<std::pair<Logger::Level, std::string>>;
LogMsgVec log;
Logger::SetLevel(Logger::Level::Verbose);
Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); });
try
{
CredentialTestHelper::EnvironmentOverride const env({
{"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"},
{"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"},
{"AZURE_CLIENT_SECRET", "CLIENTSECRET"},
{"AZURE_AUTHORITY_HOST", "https://microsoft.com/"},
{"AZURE_FEDERATED_TOKEN_FILE", "azure-identity-test.pem"},
{"AZURE_USERNAME", ""},
{"AZURE_PASSWORD", ""},
{"AZURE_CLIENT_CERTIFICATE_PATH", ""},
{"MSI_ENDPOINT", ""},
{"MSI_SECRET", ""},
{"IDENTITY_ENDPOINT", ""},
{"IMDS_ENDPOINT", ""},
{"IDENTITY_HEADER", ""},
{"IDENTITY_SERVER_THUMBPRINT", ""},
{"AZURE_TOKEN_CREDENTIALS", specificCredentialInfo.EnvVarValue},
});
static_cast<void>(std::make_unique<DefaultAzureCredential>());
EXPECT_EQ(log.size(), specificCredentialInfo.ExpectedLogMsgCount);
LogMsgVec::size_type i = 0;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: Creating DefaultAzureCredential which combines "
"multiple parameterless credentials into a single one."
"\nDefaultAzureCredential is only recommended for the early stages of development, "
"and not for usage in production environment."
"\nOnce the developer focuses on the Credentials "
"and Authentication aspects of their application, "
"DefaultAzureCredential needs to be replaced with the credential that "
"is the better fit for the application.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: DefaultAzureCredential: "
"'AZURE_TOKEN_CREDENTIALS' environment variable is set to '"
+ specificCredentialInfo.EnvVarValue
+ "', therefore credential chain will only contain single credential: "
+ specificCredentialInfo.CredentialName + '.');
if (specificCredentialInfo.CredentialName == "EnvironmentCredential")
{
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
"Identity: EnvironmentCredential gets created with ClientSecretCredential.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', "
"'AZURE_CLIENT_SECRET', and 'AZURE_AUTHORITY_HOST' environment variables "
"are set, so ClientSecretCredential with corresponding "
"tenantId, clientId, clientSecret, and authorityHost gets created.");
}
else if (specificCredentialInfo.CredentialName == "WorkloadIdentityCredential")
{
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(log.at(i).second, "Identity: WorkloadIdentityCredential was created successfully.");
}
else if (specificCredentialInfo.CredentialName == "ManagedIdentityCredential")
{
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: ManagedIdentityCredential: Environment is not set up "
"for the credential to be created with App Service 2019 source.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: ManagedIdentityCredential: Environment is not set up "
"for the credential to be created with App Service 2017 source.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: ManagedIdentityCredential: Environment is not set up "
"for the credential to be created with Cloud Shell source.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: ManagedIdentityCredential: Environment is not set up "
"for the credential to be created with Azure Arc source.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
"Identity: ManagedIdentityCredential will be created "
"with Azure Instance Metadata Service source."
"\nSuccessful creation does not guarantee further successful token retrieval.");
}
else if (specificCredentialInfo.CredentialName == "AzureCliCredential")
{
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
"Identity: AzureCliCredential created."
"\nSuccessful creation does not guarantee further successful token retrieval.");
}
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
std::string(
"Identity: DefaultAzureCredential: Created with the following credentials: "
+ specificCredentialInfo.CredentialName + '.'));
++i;
EXPECT_EQ(i, log.size());
log.clear();
}
catch (...)
{
Logger::SetListener(nullptr);
throw;
}
Logger::SetListener(nullptr);
}