Implement AZURE_TOKEN_CREDENTIALS (#6593)

* Implement AZURE_TOKEN_CREDENTIALS

* Clang-format

* Update sdk/identity/azure-identity/src/default_azure_credential.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update sdk/identity/azure-identity/src/default_azure_credential.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

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

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

* TrimString()

* Test against contains() or startsWith() being used

* Update sdk/identity/azure-identity/src/default_azure_credential.cpp

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

* TrimString() => StringExtensions::Trim()

---------

Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>
This commit is contained in:
Anton Kolesnyk 2025-05-27 16:40:16 -07:00 committed by GitHub
parent 2d5514fa8a
commit 47c738bba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 249 additions and 127 deletions

View File

@ -119,6 +119,13 @@ namespace Azure { namespace Core { namespace _internal {
return result;
}
static std::string Trim(std::string s)
{
s.erase(s.begin(), std::find_if_not(s.begin(), s.end(), IsSpace));
s.erase(std::find_if_not(s.rbegin(), s.rend(), IsSpace).base(), s.end());
return s;
}
};
}}} // namespace Azure::Core::_internal

View File

@ -128,20 +128,6 @@ std::string GetOSVersion()
return osVersionInfo.str();
}
std::string TrimString(std::string s)
{
s.erase(
s.begin(),
std::find_if_not(s.begin(), s.end(), Azure::Core::_internal::StringExtensions::IsSpace));
s.erase(
std::find_if_not(s.rbegin(), s.rend(), Azure::Core::_internal::StringExtensions::IsSpace)
.base(),
s.end());
return s;
}
} // namespace
namespace Azure { namespace Core { namespace Http { namespace _internal {
@ -152,12 +138,14 @@ namespace Azure { namespace Core { namespace Http { namespace _internal {
std::string const& applicationId,
long cplusplusValue)
{
using Azure::Core::_internal::StringExtensions;
// Spec: https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy
std::ostringstream telemetryId;
if (!applicationId.empty())
{
telemetryId << TrimString(applicationId).substr(0, 24) << " ";
telemetryId << StringExtensions::Trim(applicationId).substr(0, 24) << " ";
}
static std::string const osVer = GetOSVersion();

View File

@ -4,6 +4,8 @@
### Features Added
- Added support for the `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential`, which allows for choosing between 'deployed service' and 'developer tool' credentials. Valid values are 'dev' for developer tools and 'prod' for deployed service.
### Breaking Changes
### Bugs Fixed

View File

@ -10,10 +10,15 @@
#include "private/chained_token_credential_impl.hpp"
#include "private/identity_log.hpp"
#include <azure/core/internal/environment.hpp>
#include <azure/core/internal/strings.hpp>
using namespace Azure::Identity;
using namespace Azure::Core::Credentials;
using Azure::Core::Context;
using Azure::Core::_internal::Environment;
using Azure::Core::_internal::StringExtensions;
using Azure::Core::Diagnostics::Logger;
using Azure::Identity::_detail::IdentityLog;
@ -38,17 +43,49 @@ DefaultAzureCredential::DefaultAzureCredential(
"is the better fit for the application.");
// Creating credentials in order to ensure the order of log messages.
auto const envCred = std::make_shared<EnvironmentCredential>(options);
auto const wiCred = std::make_shared<WorkloadIdentityCredential>(options);
auto const azCliCred = std::make_shared<AzureCliCredential>(options);
auto const managedIdentityCred = std::make_shared<ManagedIdentityCredential>(options);
ChainedTokenCredential::Sources miSources;
{
miSources.emplace_back(std::make_shared<EnvironmentCredential>(options));
miSources.emplace_back(std::make_shared<WorkloadIdentityCredential>(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)
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
}
else if (
trimmedEnvVarValue.empty()
|| StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev"))
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
miSources.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.");
}
miSources.emplace_back(std::make_shared<ManagedIdentityCredential>(options));
}
// DefaultAzureCredential caches the selected credential, so that it can be reused on subsequent
// calls.
m_impl = std::make_unique<_detail::ChainedTokenCredentialImpl>(
GetCredentialName(),
ChainedTokenCredential::Sources{envCred, wiCred, azCliCred, managedIdentityCred},
true);
GetCredentialName(), std::move(miSources), true);
}
DefaultAzureCredential::~DefaultAzureCredential() = default;

View File

@ -138,131 +138,219 @@ TEST(DefaultAzureCredential, CachingCredential)
EXPECT_TRUE(c2->WasInvoked);
}
TEST(DefaultAzureCredential, LogMessages)
class LogMessages : public ::testing::TestWithParam<std::string> {
};
INSTANTIATE_TEST_SUITE_P(
DefaultAzureCredential,
LogMessages,
::testing::Values(
"",
" ",
"dev",
"DeV",
"dEv ",
" DEV ",
"prod",
"pRoD",
" PrOd",
"d ev",
"production"));
TEST_P(LogMessages, )
{
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)); });
const auto azTokenCredsEnvVarValue = GetParam();
if (azTokenCredsEnvVarValue == "d ev" || azTokenCredsEnvVarValue == "production")
{
CredentialTestHelper::EnvironmentOverride const env(
{{"AZURE_TOKEN_CREDENTIALS", azTokenCredsEnvVarValue}});
CredentialTestHelper::SimulateTokenRequest(
[&](auto transport) {
TokenCredentialOptions options;
options.Transport.Transport = transport;
EXPECT_THROW(
static_cast<void>(std::make_unique<DefaultAzureCredential>()), AuthenticationException);
}
else
{
const auto isDev = azTokenCredsEnvVarValue == "dev" || azTokenCredsEnvVarValue == "DeV"
|| azTokenCredsEnvVarValue == "dEv " || azTokenCredsEnvVarValue == " DEV "
|| azTokenCredsEnvVarValue == "" || azTokenCredsEnvVarValue == " ";
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", ""},
});
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)); });
auto credential = std::make_unique<DefaultAzureCredential>(options);
try
{
CredentialTestHelper::SimulateTokenRequest(
[&](auto transport) {
TokenCredentialOptions options;
options.Transport.Transport = transport;
EXPECT_EQ(log.size(), LogMsgVec::size_type(11));
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", azTokenCredsEnvVarValue},
});
EXPECT_EQ(log[0].first, Logger::Level::Verbose);
EXPECT_EQ(
log[0].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.");
auto credential = std::make_unique<DefaultAzureCredential>(options);
EXPECT_EQ(log[1].first, Logger::Level::Informational);
EXPECT_EQ(
log[1].second,
"Identity: EnvironmentCredential gets created with ClientSecretCredential.");
EXPECT_EQ(log.size(), LogMsgVec::size_type(isDev ? 12 : 11));
LogMsgVec::size_type i = 0;
EXPECT_EQ(log[2].first, Logger::Level::Verbose);
EXPECT_EQ(
log[2].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.");
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.");
EXPECT_EQ(log[3].first, Logger::Level::Informational);
EXPECT_EQ(log[3].second, "Identity: WorkloadIdentityCredential was created successfully.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
"Identity: EnvironmentCredential gets created with ClientSecretCredential.");
EXPECT_EQ(log[5].first, Logger::Level::Verbose);
EXPECT_EQ(
log[5].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: 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.");
EXPECT_EQ(log[6].first, Logger::Level::Verbose);
EXPECT_EQ(
log[6].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::Informational);
EXPECT_EQ(
log.at(i).second, "Identity: WorkloadIdentityCredential was created successfully.");
EXPECT_EQ(log[7].first, Logger::Level::Verbose);
EXPECT_EQ(
log[7].second,
"Identity: ManagedIdentityCredential: Environment is not set up for the credential "
"to be created with Cloud Shell source.");
{
const auto variableSetWording = azTokenCredsEnvVarValue.empty()
? "not set"
: ("set to '" + azTokenCredsEnvVarValue + "'");
EXPECT_EQ(log[8].first, Logger::Level::Verbose);
EXPECT_EQ(
log[8].second,
"Identity: ManagedIdentityCredential: Environment is not set up for the credential "
"to be created with Azure Arc source.");
const auto beIncludedWording = isDev ? "" : "NOT ";
EXPECT_EQ(log[9].first, Logger::Level::Informational);
EXPECT_EQ(
log[9].second,
"Identity: ManagedIdentityCredential will be created "
"with Azure Instance Metadata Service source."
"\nSuccessful creation does not guarantee further successful token retrieval.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: DefaultAzureCredential: "
"'AZURE_TOKEN_CREDENTIALS' environment variable is "
+ variableSetWording + ", therefore AzureCliCredential will "
+ beIncludedWording + "be included in the credential chain.");
}
EXPECT_EQ(log[4].first, Logger::Level::Informational);
EXPECT_EQ(
log[4].second,
"Identity: AzureCliCredential created."
"\nSuccessful creation does not guarantee further successful token retrieval.");
if (isDev)
{
++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.");
}
EXPECT_EQ(log[10].first, Logger::Level::Informational);
EXPECT_EQ(
log[10].second,
"Identity: DefaultAzureCredential: Created with the following credentials: "
"EnvironmentCredential, WorkloadIdentityCredential, AzureCliCredential, "
"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.");
log.clear();
++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.");
return credential;
},
{{"https://azure.com/.default"}},
{"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"});
++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.");
EXPECT_EQ(
log.size(),
LogMsgVec::size_type(5)); // Request and retry policies will get their messages here as well.
++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.");
EXPECT_EQ(log[3].first, Logger::Level::Informational);
EXPECT_EQ(
log[3].second,
"Identity: DefaultAzureCredential: Successfully got token from EnvironmentCredential. This "
"credential will be reused for subsequent calls.");
++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.");
EXPECT_EQ(log[4].first, Logger::Level::Verbose);
EXPECT_EQ(
log[4].second,
"Identity: DefaultAzureCredential: Saved this credential at index 0 for subsequent calls.");
++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: "
"EnvironmentCredential, WorkloadIdentityCredential, ")
+ (isDev ? "AzureCliCredential, " : "") + "ManagedIdentityCredential.");
Logger::SetListener(nullptr);
++i;
EXPECT_EQ(i, log.size());
log.clear();
return credential;
},
{{"https://azure.com/.default"}},
{"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"});
EXPECT_EQ(
log.size(),
LogMsgVec::size_type(
5)); // Request and retry policies will get their messages here as well.
LogMsgVec::size_type i = 3;
EXPECT_EQ(log.at(i).first, Logger::Level::Informational);
EXPECT_EQ(
log.at(i).second,
"Identity: DefaultAzureCredential: Successfully got token from EnvironmentCredential. "
"This credential will be reused for subsequent calls.");
++i;
EXPECT_EQ(log.at(i).first, Logger::Level::Verbose);
EXPECT_EQ(
log.at(i).second,
"Identity: DefaultAzureCredential: "
"Saved this credential at index 0 for subsequent calls.");
++i;
EXPECT_EQ(i, log.size());
}
catch (...)
{
Logger::SetListener(nullptr);
throw;
}
Logger::SetListener(nullptr);
}
}