Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
154 changes: 129 additions & 25 deletions sdk/identity/azure-identity/src/default_azure_credential.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
}
else if (
trimmedEnvVarValue.empty()
|| StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev"))
bool specificCred = false;
if (!trimmedEnvVarValue.empty())
{
IdentityLog::Write(IdentityLog::Level::Verbose, logMsg);
credentialChain.emplace_back(std::make_shared<AzureCliCredential>(options));
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 (!specificCred)
{
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.");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading