diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 6f94fbb52..b7c5ac838 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,10 @@ ### Features Added +- Added `UseProbeRequest` option for `ManagedIdentityCredential`. +- By default, `ManagedIdentityCredential` does not send a probe request, unless it is a part of credential chain in `DefaultAzureCredential`. +- When `AZURE_TOKEN_CREDENTIALS` environment variable is configured to `ManagedIdentityCredential`, the `DefaultAzureCredential` does not issue a probe request and performs retries with exponential backoff. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/inc/azure/identity/managed_identity_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/managed_identity_credential.hpp index b4234a7e4..7488a5b1f 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/managed_identity_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/managed_identity_credential.hpp @@ -168,6 +168,18 @@ namespace Azure { namespace Identity { * it was configured. */ ManagedIdentityId IdentityId; + + /** + * @brief If Azure Instance Metadata Service (IMDS) gets selected as managed identity source, + * specifies whether the first request should be a short probe request (`true`), instead of a + * normal request with retries and exponential backoff (`false`). Default is `false`. + * + * @note When `true`, there's a potential that the credential would not detect IMDS being + * available on a machine, if the response was not received fast enough. When `false` and IMDS + * is not available, credential creation may take tens of seconds until multiple attempts to get + * a response from IMDS would fail. + */ + bool UseProbeRequest = false; }; /** @@ -181,6 +193,11 @@ namespace Azure { namespace Identity { private: std::unique_ptr<_detail::ManagedIdentitySource> m_managedIdentitySource; + explicit ManagedIdentityCredential( + std::string const& clientId, + bool useProbeRequest, + Core::Credentials::TokenCredentialOptions const& options); + public: /** * @brief Destructs `%TokenCredential`. @@ -196,8 +213,8 @@ namespace Azure { namespace Identity { */ explicit ManagedIdentityCredential( std::string const& clientId = std::string(), - Azure::Core::Credentials::TokenCredentialOptions const& options - = Azure::Core::Credentials::TokenCredentialOptions()); + Core::Credentials::TokenCredentialOptions const& options + = Core::Credentials::TokenCredentialOptions()); /** * @brief Constructs a Managed Identity Credential. @@ -212,8 +229,7 @@ namespace Azure { namespace Identity { * * @param options Options for token retrieval. */ - explicit ManagedIdentityCredential( - Azure::Core::Credentials::TokenCredentialOptions const& options); + explicit ManagedIdentityCredential(Core::Credentials::TokenCredentialOptions const& options); /** * @brief Gets an authentication token. diff --git a/sdk/identity/azure-identity/src/default_azure_credential.cpp b/sdk/identity/azure-identity/src/default_azure_credential.cpp index e2ba76220..5f3faa2b5 100644 --- a/sdk/identity/azure-identity/src/default_azure_credential.cpp +++ b/sdk/identity/azure-identity/src/default_azure_credential.cpp @@ -69,25 +69,6 @@ DefaultAzureCredential::DefaultAzureCredential( Create; }; - static const std::array credentials = { - CredentialInfo{ - true, - "EnvironmentCredential", - [](auto options) { return std::make_shared(options); }}, - CredentialInfo{ - true, - "WorkloadIdentityCredential", - [](auto options) { return std::make_shared(options); }}, - CredentialInfo{ - true, - "ManagedIdentityCredential", - [](auto options) { return std::make_shared(options); }}, - CredentialInfo{ - false, - "AzureCliCredential", - [](auto options) { return std::make_shared(options); }}, - }; - const auto envVarValue = Environment::GetVariable(CredentialSpecifierEnvVarName); const auto trimmedEnvVarValue = StringExtensions::Trim(envVarValue); @@ -99,6 +80,35 @@ DefaultAzureCredential::DefaultAzureCredential( } bool specificCred = false; + const std::array credentials = { + CredentialInfo{ + true, + "EnvironmentCredential", + [](auto options) { return std::make_shared(options); }}, + CredentialInfo{ + true, + "WorkloadIdentityCredential", + [](auto options) { return std::make_shared(options); }}, + CredentialInfo{ + true, + "ManagedIdentityCredential", + [&](auto options) { + // If specifically 'ManagedIdentityCredential' is used, do not perform a probe + // request, going for the full retry with exponential backoffs instead. + ManagedIdentityCredentialOptions managedIdentityCredentialOptions; + static_cast( + managedIdentityCredentialOptions) + = options; + + managedIdentityCredentialOptions.UseProbeRequest = !specificCred; + return std::make_shared(managedIdentityCredentialOptions); + }}, + CredentialInfo{ + false, + "AzureCliCredential", + [](auto options) { return std::make_shared(options); }}, + }; + if (!trimmedEnvVarValue.empty()) { for (const auto& cred : credentials) diff --git a/sdk/identity/azure-identity/src/managed_identity_credential.cpp b/sdk/identity/azure-identity/src/managed_identity_credential.cpp index 76ca635f2..c61bc5eff 100644 --- a/sdk/identity/azure-identity/src/managed_identity_credential.cpp +++ b/sdk/identity/azure-identity/src/managed_identity_credential.cpp @@ -14,6 +14,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) { using namespace Azure::Core::Credentials; @@ -23,6 +24,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, TokenCredentialOptions const& options) = {AppServiceV2019ManagedIdentitySource::Create, AppServiceV2017ManagedIdentitySource::Create, @@ -34,7 +36,8 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( // For that reason, it is not possible to cover that execution branch in tests. for (auto create : managedIdentitySourceCreate) { - if (auto source = create(credentialName, clientId, objectId, resourceId, options)) + if (auto source + = create(credentialName, clientId, objectId, resourceId, useProbeRequest, options)) { return source; } @@ -49,11 +52,19 @@ ManagedIdentityCredential::~ManagedIdentityCredential() = default; ManagedIdentityCredential::ManagedIdentityCredential( std::string const& clientId, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) : TokenCredential("ManagedIdentityCredential") { - m_managedIdentitySource - = CreateManagedIdentitySource(GetCredentialName(), clientId, {}, {}, options); + m_managedIdentitySource = CreateManagedIdentitySource( + GetCredentialName(), clientId, {}, {}, useProbeRequest, options); +} + +ManagedIdentityCredential::ManagedIdentityCredential( + std::string const& clientId, + Azure::Core::Credentials::TokenCredentialOptions const& options) + : ManagedIdentityCredential(clientId, false, options) +{ } ManagedIdentityCredential::ManagedIdentityCredential( @@ -64,20 +75,35 @@ ManagedIdentityCredential::ManagedIdentityCredential( switch (idType) { case ManagedIdentityIdKind::SystemAssigned: - m_managedIdentitySource - = CreateManagedIdentitySource(GetCredentialName(), {}, {}, {}, options); + m_managedIdentitySource = CreateManagedIdentitySource( + GetCredentialName(), {}, {}, {}, options.UseProbeRequest, options); break; case ManagedIdentityIdKind::ClientId: m_managedIdentitySource = CreateManagedIdentitySource( - GetCredentialName(), options.IdentityId.GetId(), {}, {}, options); + GetCredentialName(), + options.IdentityId.GetId(), + {}, + {}, + options.UseProbeRequest, + options); break; case ManagedIdentityIdKind::ObjectId: m_managedIdentitySource = CreateManagedIdentitySource( - GetCredentialName(), {}, options.IdentityId.GetId(), {}, options); + GetCredentialName(), + {}, + options.IdentityId.GetId(), + {}, + options.UseProbeRequest, + options); break; case ManagedIdentityIdKind::ResourceId: m_managedIdentitySource = CreateManagedIdentitySource( - GetCredentialName(), {}, {}, options.IdentityId.GetId(), options); + GetCredentialName(), + {}, + {}, + options.IdentityId.GetId(), + options.UseProbeRequest, + options); break; default: throw std::invalid_argument( @@ -88,7 +114,7 @@ ManagedIdentityCredential::ManagedIdentityCredential( ManagedIdentityCredential::ManagedIdentityCredential( Azure::Core::Credentials::TokenCredentialOptions const& options) - : ManagedIdentityCredential(std::string(), options) + : ManagedIdentityCredential({}, false, options) { } diff --git a/sdk/identity/azure-identity/src/managed_identity_source.cpp b/sdk/identity/azure-identity/src/managed_identity_source.cpp index c3d203a58..da09c8429 100644 --- a/sdk/identity/azure-identity/src/managed_identity_source.cpp +++ b/sdk/identity/azure-identity/src/managed_identity_source.cpp @@ -263,8 +263,10 @@ std::unique_ptr AppServiceV2017ManagedIdentitySource::Cre std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options) { + static_cast(useProbeRequest); return AppServiceManagedIdentitySource::Create( credName, clientId, objectId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); } @@ -274,8 +276,10 @@ std::unique_ptr AppServiceV2019ManagedIdentitySource::Cre std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options) { + static_cast(useProbeRequest); return AppServiceManagedIdentitySource::Create( credName, clientId, @@ -292,8 +296,10 @@ std::unique_ptr CloudShellManagedIdentitySource::Create( std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) { + static_cast(useProbeRequest); using Azure::Core::Credentials::AuthenticationException; constexpr auto EndpointVarName = "MSI_ENDPOINT"; @@ -370,8 +376,10 @@ std::unique_ptr AzureArcManagedIdentitySource::Create( std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) { + static_cast(useProbeRequest); using Azure::Core::Credentials::AuthenticationException; constexpr auto EndpointVarName = "IDENTITY_ENDPOINT"; @@ -499,6 +507,7 @@ std::unique_ptr ImdsManagedIdentitySource::Create( std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) { const std::string ImdsName = "Azure Instance Metadata Service"; @@ -529,8 +538,8 @@ std::unique_ptr ImdsManagedIdentitySource::Create( } imdsUrl.SetPath("metadata/identity/oauth2/token"); - return std::unique_ptr( - new ImdsManagedIdentitySource(clientId, objectId, resourceId, imdsUrl, options)); + return std::unique_ptr(new ImdsManagedIdentitySource( + clientId, objectId, resourceId, imdsUrl, useProbeRequest, options)); } ImdsManagedIdentitySource::ImdsManagedIdentitySource( @@ -538,6 +547,7 @@ ImdsManagedIdentitySource::ImdsManagedIdentitySource( std::string const& objectId, std::string const& resourceId, Azure::Core::Url const& imdsUrl, + bool useProbeRequest, Azure::Core::Credentials::TokenCredentialOptions const& options) : ManagedIdentitySource(clientId, std::string(), options), m_request(Azure::Core::Http::HttpMethod::Get, imdsUrl) @@ -569,7 +579,7 @@ ImdsManagedIdentitySource::ImdsManagedIdentitySource( Core::Credentials::TokenCredentialOptions firstRequestOptions = options; firstRequestOptions.Retry.MaxRetries = 0; m_firstRequestPipeline = std::make_unique(firstRequestOptions); - m_firstRequestSucceeded = false; + m_firstRequestSucceeded = !useProbeRequest; } Azure::Core::Credentials::AccessToken ImdsManagedIdentitySource::GetToken( 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 4b86f99ab..91dd2e584 100644 --- a/sdk/identity/azure-identity/src/private/managed_identity_source.hpp +++ b/sdk/identity/azure-identity/src/private/managed_identity_source.hpp @@ -113,6 +113,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); }; @@ -146,6 +147,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); }; @@ -164,6 +166,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( @@ -185,6 +188,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( @@ -204,6 +208,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& objectId, std::string const& resourceId, Core::Url const& imdsUrl, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); public: @@ -212,6 +217,7 @@ namespace Azure { namespace Identity { namespace _detail { std::string const& clientId, std::string const& objectId, std::string const& resourceId, + bool useProbeRequest, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( diff --git a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp index 4b1a84eec..5043fd9ea 100644 --- a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp @@ -528,3 +528,96 @@ TEST(DefaultAzureCredential, RequireCredentialSpecifierEnvVarValue) EXPECT_THROW( static_cast(std::make_unique(true)), AuthenticationException); } + +TEST(DefaultAzureCredential, ImdsProbe) +{ + using Azure::Core::Http::HttpStatusCode; + using Azure::Identity::Test::_detail::CredentialTestHelper; + + constexpr auto ImATeapot = static_cast(418); + + // AZURE_TOKEN_CREDENTIALS is set to "prod", which should result in ManagedIdentityCredential + // using useProbeRequest = true. + EXPECT_THROW( + static_cast(CredentialTestHelper::SimulateTokenRequest( + [&ImATeapot](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + options.Retry.MaxRetries = 3; + options.Retry.RetryDelay = std::chrono::milliseconds(1); + options.Retry.StatusCodes.insert(ImATeapot); + + CredentialTestHelper::EnvironmentOverride const env({ + // Env vars are set to ensure that all the credential chain is inactive all the way + // up to ManagedIdentityCredential with IMDS source. + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + {"AZURE_AUTHORITY_HOST", ""}, + {"AZURE_TENANT_ID", ""}, + {"AZURE_CLIENT_ID", ""}, + {"AZURE_CLIENT_SECRET", ""}, + {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, + {"AZURE_FEDERATED_TOKEN_FILE", ""}, + {"SYSTEM_OIDCREQUESTURI", ""}, + {"AZURE_TOKEN_CREDENTIALS", + "prod"}, // <- should result in MIC with useProbeRequest = true + }); + + return std::make_unique(options); + }, + {{"https://azure.com/.default"}}, + {{ImATeapot, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", {}}, + // Given there aren't going to be any retries due to probe request, the credential + // should never get to make a second request to receive the successful response below. + {HttpStatusCode::Ok, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN2\"}", {}}})), + Azure::Core::Credentials::AuthenticationException); + + // Everything is the same, including the retry policy, but this time AZURE_TOKEN_CREDENTIALS is + // set to "ManagedIdentityCredential", which should result in ManagedIdentityCredential using + // useProbeRequest = false. + auto const whenProbeDisabled = CredentialTestHelper::SimulateTokenRequest( + [&ImATeapot](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + options.Retry.MaxRetries = 3; + options.Retry.RetryDelay = std::chrono::milliseconds(1); + options.Retry.StatusCodes.insert(ImATeapot); + + CredentialTestHelper::EnvironmentOverride const env({ + // Env vars are set to ensure that all the credential chain is inactive all the way + // up to ManagedIdentityCredential with IMDS source. + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + {"AZURE_AUTHORITY_HOST", ""}, + {"AZURE_TENANT_ID", ""}, + {"AZURE_CLIENT_ID", ""}, + {"AZURE_CLIENT_SECRET", ""}, + {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, + {"AZURE_FEDERATED_TOKEN_FILE", ""}, + {"SYSTEM_OIDCREQUESTURI", ""}, + {"AZURE_TOKEN_CREDENTIALS", + "ManagedIdentityCredential"}, // <- should result in MIC with useProbeRequest = false + }); + + return std::make_unique(options); + }, + {{"https://azure.com/.default"}}, + {{ImATeapot, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", {}}, + {HttpStatusCode::Ok, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN2\"}", {}}}); + + EXPECT_EQ(whenProbeDisabled.Requests.size(), 2U); + EXPECT_EQ(whenProbeDisabled.Responses.size(), 1U); + EXPECT_EQ(whenProbeDisabled.Responses.at(0).AccessToken.Token, "ACCESSTOKEN2"); +} 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 9fa671b6f..ada389fa5 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 @@ -3193,4 +3193,72 @@ namespace Azure { namespace Identity { namespace Test { Logger::SetListener(nullptr); } + TEST(ManagedIdentityCredential, ImdsProbe) + { + constexpr auto ImATeapot = static_cast(418); + + EXPECT_THROW( + static_cast(CredentialTestHelper::SimulateTokenRequest( + [&ImATeapot](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + + options.Retry.MaxRetries = 3; + options.Retry.RetryDelay = std::chrono::milliseconds(1); + options.Retry.StatusCodes.insert(ImATeapot); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + }); + + options.UseProbeRequest = true; + return std::make_unique(options); + }, + {{"https://azure.com/.default"}}, + {{ImATeapot, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", {}}, + // Given there aren't going to be any retries due to probe request, the credential + // should never get to make a second request to receive the successful response below. + {HttpStatusCode::Ok, + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN2\"}", + {}}})), + Azure::Core::Credentials::AuthenticationException); + + // Everything is the same, including the retry policy, but this time useProbeRequest = false. + auto const whenProbeDisabled = CredentialTestHelper::SimulateTokenRequest( + [&ImATeapot](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + options.Retry.MaxRetries = 3; + options.Retry.RetryDelay = std::chrono::milliseconds(1); + options.Retry.StatusCodes.insert(ImATeapot); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_POD_IDENTITY_AUTHORITY_HOST", ""}, + }); + + return std::make_unique( + options); // <-- useProbeRequest = false (default) + }, + {{"https://azure.com/.default"}}, + {{ImATeapot, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", {}}, + {HttpStatusCode::Ok, "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN2\"}", {}}}); + + EXPECT_EQ(whenProbeDisabled.Requests.size(), 2U); + EXPECT_EQ(whenProbeDisabled.Responses.size(), 1U); + EXPECT_EQ(whenProbeDisabled.Responses.at(0).AccessToken.Token, "ACCESSTOKEN2"); + } + }}} // namespace Azure::Identity::Test