diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 73547d2624..e111fc1a0f 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,15 +1,15 @@ # Release History -## 1.5.0-beta.1 (Unreleased) +## 1.5.0-beta.1 (2023-03-07) ### Features Added -### Breaking Changes - -### Bugs Fixed +- Added `DefaultAzureCredential`. ### Other Changes +- Improved log messages. + ## 1.4.0 (2023-02-07) ### Features Added diff --git a/sdk/identity/azure-identity/CMakeLists.txt b/sdk/identity/azure-identity/CMakeLists.txt index 0ea5fe69e1..ab2cd1b6b5 100644 --- a/sdk/identity/azure-identity/CMakeLists.txt +++ b/sdk/identity/azure-identity/CMakeLists.txt @@ -52,6 +52,7 @@ set( inc/azure/identity/chained_token_credential.hpp inc/azure/identity/client_certificate_credential.hpp inc/azure/identity/client_secret_credential.hpp + inc/azure/identity/default_azure_credential.hpp inc/azure/identity/dll_import_export.hpp inc/azure/identity/environment_credential.hpp inc/azure/identity/managed_identity_credential.hpp @@ -69,6 +70,7 @@ set( src/client_certificate_credential.cpp src/client_credential_core.cpp src/client_secret_credential.cpp + src/default_azure_credential.cpp src/environment_credential.cpp src/managed_identity_credential.cpp src/managed_identity_source.cpp diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 02c391d782..9d581e7547 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -22,6 +22,21 @@ find_package(azure-identity-cpp CONFIG REQUIRED) target_link_libraries( PRIVATE Azure::azure-identity) ``` +### Prerequisites + +* An [Azure subscription][azure_sub]. +* The [Azure CLI][azure_cli] can also be useful for authenticating in a development environment, creating accounts, and managing account roles. + +### Authenticate the client + +When debugging and executing code locally it is typical for a developer to use their own account for authenticating calls to Azure services. There are several developer tools which can be used to perform this authentication in your development environment. + +#### Authenticate via the Azure CLI + +Developers can use the [Azure CLI][azure_cli] to authenticate. Applications using the `DefaultAzureCredential` or the `AzureCliCredential` can then use this account to authenticate calls in their application when running locally. + +To authenticate with the [Azure CLI][azure_cli], users can run the command `az login`. For users running on a system with a default web browser, the Azure CLI will launch the browser to authenticate the user. + ## Key concepts ### Credentials @@ -29,106 +44,103 @@ A credential is a class which contains or can obtain the data needed for a servi The Azure Identity library focuses on OAuth authentication with Azure Active directory, and it offers a variety of credential classes capable of acquiring an AAD token to authenticate service requests. All of the credential classes in this library are implementations of the `TokenCredential` abstract class in [azure-core][azure_core_library], and any of them can be used by to construct service clients capable of authenticating with a `TokenCredential`. -### Authenticating Service Principals - - - - - - - - - - - - - - - - - - - - - -
credential classusageconfiguration
ClientSecretCredentialauthenticates a service principal using a secretService principal authentication
ClientCertificateCredentialauthenticates a service principal using a certificateService principal authentication
+See [Credential Classes](#credential-classes) for a complete listing of available credential types. -## Environment Variables -`EnvironmentCredential` can be configured with environment variables. Each type of authentication requires values for specific variables: +### DefaultAzureCredential -#### Service principal with secret - - - - - - - - - - - - - - - - - - - - - -
variable namevalue
AZURE_CLIENT_IDid of an Azure Active Directory application
AZURE_TENANT_IDid of the application's Azure Active Directory tenant
AZURE_CLIENT_SECRETone of the application's client secrets
+`DefaultAzureCredential` combines credentials commonly used to authenticate when deployed, with credentials used to authenticate in a development environment. -#### Service principal with certificate - - - - - - - - - - - - - - - - - - - - - -
variable namevalue
AZURE_CLIENT_IDid of an Azure Active Directory application
AZURE_TENANT_IDid of the application's Azure Active Directory tenant
AZURE_CLIENT_CERTIFICATE_PATHpath to a PEM-encoded certificate file including private key (without password protection)
+> Note: `DefaultAzureCredential` is intended to simplify getting started with the SDK by handling common scenarios with reasonable default behaviors. It is not recommended to use it in production. Developers who want more control or whose scenario isn't served by the default settings should use other credential types. -Configuration is attempted in the above order. For example, if values for a client secret and certificate are both present, the client secret will be used. +The `DefaultAzureCredential` attempts to authenticate via the following mechanisms, in this order, stopping when one succeeds: -## Managed Identity Support -The [Managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) is supported via the `ManagedIdentityCredential` for the following Azure Services: -* [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token) -* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization) -* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication) +1. **Environment** - The `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate. +1. **Azure CLI** - If the developer has authenticated an account via the Azure CLI `az login` command, the `DefaultAzureCredential` will authenticate with that account. +1. **Managed Identity** - If the application is deployed to an Azure host with Managed Identity enabled, the `DefaultAzureCredential` will authenticate with that account. + +`DefaultAzureCredential` uses [`ChainedTokenCredential`](#chained-token-credential) that consists of a chain of `EnvironmentCredential`, `AzureCliCredential`, and `ManagedIdentityCredential`. Implementation, including the order in which credentials are applied is documented, but it may change from release to release. + +`DefaultAzureCredential` intends to provide a credential that "just works out of the box and without requiring any information", if only the environment is set up sufficiently for the credential to work. +Therefore, it could be simple to use, but since it uses a chain of credentials, it could be a bit complicated to diagnose if the environment setup is not sufficient. +TO help with this, `DefaultAzureCredential` code paths are instrumented with [log messages](#troubleshooting). + +## Examples + +See the [code samples](https://github.com/Azure/azure-sdk-for-cpp/tree/main/sdk/identity/azure-identity/samples). ## Chained Token Credential -`ChainedTokenCredential` allows users to customize the credentials considered when authenticating. + +`ChainedTokenCredential` allows users to set up custom authentication flow consisting of multiple credentials. An example below demonstrates using `ChainedTokenCredential` which will attempt to authenticate using `EnvironmentCredential`, and fall back to authenticate using `ManagedIdentityCredential`. ```cpp -// Authenticate using environment credential if it is available; otherwise use the managed identity credential to authenticate. +// A configuration demonstrated below would authenticate using EnvironmentCredential if it is +// available, and if it is not available, would fall back to use AzureCliCredential, and then to +// ManagedIdentityCredential. auto chainedTokenCredential = std::make_shared( Azure::Identity::ChainedTokenCredential::Sources{ std::make_shared(), + std::make_shared(), std::make_shared()}); Azure::Service::Client azureServiceClient("serviceUrl", chainedTokenCredential); ``` +## Managed Identity Support + +The [Managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) is supported via the `ManagedIdentityCredential` for the following Azure Services: +* [Azure App Service and Azure Functions](https://docs.microsoft.com/azure/app-service/overview-managed-identity) +* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization) +* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication) +* [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token) + +## Environment Variables + +`DefaultAzureCredential` and `EnvironmentCredential` can be configured with environment variables. Each type of authentication requires values for specific variables: + +#### Service principal with secret +|Variable name|Value +|-|- +|`AZURE_TENANT_ID`|ID of the application's Azure AD tenant +|`AZURE_CLIENT_ID`|ID of an Azure AD application +|`AZURE_CLIENT_SECRET`|one of the application's client secrets +|`AZURE_AUTHORITY_HOST`|(optional) [authentication authority URL](https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud) + +#### Service principal with certificate +|variable name|Value +|-|- +|`AZURE_CLIENT_ID`|ID of an Azure AD application +|`AZURE_TENANT_ID`|ID of the application's Azure AD tenant +|`AZURE_CLIENT_CERTIFICATE_PATH`|path to a PFX or PEM-encoded certificate file including private key +|`AZURE_AUTHORITY_HOST`|(optional) [authentication authority URL](https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud) + +Configuration is attempted in the above order. For example, if values for a client secret and certificate are both present, the client secret will be used. + +## Credential classes + +### Authenticate Azure-hosted applications +|Credential | Usage +|-|- +|`DefaultAzureCredential`|Provides a simplified authentication experience to quickly start developing applications run in Azure. +|`ChainedTokenCredential`|Allows users to define custom authentication flows composing multiple credentials. +|`ManagedIdentityCredential`|Authenticates the managed identity of an Azure resource. +|`EnvironmentCredential`|Authenticates a service principal or user via credential information specified in environment variables. + +### Authenticate service principals +|Credential | Usage +|-|- +|`ClientSecretCredential`|Authenticates a service principal [using a secret](https://learn.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals). +|`ClientCertificateCredential`|Authenticates a service principal [using a certificate](https://learn.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals). + +### Authenticate via development tools +|Credential | Usage +|-|- +|`AzureCliCredential`|Authenticates in a development environment [with the Azure CLI](https://learn.microsoft.com/cli/azure/authenticate-azure-cli). + ## Troubleshooting -Credentials raise exceptions either when they fail to authenticate or cannot execute authentication. -When credentials fail to authenticate, the `AuthenticationException` is thrown and it has the `what()` functions returning the description why authentication failed. + +1. Azure Identity [SDK log messages](https://github.com/Azure/azure-sdk-for-cpp/tree/main/sdk/core/azure-core#sdk-log-messages) contain disgnostic information, and start with "`Identity: `". +1. Credentials raise exceptions either when they fail to authenticate or cannot execute authentication. When a credential fails to authenticate, an `AuthenticationException` is thrown. The exception has the `what()` function that provides more information about the failure. ## Contributing For details on contributing to this repository, see the [contributing guide][azure_sdk_for_cpp_contributing]. @@ -163,6 +175,7 @@ Security issues and bugs should be reported privately, via email, to the Microso Azure SDK for C++ is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-cpp/blob/main/LICENSE.txt) license. +[azure_cli]: https://learn.microsoft.com/cli/azure [azsdk_vcpkg_install]: https://github.com/Azure/azure-sdk-for-cpp#download--install-the-sdk [azure_sdk_for_cpp_contributing]: https://github.com/Azure/azure-sdk-for-cpp/blob/main/CONTRIBUTING.md [azure_sdk_for_cpp_contributing_developer_guide]: https://github.com/Azure/azure-sdk-for-cpp/blob/main/CONTRIBUTING.md#developer-guide diff --git a/sdk/identity/azure-identity/inc/azure/identity.hpp b/sdk/identity/azure-identity/inc/azure/identity.hpp index 6afce616a9..5970cb7ae7 100644 --- a/sdk/identity/azure-identity/inc/azure/identity.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity.hpp @@ -12,6 +12,7 @@ #include "azure/identity/chained_token_credential.hpp" #include "azure/identity/client_certificate_credential.hpp" #include "azure/identity/client_secret_credential.hpp" +#include "azure/identity/default_azure_credential.hpp" #include "azure/identity/dll_import_export.hpp" #include "azure/identity/environment_credential.hpp" #include "azure/identity/managed_identity_credential.hpp" diff --git a/sdk/identity/azure-identity/inc/azure/identity/chained_token_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/chained_token_credential.hpp index e4de47d850..726735c2ef 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/chained_token_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/chained_token_credential.hpp @@ -11,9 +11,12 @@ #include #include +#include #include namespace Azure { namespace Identity { + class DefaultAzureCredential; + /** * @brief Chained Token Credential provides a token credential implementation which chains * multiple Azure::Core::Credentials::TokenCredential implementations to be tried in order until @@ -21,6 +24,10 @@ namespace Azure { namespace Identity { * */ class ChainedTokenCredential final : public Core::Credentials::TokenCredential { + // Friend declaration is needed for DefaultAzureCredential to access ChainedTokenCredential's + // private constructor built to be used specifically by it. + friend class DefaultAzureCredential; + public: /** * @brief A container type to store the ordered chain of credentials. @@ -55,7 +62,14 @@ namespace Azure { namespace Identity { Core::Context const& context) const override; private: + explicit ChainedTokenCredential( + Sources sources, + std::string const& enclosingCredential, + std::vector sourcesFriendlyNames); + Sources m_sources; + std::vector m_sourcesFriendlyNames; + std::string m_logPrefix; }; }} // namespace Azure::Identity 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 new file mode 100644 index 0000000000..6bde29f95e --- /dev/null +++ b/sdk/identity/azure-identity/inc/azure/identity/default_azure_credential.hpp @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Default Azure Credential. + */ + +#pragma once + +#include + +#include + +#include + +namespace Azure { namespace Identity { + + /** + * @brief Default Azure Credential combines multiple credentials that depend on the setup + * environment and require no parameters into a single chain. If the environment is set up + * sufficiently for at least one of such credentials to work, `DefaultAzureCredential` will work + * as well. + * + * @details This credential is using the #ChainedTokenCredential of 3 credentials in the order: + * #EnvironmentCredential, #AzureCliCredential, and #ManagedIdentityCredential. Even though the + * credentials being used and their order is documented, it may be changed in the future versions + * of the SDK, potentially bringing breaking changes in its behavior. + * + * @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 + * intended to be used in a production environment. + * + */ + class DefaultAzureCredential final : public Core::Credentials::TokenCredential { + public: + /** + * @brief Constructs `%DefaultAzureCredential`. + * + */ + explicit DefaultAzureCredential() + : DefaultAzureCredential(Core::Credentials::TokenCredentialOptions{}){}; + + /** + * @brief Constructs `%DefaultAzureCredential`. + * + * @param options Generic Token Credential Options. + */ + explicit DefaultAzureCredential(Core::Credentials::TokenCredentialOptions const& options); + + /** + * @brief Destructs `%DefaultAzureCredential`. + * + */ + ~DefaultAzureCredential() override; + + /** + * @brief Gets an authentication token. + * + * @param tokenRequestContext A context to get the token in. + * @param context A context to control the request lifetime. + * + * @throw Azure::Core::Credentials::AuthenticationException Authentication error occurred. + */ + Core::Credentials::AccessToken GetToken( + Core::Credentials::TokenRequestContext const& tokenRequestContext, + Core::Context const& context) const override; + + private: + std::shared_ptr m_credentials; + }; + +}} // namespace Azure::Identity diff --git a/sdk/identity/azure-identity/samples/CMakeLists.txt b/sdk/identity/azure-identity/samples/CMakeLists.txt index 1e56cd640e..c9ceca8774 100644 --- a/sdk/identity/azure-identity/samples/CMakeLists.txt +++ b/sdk/identity/azure-identity/samples/CMakeLists.txt @@ -27,6 +27,11 @@ target_link_libraries(client_secret_credential_sample PRIVATE azure-identity ser target_include_directories(client_secret_credential_sample PRIVATE .) create_per_service_target_build_for_sample(identity client_secret_credential_sample) +add_executable(default_azure_credential_sample default_azure_credential.cpp) +target_link_libraries(default_azure_credential_sample PRIVATE azure-identity service) +target_include_directories(default_azure_credential_sample PRIVATE .) +create_per_service_target_build_for_sample(identity default_azure_credential_sample) + add_executable(environment_credential_sample environment_credential.cpp) target_link_libraries(environment_credential_sample PRIVATE azure-identity service) target_include_directories(environment_credential_sample PRIVATE .) diff --git a/sdk/identity/azure-identity/samples/default_azure_credential.cpp b/sdk/identity/azure-identity/samples/default_azure_credential.cpp new file mode 100644 index 0000000000..238d5c3f39 --- /dev/null +++ b/sdk/identity/azure-identity/samples/default_azure_credential.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include + +#include + +int main() +{ + try + { + // Step 1: Initialize Default Azure Credential. + // Default Azure Credential is good for samples and initial development stages only. + // It is not recommended used it in a production environment. + // To diagnose, see + // https://github.com/Azure/azure-sdk-for-cpp/tree/main/sdk/identity/azure-identity#troubleshooting + + auto defaultAzureCredential = std::make_shared(); + + // Step 2: Pass the credential to an Azure Service Client. + Azure::Service::Client azureServiceClient("serviceUrl", defaultAzureCredential); + + // Step 3: Start using the Azure Service Client. + azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext); + + std::cout << "Success!" << std::endl; + } + catch (const Azure::Core::Credentials::AuthenticationException& exception) + { + // Step 4: Handle authentication errors, if needed + // (invalid credential parameters, insufficient permissions). + std::cout << "Authentication error: " << exception.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/sdk/identity/azure-identity/src/azure_cli_credential.cpp b/sdk/identity/azure-identity/src/azure_cli_credential.cpp index fc1f1329f9..83e3e4b05c 100644 --- a/sdk/identity/azure-identity/src/azure_cli_credential.cpp +++ b/sdk/identity/azure-identity/src/azure_cli_credential.cpp @@ -5,6 +5,7 @@ #include "private/token_credential_impl.hpp" +#include #include #include #include @@ -43,11 +44,15 @@ using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Credentials::TokenCredentialOptions; using Azure::Core::Credentials::TokenRequestContext; +using Azure::Core::Diagnostics::Logger; +using Azure::Core::Diagnostics::_internal::Log; using Azure::Identity::AzureCliCredentialOptions; using Azure::Identity::_detail::TokenCache; using Azure::Identity::_detail::TokenCredentialImpl; namespace { +std::string const MsgPrefix = "Identity: AzureCliCredential"; + void ThrowIfNotSafeCmdLineInput(std::string const& input, std::string const& description) { for (auto const c : input) @@ -66,8 +71,7 @@ void ThrowIfNotSafeCmdLineInput(std::string const& input, std::string const& des if (!std::isalnum(c, std::locale::classic())) { throw AuthenticationException( - "AzureCliCredential: Unsafe command line input found in " + description + ": " - + input); + MsgPrefix + ": Unsafe command line input found in " + description + ": " + input); } } } @@ -82,6 +86,16 @@ AzureCliCredential::AzureCliCredential( { static_cast(options); ThrowIfNotSafeCmdLineInput(m_tenantId, "TenantID"); + + auto const logLevel = Logger::Level::Informational; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + MsgPrefix + + " created.\n" + "Successful creation does not guarantee further successful token retrieval."); + } } AzureCliCredential::AzureCliCredential(AzureCliCredentialOptions const& options) @@ -147,7 +161,15 @@ AccessToken AzureCliCredential::GetToken( } catch (std::exception const& e) { - throw AuthenticationException(std::string("AzureCliCredential::GetToken(): ") + e.what()); + auto const errorMsg = MsgPrefix + " didn't get the token: \"" + e.what() + '\"'; + + auto const logLevel = Logger::Level::Warning; + if (Log::ShouldWrite(logLevel)) + { + Log::Write(logLevel, errorMsg); + } + + throw AuthenticationException(errorMsg); } }); } diff --git a/sdk/identity/azure-identity/src/chained_token_credential.cpp b/sdk/identity/azure-identity/src/chained_token_credential.cpp index 6db48d282b..3a63bea9ce 100644 --- a/sdk/identity/azure-identity/src/chained_token_credential.cpp +++ b/sdk/identity/azure-identity/src/chained_token_credential.cpp @@ -4,69 +4,138 @@ #include "azure/identity/chained_token_credential.hpp" #include "azure/core/internal/diagnostics/log.hpp" +#include #include using namespace Azure::Identity; using namespace Azure::Core::Credentials; using Azure::Core::Context; +using Azure::Core::Diagnostics::Logger; +using Azure::Core::Diagnostics::_internal::Log; + +namespace { +std::string const IdentityPrefix = "Identity: "; +} ChainedTokenCredential::ChainedTokenCredential(ChainedTokenCredential::Sources sources) - : m_sources(std::move(sources)) + : ChainedTokenCredential(sources, {}, {}) { } -ChainedTokenCredential::~ChainedTokenCredential() = default; - -AccessToken ChainedTokenCredential::GetToken( - TokenRequestContext const& tokenRequestContext, - Context const& context) const +ChainedTokenCredential::ChainedTokenCredential( + ChainedTokenCredential::Sources sources, + std::string const& enclosingCredential, + std::vector sourcesFriendlyNames) + : m_sources(std::move(sources)), m_sourcesFriendlyNames(std::move(sourcesFriendlyNames)) { - using Azure::Core::Diagnostics::Logger; - using Azure::Core::Diagnostics::_internal::Log; + // LCOV_EXCL_START + AZURE_ASSERT(m_sourcesFriendlyNames.empty() || m_sourcesFriendlyNames.size() == m_sources.size()); + // LCOV_EXCL_STOP - auto n = 0; - for (const auto& source : m_sources) + auto const logLevel = m_sources.empty() ? Logger::Level::Warning : Logger::Level::Informational; + if (Log::ShouldWrite(logLevel)) { - try + std::string credentialsList; + if (!m_sourcesFriendlyNames.empty()) { - ++n; - auto token = source->GetToken(tokenRequestContext, context); - + auto const sourceDescriptionsSize = m_sourcesFriendlyNames.size(); + for (size_t i = 0; i < (sourceDescriptionsSize - 1); ++i) { - const auto logLevel = Logger::Level::Informational; - if (Log::ShouldWrite(logLevel)) - { - Log::Write( - logLevel, - std::string("ChainedTokenCredential authentication attempt with credential #") - + std::to_string(n) + " did succeed."); - } + credentialsList += m_sourcesFriendlyNames[i] + ", "; } - return token; + credentialsList += m_sourcesFriendlyNames.back(); } - catch (const AuthenticationException& e) + + Log::Write( + logLevel, + IdentityPrefix + + (enclosingCredential.empty() + ? "ChainedTokenCredential: Created" + : (enclosingCredential + ": Created ChainedTokenCredential")) + + " with " + + (m_sourcesFriendlyNames.empty() + ? (std::to_string(m_sources.size()) + " credentials.") + : (std::string("the following credentials: ") + credentialsList + '.'))); + } + + m_logPrefix = IdentityPrefix + + (enclosingCredential.empty() ? "ChainedTokenCredential" + : (enclosingCredential + " -> ChainedTokenCredential")) + + ": "; + + if (m_sourcesFriendlyNames.empty()) + { + auto const sourcesSize = m_sources.size(); + for (size_t i = 1; i <= sourcesSize; ++i) { - const auto logLevel = Logger::Level::Verbose; - if (Log::ShouldWrite(logLevel)) - { - Log::Write( - logLevel, - std::string("ChainedTokenCredential authentication attempt with credential #") - + std::to_string(n) + " did not succeed: " + e.what()); - } + m_sourcesFriendlyNames.push_back(std::string("credential #") + std::to_string(i)); } } +} + +ChainedTokenCredential::~ChainedTokenCredential() = default; + +AccessToken ChainedTokenCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const +{ + auto const sourcesSize = m_sources.size(); - if (n == 0) + if (sourcesSize == 0) { - const auto logLevel = Logger::Level::Verbose; + const auto logLevel = Logger::Level::Warning; if (Log::ShouldWrite(logLevel)) { Log::Write( - logLevel, - "ChainedTokenCredential authentication did not succeed: list of sources is empty."); + logLevel, m_logPrefix + "Authentication did not succeed: List of sources is empty."); + } + } + else + { + for (size_t i = 0; i < sourcesSize; ++i) + { + try + { + auto token = m_sources[i]->GetToken(tokenRequestContext, context); + + { + auto const logLevel = Logger::Level::Informational; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + m_logPrefix + "Successfully got token from " + m_sourcesFriendlyNames[i] + '.'); + } + } + + return token; + } + catch (AuthenticationException const& e) + { + { + auto const logLevel = Logger::Level::Verbose; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + m_logPrefix + "Failed to get token from " + m_sourcesFriendlyNames[i] + ": " + + e.what()); + } + } + + if ((sourcesSize - 1) == i) // On the last credential + { + auto const logLevel = Logger::Level::Warning; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + m_logPrefix + "Didn't succeed to get a token from any credential in the chain."); + } + } + } } } diff --git a/sdk/identity/azure-identity/src/default_azure_credential.cpp b/sdk/identity/azure-identity/src/default_azure_credential.cpp new file mode 100644 index 0000000000..85d0eead91 --- /dev/null +++ b/sdk/identity/azure-identity/src/default_azure_credential.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/default_azure_credential.hpp" + +#include "azure/identity/azure_cli_credential.hpp" +#include "azure/identity/environment_credential.hpp" +#include "azure/identity/managed_identity_credential.hpp" + +#include "azure/core/internal/diagnostics/log.hpp" + +using namespace Azure::Identity; +using namespace Azure::Core::Credentials; + +using Azure::Core::Context; +using Azure::Core::Diagnostics::Logger; +using Azure::Core::Diagnostics::_internal::Log; + +namespace { +std::string const IdentityPrefix = "Identity: "; +} + +DefaultAzureCredential::DefaultAzureCredential(TokenCredentialOptions const& options) +{ + // Initializing m_credential below and not in the member initializer list to have a specific order + // of log messages. + auto const logLevel = Logger::Level::Verbose; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + IdentityPrefix + + "Creating DefaultAzureCredential which combines mutiple parameterless credentials " + "into a single one (by using ChainedTokenCredential)." + "\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."); + } + + // Creating credentials in order to ensure the order of log messages. + auto const envCred = std::make_shared(options); + auto const azCliCred = std::make_shared(options); + auto const managedIdentityCred = std::make_shared(options); + + // Using the ChainedTokenCredential's private constructor for more detailed log messages. + m_credentials.reset(new ChainedTokenCredential( + ChainedTokenCredential::Sources{envCred, azCliCred, managedIdentityCred}, + "DefaultAzureCredential", // extra args for the ChainedTokenCredential's private constructor. + std::vector{ + "EnvironmentCredential", "AzureCliCredential", "ManagedIdentityCredential"})); +} + +DefaultAzureCredential::~DefaultAzureCredential() = default; + +AccessToken DefaultAzureCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const +{ + try + { + return m_credentials->GetToken(tokenRequestContext, context); + } + catch (AuthenticationException const&) + { + throw AuthenticationException("Failed to get token from DefaultAzureCredential." + "\nSee Azure::Core::Diagnostics::Logger for details " + "(https://github.com/Azure/azure-sdk-for-cpp/tree/main/sdk/" + "identity/azure-identity#troubleshooting)."); + } +} diff --git a/sdk/identity/azure-identity/src/environment_credential.cpp b/sdk/identity/azure-identity/src/environment_credential.cpp index 8ace56ca22..2d6eb2479c 100644 --- a/sdk/identity/azure-identity/src/environment_credential.cpp +++ b/sdk/identity/azure-identity/src/environment_credential.cpp @@ -5,8 +5,13 @@ #include "azure/identity/client_certificate_credential.hpp" #include "azure/identity/client_secret_credential.hpp" +#include +#include #include +#include +#include + using Azure::Identity::EnvironmentCredential; using Azure::Core::Context; @@ -15,19 +20,32 @@ using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Credentials::TokenCredentialOptions; using Azure::Core::Credentials::TokenRequestContext; +using Azure::Core::Diagnostics::Logger; +using Azure::Core::Diagnostics::_internal::Log; + +namespace { +constexpr auto AzureTenantIdEnvVarName = "AZURE_TENANT_ID"; +constexpr auto AzureClientIdEnvVarName = "AZURE_CLIENT_ID"; +constexpr auto AzureClientSecretEnvVarName = "AZURE_CLIENT_SECRET"; +constexpr auto AzureAuthorityHostEnvVarName = "AZURE_AUTHORITY_HOST"; +constexpr auto AzureClientCertificatePathEnvVarName = "AZURE_CLIENT_CERTIFICATE_PATH"; + +std::string const LogMsgPrefix = "Identity: EnvironmentCredential"; + +void PrintCredentialCreationLogMessage( + std::vector> const& envVarsToParams, + char const* credThatGetsCreated); +} // namespace EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options) { - auto tenantId = Environment::GetVariable("AZURE_TENANT_ID"); - auto clientId = Environment::GetVariable("AZURE_CLIENT_ID"); - - auto clientSecret = Environment::GetVariable("AZURE_CLIENT_SECRET"); - auto authority = Environment::GetVariable("AZURE_AUTHORITY_HOST"); + auto tenantId = Environment::GetVariable(AzureTenantIdEnvVarName); + auto clientId = Environment::GetVariable(AzureClientIdEnvVarName); - // auto username = Environment::GetVariable("AZURE_USERNAME"); - // auto password = Environment::GetVariable("AZURE_PASSWORD"); + auto clientSecret = Environment::GetVariable(AzureClientSecretEnvVarName); + auto authority = Environment::GetVariable(AzureAuthorityHostEnvVarName); - auto clientCertificatePath = Environment::GetVariable("AZURE_CLIENT_CERTIFICATE_PATH"); + auto clientCertificatePath = Environment::GetVariable(AzureClientCertificatePathEnvVarName); if (!tenantId.empty() && !clientId.empty()) { @@ -35,6 +53,15 @@ EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options) { if (!authority.empty()) { + PrintCredentialCreationLogMessage( + { + {AzureTenantIdEnvVarName, "tenantId"}, + {AzureClientIdEnvVarName, "clientId"}, + {AzureClientSecretEnvVarName, "clientSecret"}, + {AzureAuthorityHostEnvVarName, "authorityHost"}, + }, + "ClientSecretCredential"); + ClientSecretCredentialOptions clientSecretCredentialOptions; static_cast(clientSecretCredentialOptions) = options; clientSecretCredentialOptions.AuthorityHost = authority; @@ -44,20 +71,31 @@ EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options) } else { + PrintCredentialCreationLogMessage( + { + {AzureTenantIdEnvVarName, "tenantId"}, + {AzureClientIdEnvVarName, "clientId"}, + {AzureClientSecretEnvVarName, "clientSecret"}, + }, + "ClientSecretCredential"); + m_credentialImpl.reset( new ClientSecretCredential(tenantId, clientId, clientSecret, options)); } } - // TODO: UsernamePasswordCredential is not implemented. Uncomment when implemented. - // else if (!username.empty() && !password.empty()) - // { - // m_credentialImpl.reset( - // new UsernamePasswordCredential(tenantId, clientId, username, password, options)); - // } else if (!clientCertificatePath.empty()) { if (!authority.empty()) { + PrintCredentialCreationLogMessage( + { + {AzureTenantIdEnvVarName, "tenantId"}, + {AzureClientIdEnvVarName, "clientId"}, + {AzureClientCertificatePathEnvVarName, "clientCertificatePath"}, + {AzureAuthorityHostEnvVarName, "authorityHost"}, + }, + "ClientCertificateCredential"); + ClientCertificateCredentialOptions clientCertificateCredentialOptions; static_cast(clientCertificateCredentialOptions) = options; clientCertificateCredentialOptions.AuthorityHost = authority; @@ -67,11 +105,56 @@ EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options) } else { + PrintCredentialCreationLogMessage( + { + {AzureTenantIdEnvVarName, "tenantId"}, + {AzureClientIdEnvVarName, "clientId"}, + {AzureClientCertificatePathEnvVarName, "clientCertificatePath"}, + }, + "ClientCertificateCredential"); + m_credentialImpl.reset( new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options)); } } } + + if (!m_credentialImpl) + { + auto const logLevel = Logger::Level::Warning; + if (Log::ShouldWrite(logLevel)) + { + auto const basicMessage = LogMsgPrefix + " was not initialized with underlying credential"; + + if (!Log::ShouldWrite(Logger::Level::Verbose)) + { + Log::Write(logLevel, basicMessage + '.'); + } + else + { + auto logMsg = basicMessage + ": Both '" + AzureTenantIdEnvVarName + "' and '" + + AzureClientIdEnvVarName + "', and at least one of '" + AzureClientSecretEnvVarName + + "', '" + AzureClientCertificatePathEnvVarName + "' needs to be set. Additionally, '" + + AzureAuthorityHostEnvVarName + + "' could be set to override the default authority host. Currently:\n"; + + std::pair envVarStatus[] = { + {AzureTenantIdEnvVarName, !tenantId.empty()}, + {AzureClientIdEnvVarName, !clientId.empty()}, + {AzureClientSecretEnvVarName, !clientSecret.empty()}, + {AzureClientCertificatePathEnvVarName, !clientCertificatePath.empty()}, + {AzureAuthorityHostEnvVarName, !authority.empty()}, + }; + for (auto const& status : envVarStatus) + { + logMsg += std::string(" * '") + status.first + "' " + "is" + + (status.second ? " " : " NOT ") + "set\n"; + } + + Log::Write(logLevel, logMsg); + } + } + } } AccessToken EnvironmentCredential::GetToken( @@ -80,9 +163,68 @@ AccessToken EnvironmentCredential::GetToken( { if (!m_credentialImpl) { - throw AuthenticationException("EnvironmentCredential authentication unavailable. " - "Environment variables are not fully configured."); + auto const AuthUnavailable = LogMsgPrefix + " authentication unavailable. "; + + { + auto const logLevel = Logger::Level::Warning; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + AuthUnavailable + "See earlier EnvironmentCredential log messages for details."); + } + } + + throw AuthenticationException( + AuthUnavailable + "Environment variables are not fully configured."); } return m_credentialImpl->GetToken(tokenRequestContext, context); } + +namespace { +void PrintCredentialCreationLogMessage( + std::vector> const& envVarsToParams, + char const* credThatGetsCreated) +{ + if (!Log::ShouldWrite(Logger::Level::Verbose)) + { + if (Log::ShouldWrite(Logger::Level::Informational)) + { + Log::Write( + Logger::Level::Informational, + LogMsgPrefix + " gets created with " + credThatGetsCreated + '.'); + } + + return; + } + + auto const envVarsToParamsSize = envVarsToParams.size(); + + // LCOV_EXCL_START + AZURE_ASSERT(envVarsToParamsSize > 1); + // LCOV_EXCL_STOP + + constexpr auto Tick = "'"; + constexpr auto Comma = ", "; + + std::string const And = "and "; + + std::string envVars; + std::string credParams; + for (size_t i = 0; i < envVarsToParamsSize - 1; + ++i) // not iterating over the last element for ", and". + { + envVars += Tick + std::string(envVarsToParams[i].first) + Tick + Comma; + credParams += std::string(envVarsToParams[i].second) + Comma; + } + + envVars += And + Tick + envVarsToParams.back().first + Tick; + credParams += And + envVarsToParams.back().second; + + Log::Write( + Logger::Level::Verbose, + LogMsgPrefix + ": " + envVars + " environment variables are set, so " + credThatGetsCreated + + " with corresponding " + credParams + " gets created."); +} +} // namespace diff --git a/sdk/identity/azure-identity/src/managed_identity_source.cpp b/sdk/identity/azure-identity/src/managed_identity_source.cpp index dc4af9d046..de7e8e6c3c 100644 --- a/sdk/identity/azure-identity/src/managed_identity_source.cpp +++ b/sdk/identity/azure-identity/src/managed_identity_source.cpp @@ -5,24 +5,62 @@ #include +#include + #include #include #include #include using namespace Azure::Identity::_detail; + using Azure::Core::_internal::Environment; +using Azure::Core::Diagnostics::Logger; +using Azure::Core::Diagnostics::_internal::Log; + +namespace { +std::string const IdentityPrefix = "Identity: "; +constexpr auto CredPrefix = "ManagedIdentityCredential"; + +std::string WithSourceMessage(std::string const& credSource) +{ + return " with " + credSource + " source"; +} + +void PrintEnvNotSetUpMessage(std::string const& credSource) +{ + auto const logLevel = Logger::Level::Verbose; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + IdentityPrefix + CredPrefix + ": Environment is not set up for the credential to be created" + + WithSourceMessage(credSource) + '.'); + } +} +} // namespace Azure::Core::Url ManagedIdentitySource::ParseEndpointUrl( std::string const& url, - char const* envVarName) + char const* envVarName, + std::string const& credSource) { using Azure::Core::Url; using Azure::Core::Credentials::AuthenticationException; try { - return Url(url); + auto const endpointUrl = Url(url); + + auto const logLevel = Logger::Level::Informational; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + IdentityPrefix + CredPrefix + " will be created" + WithSourceMessage(credSource) + '.'); + } + + return endpointUrl; } catch (std::invalid_argument const&) { @@ -31,24 +69,40 @@ Azure::Core::Url ManagedIdentitySource::ParseEndpointUrl( { } - throw AuthenticationException( - std::string("The environment variable ") + envVarName + " contains an invalid URL."); + auto const errorMessage = CredPrefix + WithSourceMessage(credSource) + + ": Failed to create: The environment variable \'" + envVarName + + "\' contains an invalid URL."; + + auto const logLevel = Logger::Level::Warning; + if (Log::ShouldWrite(logLevel)) + { + Log::Write(logLevel, IdentityPrefix + errorMessage); + } + + throw AuthenticationException(errorMessage); } template std::unique_ptr AppServiceManagedIdentitySource::Create( std::string const& clientId, Azure::Core::Credentials::TokenCredentialOptions const& options, - const char* endpointVarName, - const char* secretVarName) + char const* endpointVarName, + char const* secretVarName, + char const* appServiceVersion) { - auto msiEndpoint = Environment::GetVariable(endpointVarName); - auto msiSecret = Environment::GetVariable(secretVarName); + auto const msiEndpoint = Environment::GetVariable(endpointVarName); + auto const msiSecret = Environment::GetVariable(secretVarName); + + auto const credSource = std::string("App Service ") + appServiceVersion; - return (msiEndpoint.empty() || msiSecret.empty()) - ? nullptr - : std::unique_ptr( - new T(clientId, options, ParseEndpointUrl(msiEndpoint, endpointVarName), msiSecret)); + if (!msiEndpoint.empty() && !msiSecret.empty()) + { + return std::unique_ptr(new T( + clientId, options, ParseEndpointUrl(msiEndpoint, endpointVarName, credSource), msiSecret)); + } + + PrintEnvNotSetUpMessage(credSource); + return nullptr; } AppServiceManagedIdentitySource::AppServiceManagedIdentitySource( @@ -113,7 +167,7 @@ std::unique_ptr AppServiceV2017ManagedIdentitySource::Cre Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - clientId, options, "MSI_ENDPOINT", "MSI_SECRET"); + clientId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); } std::unique_ptr AppServiceV2019ManagedIdentitySource::Create( @@ -121,7 +175,7 @@ std::unique_ptr AppServiceV2019ManagedIdentitySource::Cre Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - clientId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER"); + clientId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER", "2019"); } std::unique_ptr CloudShellManagedIdentitySource::Create( @@ -131,10 +185,16 @@ std::unique_ptr CloudShellManagedIdentitySource::Create( constexpr auto EndpointVarName = "MSI_ENDPOINT"; auto msiEndpoint = Environment::GetVariable(EndpointVarName); - return (msiEndpoint.empty()) - ? nullptr - : std::unique_ptr(new CloudShellManagedIdentitySource( - clientId, options, ParseEndpointUrl(msiEndpoint, EndpointVarName))); + std::string const CredSource = "Cloud Shell"; + + if (!msiEndpoint.empty()) + { + return std::unique_ptr(new CloudShellManagedIdentitySource( + clientId, options, ParseEndpointUrl(msiEndpoint, EndpointVarName, CredSource))); + } + + PrintEnvNotSetUpMessage(CredSource); + return nullptr; } CloudShellManagedIdentitySource::CloudShellManagedIdentitySource( @@ -200,8 +260,11 @@ std::unique_ptr AzureArcManagedIdentitySource::Create( constexpr auto EndpointVarName = "IDENTITY_ENDPOINT"; auto identityEndpoint = Environment::GetVariable(EndpointVarName); + std::string const credSource = "Azure Arc"; + if (identityEndpoint.empty() || Environment::GetVariable("IMDS_ENDPOINT").empty()) { + PrintEnvNotSetUpMessage(credSource); return nullptr; } @@ -214,7 +277,7 @@ std::unique_ptr AzureArcManagedIdentitySource::Create( } return std::unique_ptr(new AzureArcManagedIdentitySource( - options, ParseEndpointUrl(identityEndpoint, EndpointVarName))); + options, ParseEndpointUrl(identityEndpoint, EndpointVarName, credSource))); } AzureArcManagedIdentitySource::AzureArcManagedIdentitySource( @@ -313,6 +376,16 @@ std::unique_ptr ImdsManagedIdentitySource::Create( std::string const& clientId, Azure::Core::Credentials::TokenCredentialOptions const& options) { + auto const logLevel = Logger::Level::Informational; + if (Log::ShouldWrite(logLevel)) + { + Log::Write( + logLevel, + IdentityPrefix + CredPrefix + " will be created" + + WithSourceMessage("Azure Instance Metadata Service") + + ".\nSuccessful creation does not guarantee further successful token retrieval."); + } + return std::unique_ptr(new ImdsManagedIdentitySource(clientId, options)); } diff --git a/sdk/identity/azure-identity/src/private/managed_identity_source.hpp b/sdk/identity/azure-identity/src/private/managed_identity_source.hpp index cf80baf9e0..9972ce904a 100644 --- a/sdk/identity/azure-identity/src/private/managed_identity_source.hpp +++ b/sdk/identity/azure-identity/src/private/managed_identity_source.hpp @@ -28,7 +28,11 @@ namespace Azure { namespace Identity { namespace _detail { protected: _detail::TokenCache m_tokenCache; - static Core::Url ParseEndpointUrl(std::string const& url, char const* envVarName); + + static Core::Url ParseEndpointUrl( + std::string const& url, + char const* envVarName, + std::string const& credSource); explicit ManagedIdentitySource( std::string clientId, @@ -61,8 +65,9 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& clientId, Core::Credentials::TokenCredentialOptions const& options, - const char* endpointVarName, - const char* secretVarName); + char const* endpointVarName, + char const* secretVarName, + char const* appServiceVersion); public: Core::Credentials::AccessToken GetToken( diff --git a/sdk/identity/azure-identity/test/ut/CMakeLists.txt b/sdk/identity/azure-identity/test/ut/CMakeLists.txt index a87255464d..2c560e245f 100644 --- a/sdk/identity/azure-identity/test/ut/CMakeLists.txt +++ b/sdk/identity/azure-identity/test/ut/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable ( client_secret_credential_test.cpp credential_test_helper.cpp credential_test_helper.hpp + default_azure_credential_test.cpp environment_credential_test.cpp macro_guard_test.cpp managed_identity_credential_test.cpp diff --git a/sdk/identity/azure-identity/test/ut/azure_cli_credential_test.cpp b/sdk/identity/azure-identity/test/ut/azure_cli_credential_test.cpp index 44f5ad4cc4..31b740494e 100644 --- a/sdk/identity/azure-identity/test/ut/azure_cli_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/azure_cli_credential_test.cpp @@ -3,6 +3,7 @@ #include "azure/identity/azure_cli_credential.hpp" +#include #include #include @@ -122,13 +123,39 @@ TEST(AzureCliCredential, NotAvailable) || (!defined(WINAPI_PARTITION_DESKTOP) || WINAPI_PARTITION_DESKTOP) // not UWP TEST(AzureCliCredential, Error) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Informational); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + AzureCliTestCredential const azCliCred( - EchoCommand("ERROR: Please run 'az login' to setup account.")); + EchoCommand("ERROR: Please run az login to setup account.")); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Informational); + EXPECT_EQ( + log[0].second, + "Identity: AzureCliCredential created." + "\nSuccessful creation does not guarantee further successful token retrieval."); TokenRequestContext trc; trc.Scopes.push_back("https://storage.azure.com/.default"); + log.clear(); + auto const errorMsg = "Identity: AzureCliCredential didn't get the token:" + " \"ERROR: Please run az login to setup account." +#if defined(AZ_PLATFORM_WINDOWS) + "\r" +#endif + "\n\""; + EXPECT_THROW(static_cast(azCliCred.GetToken(trc, {})), AuthenticationException); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ(log[0].second, errorMsg); + + Logger::SetListener(nullptr); } TEST(AzureCliCredential, EmptyOutput) diff --git a/sdk/identity/azure-identity/test/ut/chained_token_credential_test.cpp b/sdk/identity/azure-identity/test/ut/chained_token_credential_test.cpp index f2cb6267a8..fa8297e24a 100644 --- a/sdk/identity/azure-identity/test/ut/chained_token_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/chained_token_credential_test.cpp @@ -106,40 +106,83 @@ TEST(ChainedTokenCredential, Logging) Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); - ChainedTokenCredential c0({}); - EXPECT_THROW(c0.GetToken({}, {}), AuthenticationException); - EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); - EXPECT_EQ(log[0].first, Logger::Level::Verbose); - EXPECT_EQ( - log[0].second, - "ChainedTokenCredential authentication did not succeed: list of sources is empty."); - - log.clear(); - auto c1 = std::make_shared(); - auto c2 = std::make_shared("Token2"); - ChainedTokenCredential cred({c1, c2}); - - EXPECT_FALSE(c1->WasInvoked); - EXPECT_FALSE(c2->WasInvoked); - - auto token = cred.GetToken({}, {}); - EXPECT_EQ(token.Token, "Token2"); - - EXPECT_TRUE(c1->WasInvoked); - EXPECT_TRUE(c2->WasInvoked); - - EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); + { + ChainedTokenCredential cred({}); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ(log[0].second, "Identity: ChainedTokenCredential: Created with 0 credentials."); + + log.clear(); + EXPECT_THROW(static_cast(cred.GetToken({}, {})), AuthenticationException); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: ChainedTokenCredential: " + "Authentication did not succeed: List of sources is empty."); + } - EXPECT_EQ(log[0].first, Logger::Level::Verbose); - EXPECT_EQ( - log[0].second, - "ChainedTokenCredential authentication attempt with credential #1 did not succeed: " - "Test Error"); + { + log.clear(); + auto c = std::make_shared(); + ChainedTokenCredential cred({c}); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Informational); + EXPECT_EQ(log[0].second, "Identity: ChainedTokenCredential: Created with 1 credentials."); + + log.clear(); + EXPECT_FALSE(c->WasInvoked); + + EXPECT_THROW(static_cast(cred.GetToken({}, {})), AuthenticationException); + EXPECT_TRUE(c->WasInvoked); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ChainedTokenCredential: Failed to get token from credential #1: " + "Test Error"); + + EXPECT_EQ(log[1].first, Logger::Level::Warning); + EXPECT_EQ( + log[1].second, + "Identity: ChainedTokenCredential: " + "Didn't succeed to get a token from any credential in the chain."); + } - EXPECT_EQ(log[1].first, Logger::Level::Informational); - EXPECT_EQ( - log[1].second, - "ChainedTokenCredential authentication attempt with credential #2 did succeed."); + { + log.clear(); + auto c1 = std::make_shared(); + auto c2 = std::make_shared("Token2"); + ChainedTokenCredential cred({c1, c2}); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Informational); + EXPECT_EQ(log[0].second, "Identity: ChainedTokenCredential: Created with 2 credentials."); + + log.clear(); + EXPECT_FALSE(c1->WasInvoked); + EXPECT_FALSE(c2->WasInvoked); + + auto token = cred.GetToken({}, {}); + EXPECT_EQ(token.Token, "Token2"); + + EXPECT_TRUE(c1->WasInvoked); + EXPECT_TRUE(c2->WasInvoked); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ChainedTokenCredential: Failed to get token from credential #1: " + "Test Error"); + + EXPECT_EQ(log[1].first, Logger::Level::Informational); + EXPECT_EQ( + log[1].second, + "Identity: ChainedTokenCredential: Successfully got token from credential #2."); + } Logger::SetListener(nullptr); } 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 new file mode 100644 index 0000000000..0b7826f7e0 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/default_azure_credential.hpp" + +#include "credential_test_helper.hpp" +#include + +#include + +using Azure::Identity::DefaultAzureCredential; + +using Azure::Core::Credentials::TokenCredentialOptions; +using Azure::Core::Diagnostics::Logger; +using Azure::Identity::Test::_detail::CredentialTestHelper; + +namespace { +} // namespace + +TEST(DefaultAzureCredential, LogMessages) +{ + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + + CredentialTestHelper::SimulateTokenRequest( + [&](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + 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_USERNAME", ""}, + {"AZURE_PASSWORD", ""}, + {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); + + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(9)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: Creating DefaultAzureCredential which combines mutiple parameterless " + "credentials " + "into a single one (by using ChainedTokenCredential)." + "\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[1].first, Logger::Level::Verbose); + EXPECT_EQ( + log[1].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[2].first, Logger::Level::Informational); + EXPECT_EQ( + log[2].second, + "Identity: AzureCliCredential created." + "\nSuccessful creation does not guarantee further successful token retrieval."); + + EXPECT_EQ(log[3].first, Logger::Level::Verbose); + EXPECT_EQ( + log[3].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2019 source."); + + EXPECT_EQ(log[4].first, Logger::Level::Verbose); + EXPECT_EQ( + log[4].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2017 source."); + + 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 Cloud Shell source."); + + 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 Azure Arc source."); + + EXPECT_EQ(log[7].first, Logger::Level::Informational); + EXPECT_EQ( + log[7].second, + "Identity: ManagedIdentityCredential will be created " + "with Azure Instance Metadata Service source." + "\nSuccessful creation does not guarantee further successful token retrieval."); + + EXPECT_EQ(log[8].first, Logger::Level::Informational); + EXPECT_EQ( + log[8].second, + "Identity: DefaultAzureCredential: Created ChainedTokenCredential " + "with the following credentials: " + "EnvironmentCredential, AzureCliCredential, ManagedIdentityCredential."); + + log.clear(); + + return credential; + }, + {{"https://azure.com/.default"}}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); + + EXPECT_EQ( + log.size(), + LogMsgVec::size_type(4)); // Request and retry policies will get their messages here as well. + + EXPECT_EQ(log[3].first, Logger::Level::Informational); + EXPECT_EQ( + log[3].second, + "Identity: DefaultAzureCredential -> ChainedTokenCredential: " + "Successfully got token from EnvironmentCredential."); + + Logger::SetListener(nullptr); +} diff --git a/sdk/identity/azure-identity/test/ut/environment_credential_test.cpp b/sdk/identity/azure-identity/test/ut/environment_credential_test.cpp index d7ebfacf18..5388b21892 100644 --- a/sdk/identity/azure-identity/test/ut/environment_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/environment_credential_test.cpp @@ -4,6 +4,7 @@ #include "azure/identity/environment_credential.hpp" #include "credential_test_helper.hpp" +#include #include @@ -14,8 +15,14 @@ using Azure::Identity::Test::_detail::CredentialTestHelper; TEST(EnvironmentCredential, RegularClientSecretCredential) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -28,7 +35,19 @@ TEST(EnvironmentCredential, RegularClientSecretCredential) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].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."); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); @@ -65,12 +84,21 @@ TEST(EnvironmentCredential, RegularClientSecretCredential) using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, AzureStackClientSecretCredential) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Informational); // Setting it to Informational instead of Verbose + // will result in less detailed log message. + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -83,7 +111,16 @@ TEST(EnvironmentCredential, AzureStackClientSecretCredential) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Informational); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential gets created with ClientSecretCredential."); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); @@ -118,6 +155,8 @@ TEST(EnvironmentCredential, AzureStackClientSecretCredential) using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, Unavailable) @@ -125,8 +164,14 @@ TEST(EnvironmentCredential, Unavailable) using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + static_cast(CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -139,22 +184,57 @@ TEST(EnvironmentCredential, Unavailable) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential was not initialized with underlying credential: Both " + "'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID', and at least one of 'AZURE_CLIENT_SECRET', " + "'AZURE_CLIENT_CERTIFICATE_PATH' needs to be set. Additionally, " + "'AZURE_AUTHORITY_HOST' could be set to override the default authority host." + " Currently:\n" + " * 'AZURE_TENANT_ID' is NOT set\n" + " * 'AZURE_CLIENT_ID' is NOT set\n" + " * 'AZURE_CLIENT_SECRET' is NOT set\n" + " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" + " * 'AZURE_AUTHORITY_HOST' is NOT set\n"); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, - [](auto& credential, auto& tokenRequestContext, auto& context) { + [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential authentication unavailable. " + "See earlier EnvironmentCredential log messages for details."); + log.clear(); + return token; })); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretDefaultAuthority) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -167,7 +247,18 @@ TEST(EnvironmentCredential, ClientSecretDefaultAuthority) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', and " + "'AZURE_CLIENT_SECRET' environment variables are set, so ClientSecretCredential with " + "corresponding tenantId, clientId, and clientSecret gets created."); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); @@ -204,6 +295,8 @@ TEST(EnvironmentCredential, ClientSecretDefaultAuthority) using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoTenantId) @@ -211,8 +304,14 @@ TEST(EnvironmentCredential, ClientSecretNoTenantId) using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + static_cast(CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -225,16 +324,45 @@ TEST(EnvironmentCredential, ClientSecretNoTenantId) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential was not initialized with underlying credential: Both " + "'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID', and at least one of 'AZURE_CLIENT_SECRET', " + "'AZURE_CLIENT_CERTIFICATE_PATH' needs to be set. Additionally, " + "'AZURE_AUTHORITY_HOST' could be set to override the default authority host." + " Currently:\n" + " * 'AZURE_TENANT_ID' is NOT set\n" + " * 'AZURE_CLIENT_ID' is set\n" + " * 'AZURE_CLIENT_SECRET' is set\n" + " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" + " * 'AZURE_AUTHORITY_HOST' is set\n"); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, - [](auto& credential, auto& tokenRequestContext, auto& context) { + [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential authentication unavailable. " + "See earlier EnvironmentCredential log messages for details."); + log.clear(); + return token; })); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoClientId) @@ -242,8 +370,15 @@ TEST(EnvironmentCredential, ClientSecretNoClientId) using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Warning); // Setting it to Warning instead of Verbose will result + // in less detailed Warning message. + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + static_cast(CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -256,16 +391,36 @@ TEST(EnvironmentCredential, ClientSecretNoClientId) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential was not initialized with underlying credential."); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, - [](auto& credential, auto& tokenRequestContext, auto& context) { + [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential authentication unavailable. " + "See earlier EnvironmentCredential log messages for details."); + log.clear(); + return token; })); + + Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoClientSecret) @@ -273,8 +428,14 @@ TEST(EnvironmentCredential, ClientSecretNoClientSecret) using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + static_cast(CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -287,14 +448,43 @@ TEST(EnvironmentCredential, ClientSecretNoClientSecret) {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential was not initialized with underlying credential: Both " + "'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID', and at least one of 'AZURE_CLIENT_SECRET', " + "'AZURE_CLIENT_CERTIFICATE_PATH' needs to be set. Additionally, " + "'AZURE_AUTHORITY_HOST' could be set to override the default authority host." + " Currently:\n" + " * 'AZURE_TENANT_ID' is set\n" + " * 'AZURE_CLIENT_ID' is set\n" + " * 'AZURE_CLIENT_SECRET' is NOT set\n" + " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" + " * 'AZURE_AUTHORITY_HOST' is set\n"); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, - [](auto& credential, auto& tokenRequestContext, auto& context) { + [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: EnvironmentCredential authentication unavailable. " + "See earlier EnvironmentCredential log messages for details."); + log.clear(); + return token; })); + + Logger::SetListener(nullptr); } diff --git a/sdk/identity/azure-identity/test/ut/managed_identity_credential_test.cpp b/sdk/identity/azure-identity/test/ut/managed_identity_credential_test.cpp index 8ae5e8eb38..2923f91346 100644 --- a/sdk/identity/azure-identity/test/ut/managed_identity_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/managed_identity_credential_test.cpp @@ -4,6 +4,7 @@ #include "azure/identity/managed_identity_credential.hpp" #include "credential_test_helper.hpp" +#include #include @@ -17,8 +18,14 @@ using Azure::Identity::Test::_detail::CredentialTestHelper; TEST(ManagedIdentityCredential, AppServiceV2019) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -31,7 +38,16 @@ TEST(ManagedIdentityCredential, AppServiceV2019) {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, }); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Informational); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential will be created with App Service 2019 source."); + log.clear(); + + return credential; }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, std::vector{ @@ -99,6 +115,8 @@ TEST(ManagedIdentityCredential, AppServiceV2019) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 9999s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 9999s); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, AppServiceV2019ClientId) @@ -193,12 +211,18 @@ TEST(ManagedIdentityCredential, AppServiceV2019ClientId) TEST(ManagedIdentityCredential, AppServiceV2019InvalidUrl) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Credentials::AuthenticationException; static_cast(CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -217,10 +241,21 @@ TEST(ManagedIdentityCredential, AppServiceV2019InvalidUrl) = std::make_unique(options), AuthenticationException); + EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); + EXPECT_EQ(log[0].first, Logger::Level::Warning); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential with App Service 2019 source: " + "Failed to create: The environment variable \'IDENTITY_ENDPOINT\' " + "contains an invalid URL."); + log.clear(); + return appServiceV2019ManagedIdentityCredential; }, {}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, AppServiceV2019UnsupportedUrl) @@ -257,8 +292,14 @@ TEST(ManagedIdentityCredential, AppServiceV2019UnsupportedUrl) TEST(ManagedIdentityCredential, AppServiceV2017) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -271,7 +312,24 @@ TEST(ManagedIdentityCredential, AppServiceV2017) {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, }); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2019 source."); + + EXPECT_EQ(log[1].first, Logger::Level::Informational); + EXPECT_EQ( + log[1].second, + "Identity: ManagedIdentityCredential will be created with App Service 2017 source."); + + log.clear(); + + return credential; }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, std::vector{ @@ -339,6 +397,8 @@ TEST(ManagedIdentityCredential, AppServiceV2017) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 9999s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 9999s); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, AppServiceV2017ClientId) @@ -497,8 +557,14 @@ TEST(ManagedIdentityCredential, AppServiceV2017UnsupportedUrl) TEST(ManagedIdentityCredential, CloudShell) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -511,7 +577,30 @@ TEST(ManagedIdentityCredential, CloudShell) {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, }); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(3)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2019 source."); + + EXPECT_EQ(log[1].first, Logger::Level::Verbose); + EXPECT_EQ( + log[1].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2017 source."); + + EXPECT_EQ(log[2].first, Logger::Level::Informational); + EXPECT_EQ( + log[2].second, + "Identity: ManagedIdentityCredential will be created with Cloud Shell source."); + + log.clear(); + + return credential; }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, std::vector{ @@ -566,6 +655,8 @@ TEST(ManagedIdentityCredential, CloudShell) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 9999s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 9999s); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, CloudShellClientId) @@ -681,6 +772,12 @@ TEST(ManagedIdentityCredential, CloudShellInvalidUrl) TEST(ManagedIdentityCredential, AzureArc) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + { std::ofstream secretFile( "managed_identity_credential_test1.txt", std::ios_base::out | std::ios_base::trunc); @@ -703,7 +800,7 @@ TEST(ManagedIdentityCredential, AzureArc) } auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -716,7 +813,36 @@ TEST(ManagedIdentityCredential, AzureArc) {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, }); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(4)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2019 source."); + + EXPECT_EQ(log[1].first, Logger::Level::Verbose); + EXPECT_EQ( + log[1].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2017 source."); + + EXPECT_EQ(log[2].first, Logger::Level::Verbose); + EXPECT_EQ( + log[2].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with Cloud Shell source."); + + EXPECT_EQ(log[3].first, Logger::Level::Informational); + EXPECT_EQ( + log[3].second, + "Identity: ManagedIdentityCredential will be created with Azure Arc source."); + + log.clear(); + + return credential; }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, {{HttpStatusCode::Unauthorized, @@ -829,6 +955,8 @@ TEST(ManagedIdentityCredential, AzureArc) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 9999s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 9999s); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, AzureArcClientId) @@ -1032,8 +1160,14 @@ TEST(ManagedIdentityCredential, AzureArcInvalidUrl) TEST(ManagedIdentityCredential, Imds) { + using Azure::Core::Diagnostics::Logger; + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { + [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; @@ -1046,7 +1180,44 @@ TEST(ManagedIdentityCredential, Imds) {"IDENTITY_SERVER_THUMBPRINT", ""}, }); - return std::make_unique(options); + auto credential = std::make_unique(options); + + EXPECT_EQ(log.size(), LogMsgVec::size_type(5)); + + EXPECT_EQ(log[0].first, Logger::Level::Verbose); + EXPECT_EQ( + log[0].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2019 source."); + + EXPECT_EQ(log[1].first, Logger::Level::Verbose); + EXPECT_EQ( + log[1].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with App Service 2017 source."); + + EXPECT_EQ(log[2].first, Logger::Level::Verbose); + EXPECT_EQ( + log[2].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with Cloud Shell source."); + + EXPECT_EQ(log[3].first, Logger::Level::Verbose); + EXPECT_EQ( + log[3].second, + "Identity: ManagedIdentityCredential: Environment is not set up for the credential " + "to be created with Azure Arc source."); + + EXPECT_EQ(log[4].first, Logger::Level::Informational); + EXPECT_EQ( + log[4].second, + "Identity: ManagedIdentityCredential will be created " + "with Azure Instance Metadata Service source." + "\nSuccessful creation does not guarantee further successful token retrieval."); + + log.clear(); + + return credential; }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, std::vector{ @@ -1114,6 +1285,8 @@ TEST(ManagedIdentityCredential, Imds) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 9999s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 9999s); + + Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, ImdsClientId) diff --git a/sdk/identity/azure-identity/test/ut/simplified_header_test.cpp b/sdk/identity/azure-identity/test/ut/simplified_header_test.cpp index 313509db46..fa2e67b758 100644 --- a/sdk/identity/azure-identity/test/ut/simplified_header_test.cpp +++ b/sdk/identity/azure-identity/test/ut/simplified_header_test.cpp @@ -9,17 +9,22 @@ */ #include + #include -class DllExportTest final { - AZ_IDENTITY_DLLEXPORT static const bool DllExportHIncluded; -}; +#if !defined(AZ_IDENTITY_DLLEXPORT) +#error "azure/identity.hpp does not include dll_import_export.hpp" +#endif TEST(SimplifiedHeader, identity) { using namespace Azure::Identity; - EXPECT_NO_THROW(ClientSecretCredential clientSecretCredential("", "", "")); - EXPECT_NO_THROW(EnvironmentCredential environmentCredential); - EXPECT_NO_THROW(static_cast(static_cast(nullptr))); + static_cast(sizeof(AzureCliCredential)); + static_cast(sizeof(ChainedTokenCredential)); + static_cast(sizeof(ClientCertificateCredential)); + static_cast(sizeof(ClientSecretCredential)); + static_cast(sizeof(DefaultAzureCredential)); + static_cast(sizeof(EnvironmentCredential)); + static_cast(sizeof(ManagedIdentityCredential)); } diff --git a/sdk/identity/ci.yml b/sdk/identity/ci.yml index f3d26dd382..90162d4ebf 100644 --- a/sdk/identity/ci.yml +++ b/sdk/identity/ci.yml @@ -29,7 +29,7 @@ stages: CtestRegex: azure-identity. LiveTestCtestRegex: azure-identity. LineCoverageTarget: 95 - BranchCoverageTarget: 57 + BranchCoverageTarget: 56 Artifacts: - Name: azure-identity Path: azure-identity