From 54a7bc363b433ae7741dfb04a30ba1bbe5fe75d7 Mon Sep 17 00:00:00 2001 From: Ahson Khan Date: Fri, 16 Aug 2024 20:47:05 -0700 Subject: [PATCH] Add support for providing an object ID to ManagedIdentityCredential. (#5910) * Add support for providing an object ID to ManagedIdentityCredential. * Update cspell checks. * Add support for creating a Uuid from a string, and use that as the MICred overload for objectId. * Add a remark comment to make it clear that object and client ids are not interchangeable. * Remove the non-const ToString() as that isn't required. * Update tests to use valid hex Uuid values. * Use a discriminated union design approach with a ManagedIdentityType object and ManagedIdentityIdType enum. * Fix typo and remove Uuid CreateFromString. * Address PR feedback. * Update doc comment. * Update comments and exception message to consistently use hyphens between user/system and assigned. --- sdk/core/azure-core/CHANGELOG.md | 1 + sdk/core/azure-core/inc/azure/core/uuid.hpp | 4 +- sdk/core/azure-core/src/uuid.cpp | 2 +- sdk/identity/azure-identity/CHANGELOG.md | 2 + .../identity/managed_identity_credential.hpp | 93 ++ .../src/managed_identity_credential.cpp | 35 +- .../src/managed_identity_source.cpp | 46 +- .../src/private/managed_identity_source.hpp | 12 + .../ut/managed_identity_credential_test.cpp | 1223 +++++++++++++---- 9 files changed, 1162 insertions(+), 256 deletions(-) diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 2ed224995..81efe5cc9 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - Request logs to now include the `accept-range`, `content-range`, `range`, `WWW-Authenticate`, `x-ms-date`, `x-ms-error-code`, `x-ms-range`, and `x-ms-version` headers. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/core/azure-core/inc/azure/core/uuid.hpp b/sdk/core/azure-core/inc/azure/core/uuid.hpp index af79f6625..a6bc8190e 100644 --- a/sdk/core/azure-core/inc/azure/core/uuid.hpp +++ b/sdk/core/azure-core/inc/azure/core/uuid.hpp @@ -33,9 +33,9 @@ namespace Azure { namespace Core { public: /** * @brief Gets Uuid as a string. - * @details A string is in canonical format (4-2-2-2-6 lowercase hex and dashes only). + * @details A string is in canonical format (8-4-4-4-12 lowercase hex and dashes only). */ - std::string ToString(); + std::string ToString() const; /** * @brief Returns the binary value of the Uuid for consumption by clients who need non-string diff --git a/sdk/core/azure-core/src/uuid.cpp b/sdk/core/azure-core/src/uuid.cpp index 9242e11ce..463a8bad4 100644 --- a/sdk/core/azure-core/src/uuid.cpp +++ b/sdk/core/azure-core/src/uuid.cpp @@ -17,7 +17,7 @@ static thread_local std::mt19937_64 randomGenerator(std::random_device{}()); #endif namespace Azure { namespace Core { - std::string Uuid::ToString() + std::string Uuid::ToString() const { // Guid is 36 characters // Add one byte for the \0 diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 8accdaa8c..89773d52a 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for providing an object ID to `ManagedIdentityCredential`. + ### Breaking Changes - Previously, if a clientId was specified for Cloud Shell managed identity, which is not supported, the clientId was passed into the request body. Now, an exception will be thrown if a clientId is specified for Cloud Shell managed identity. 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 bbeec211b..9d58a1686 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 @@ -42,6 +42,91 @@ namespace Azure { namespace Identity { std::string ToString() const { return m_resourceId; } }; + /** + * @brief The type of managed identity identifier depending on how the managed identity is + * configured. + * + * @remark This can either be system-assigned, or user-assigned which corresponds to an identifier + * that represents either client ID, resource ID, or object ID, depending on how the managed + * identity is configured. + */ + enum class ManagedIdentityIdType + { + SystemAssigned, + ClientId, + ObjectId, + ResourceId, + }; + + /** + * @brief The type of managed identity and its corresponding identifier. + * + * @remark This class holds the type and unique identifier for either a system or user-assigned + * managed identity. + */ + class ManagedIdentityType final { + private: + ManagedIdentityIdType m_idType; + std::string m_id; + + public: + /** + * @brief Constructs the type of managed identity. + * + * @remark This defaults to ManagedIdentityIdType::SystemAssigned. + */ + explicit ManagedIdentityType() : m_idType(ManagedIdentityIdType::SystemAssigned) {} + + /** + * @brief Constructs the type of managed identity. + * + * @param idType The type of the managed identity identifier. + * @param id The value of the managed identity identifier. This can be either a client ID, + * resource ID, or object ID. + * + * @remark For ManagedIdentityIdType::SystemAssigned, the id must be an empty string. + * + * @remark Make sure the type of ID matches the value of the ID. For example, the client + * ID and object ID are NOT interchangeable, even though they are both Uuid values. + */ + explicit ManagedIdentityType(ManagedIdentityIdType idType, std::string id) + : m_idType(idType), m_id(id) + { + if (idType == ManagedIdentityIdType::SystemAssigned && !id.empty()) + { + throw std::invalid_argument( + "There is no need to provide an ID (such as client, object, or resource ID) if you are " + "using system-assigned managed identity."); + } + } + + /** + * @brief Gets the identifier for a user-assigned managed identity. + * + * @remark In the case of system-assigned managed identity, this will return an empty string. + */ + std::string const& GetId() const { return m_id; } + + /** + * @brief Gets the type of identifier used for the managed identity, depending on how it is + * configured. + */ + ManagedIdentityIdType GetManagedIdentityIdType() const { return m_idType; } + }; + + /** + * @brief Options for managed identity credential. + * + */ + struct ManagedIdentityCredentialOptions final : public Core::Credentials::TokenCredentialOptions + { + /** + * @brief Specifies the type of managed identity and its corresponding identifier, based on how + * it was configured. + */ + ManagedIdentityType IdentityType; + }; + /** * @brief Attempts authentication using a managed identity that has been assigned to the * deployment environment. This authentication type works in Azure VMs, App Service and Azure @@ -71,6 +156,14 @@ namespace Azure { namespace Identity { Azure::Core::Credentials::TokenCredentialOptions const& options = Azure::Core::Credentials::TokenCredentialOptions()); + /** + * @brief Constructs a Managed Identity Credential. + * + * @param options Options for token retrieval. + */ + explicit ManagedIdentityCredential( + Azure::Identity::ManagedIdentityCredentialOptions const& options); + /** * @brief Constructs an instance of ManagedIdentityCredential capable of authenticating a * resource with a user-assigned managed identity. diff --git a/sdk/identity/azure-identity/src/managed_identity_credential.cpp b/sdk/identity/azure-identity/src/managed_identity_credential.cpp index bcb97d646..9a8fe9652 100644 --- a/sdk/identity/azure-identity/src/managed_identity_credential.cpp +++ b/sdk/identity/azure-identity/src/managed_identity_credential.cpp @@ -11,6 +11,7 @@ namespace { std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( std::string const& credentialName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { @@ -19,6 +20,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( static std::unique_ptr (*managedIdentitySourceCreate[])( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, TokenCredentialOptions const& options) = {AppServiceV2019ManagedIdentitySource::Create, @@ -31,7 +33,7 @@ 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, resourceId, options)) + if (auto source = create(credentialName, clientId, objectId, resourceId, options)) { return source; } @@ -49,7 +51,34 @@ ManagedIdentityCredential::ManagedIdentityCredential( Azure::Core::Credentials::TokenCredentialOptions const& options) : TokenCredential("ManagedIdentityCredential") { - m_managedIdentitySource = CreateManagedIdentitySource(GetCredentialName(), clientId, {}, options); + m_managedIdentitySource + = CreateManagedIdentitySource(GetCredentialName(), clientId, {}, {}, options); +} + +ManagedIdentityCredential::ManagedIdentityCredential( + Azure::Identity::ManagedIdentityCredentialOptions const& options) + : TokenCredential("ManagedIdentityCredential") +{ + ManagedIdentityIdType idType = options.IdentityType.GetManagedIdentityIdType(); + switch (idType) + { + case ManagedIdentityIdType::SystemAssigned: + m_managedIdentitySource + = CreateManagedIdentitySource(GetCredentialName(), {}, {}, {}, options); + break; + case ManagedIdentityIdType::ClientId: + m_managedIdentitySource = CreateManagedIdentitySource( + GetCredentialName(), options.IdentityType.GetId(), {}, {}, options); + break; + case ManagedIdentityIdType::ObjectId: + m_managedIdentitySource = CreateManagedIdentitySource( + GetCredentialName(), {}, options.IdentityType.GetId(), {}, options); + break; + case ManagedIdentityIdType::ResourceId: + m_managedIdentitySource = CreateManagedIdentitySource( + GetCredentialName(), {}, {}, options.IdentityType.GetId(), options); + break; + } } ManagedIdentityCredential::ManagedIdentityCredential( @@ -58,7 +87,7 @@ ManagedIdentityCredential::ManagedIdentityCredential( : TokenCredential("ManagedIdentityCredential") { m_managedIdentitySource - = CreateManagedIdentitySource(GetCredentialName(), {}, resourceId.ToString(), options); + = CreateManagedIdentitySource(GetCredentialName(), {}, {}, resourceId.ToString(), options); } ManagedIdentityCredential::ManagedIdentityCredential( diff --git a/sdk/identity/azure-identity/src/managed_identity_source.cpp b/sdk/identity/azure-identity/src/managed_identity_source.cpp index 2f69ea3e1..caf923000 100644 --- a/sdk/identity/azure-identity/src/managed_identity_source.cpp +++ b/sdk/identity/azure-identity/src/managed_identity_source.cpp @@ -140,6 +140,7 @@ template std::unique_ptr AppServiceManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options, char const* endpointVarName, @@ -155,6 +156,7 @@ std::unique_ptr AppServiceManagedIdentitySource::Create( { return std::unique_ptr(new T( clientId, + objectId, resourceId, options, ParseEndpointUrl(credName, msiEndpoint, endpointVarName, credSource), @@ -167,6 +169,7 @@ std::unique_ptr AppServiceManagedIdentitySource::Create( AppServiceManagedIdentitySource::AppServiceManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options, Azure::Core::Url endpointUrl, @@ -183,13 +186,19 @@ AppServiceManagedIdentitySource::AppServiceManagedIdentitySource( url.AppendQueryParameter("api-version", apiVersion); - // Only one of clientId or resourceId will be set to a non-empty value. + // Only one of clientId, objectId, or resourceId will be set to a non-empty value. // AppService uses mi_res_id, and not msi_res_id: // https://learn.microsoft.com/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#rest-endpoint-reference + // Based on the App Service documentation, using principal_id for the query parameter name here + // instead of object_id (which is used as an alias). if (!clientId.empty()) { url.AppendQueryParameter(clientIdHeaderName, clientId); } + else if (!objectId.empty()) + { + url.AppendQueryParameter("principal_id", objectId); + } else if (!resourceId.empty()) { url.AppendQueryParameter("mi_res_id", resourceId); @@ -233,26 +242,36 @@ Azure::Core::Credentials::AccessToken AppServiceManagedIdentitySource::GetToken( std::unique_ptr AppServiceV2017ManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - credName, clientId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); + credName, clientId, objectId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); } std::unique_ptr AppServiceV2019ManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - credName, clientId, resourceId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER", "2019"); + credName, + clientId, + objectId, + resourceId, + options, + "IDENTITY_ENDPOINT", + "IDENTITY_HEADER", + "2019"); } std::unique_ptr CloudShellManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { @@ -265,11 +284,11 @@ std::unique_ptr CloudShellManagedIdentitySource::Create( if (!msiEndpoint.empty()) { - if (!clientId.empty() || !resourceId.empty()) + if (!clientId.empty() || !objectId.empty() || !resourceId.empty()) { throw AuthenticationException( "User-assigned managed identities are not supported in Cloud Shell environments. Omit " - "the clientId or resourceId when constructing the ManagedIdentityCredential."); + "the clientId, objectId, or resourceId when constructing the ManagedIdentityCredential."); } return std::unique_ptr(new CloudShellManagedIdentitySource( @@ -328,6 +347,7 @@ Azure::Core::Credentials::AccessToken CloudShellManagedIdentitySource::GetToken( std::unique_ptr AzureArcManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { @@ -344,12 +364,12 @@ std::unique_ptr AzureArcManagedIdentitySource::Create( return nullptr; } - if (!clientId.empty() || !resourceId.empty()) + if (!clientId.empty() || !objectId.empty() || !resourceId.empty()) { throw AuthenticationException( "User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. " - "To authenticate with the system assigned identity, omit the client or resource ID " - "when constructing the ManagedIdentityCredential."); + "To authenticate with the system assigned identity, omit the client, object, or resource " + "ID when constructing the ManagedIdentityCredential."); } return std::unique_ptr(new AzureArcManagedIdentitySource( @@ -455,6 +475,7 @@ Azure::Core::Credentials::AccessToken AzureArcManagedIdentitySource::GetToken( std::unique_ptr ImdsManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { @@ -475,11 +496,12 @@ std::unique_ptr ImdsManagedIdentitySource::Create( imdsUrl.AppendPath("/metadata/identity/oauth2/token"); return std::unique_ptr( - new ImdsManagedIdentitySource(clientId, resourceId, imdsUrl, options)); + new ImdsManagedIdentitySource(clientId, objectId, resourceId, imdsUrl, options)); } ImdsManagedIdentitySource::ImdsManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Azure::Core::Url const& imdsUrl, Azure::Core::Credentials::TokenCredentialOptions const& options) @@ -492,13 +514,17 @@ ImdsManagedIdentitySource::ImdsManagedIdentitySource( url.AppendQueryParameter("api-version", "2018-02-01"); - // Only one of clientId or resourceId will be set to a non-empty value. + // Only one of clientId, objectId, or resourceId will be set to a non-empty value. // IMDS uses msi_res_id, and not mi_res_id: // https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http if (!clientId.empty()) { url.AppendQueryParameter("client_id", clientId); } + else if (!objectId.empty()) + { + url.AppendQueryParameter("object_id", objectId); + } else if (!resourceId.empty()) { url.AppendQueryParameter("msi_res_id", resourceId); 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 c2bf5cf94..d8de8e20d 100644 --- a/sdk/identity/azure-identity/src/private/managed_identity_source.hpp +++ b/sdk/identity/azure-identity/src/private/managed_identity_source.hpp @@ -54,6 +54,7 @@ namespace Azure { namespace Identity { namespace _detail { protected: explicit AppServiceManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, @@ -66,6 +67,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, char const* endpointVarName, @@ -84,12 +86,14 @@ namespace Azure { namespace Identity { namespace _detail { private: explicit AppServiceV2017ManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, std::string const& secret) : AppServiceManagedIdentitySource( clientId, + objectId, resourceId, options, endpointUrl, @@ -104,6 +108,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); }; @@ -114,12 +119,14 @@ namespace Azure { namespace Identity { namespace _detail { private: explicit AppServiceV2019ManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, std::string const& secret) : AppServiceManagedIdentitySource( clientId, + objectId, resourceId, options, endpointUrl, @@ -134,6 +141,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); }; @@ -151,6 +159,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); @@ -171,6 +180,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); @@ -185,6 +195,7 @@ namespace Azure { namespace Identity { namespace _detail { explicit ImdsManagedIdentitySource( std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Url const& imdsUrl, Core::Credentials::TokenCredentialOptions const& options); @@ -193,6 +204,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& objectId, std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); 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 3aaee6f4f..efe1787dd 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 @@ -26,13 +26,59 @@ #include #endif +using Azure::Core::Uuid; using Azure::Core::Credentials::TokenCredentialOptions; using Azure::Core::Http::HttpMethod; using Azure::Core::Http::HttpStatusCode; using Azure::Identity::ManagedIdentityCredential; +using Azure::Identity::ManagedIdentityCredentialOptions; +using Azure::Identity::ManagedIdentityIdType; +using Azure::Identity::ManagedIdentityType; using Azure::Identity::ResourceIdentifier; using Azure::Identity::Test::_detail::CredentialTestHelper; +TEST(ManagedIdentityType, Basic) +{ + { + ManagedIdentityType const miType; + EXPECT_EQ(miType.GetId(), ""); + EXPECT_EQ(miType.GetManagedIdentityIdType(), ManagedIdentityIdType::SystemAssigned); + } + { + ManagedIdentityType const miType(ManagedIdentityIdType::SystemAssigned, ""); + EXPECT_EQ(miType.GetId(), ""); + EXPECT_EQ(miType.GetManagedIdentityIdType(), ManagedIdentityIdType::SystemAssigned); + } + { + ManagedIdentityType const miType(ManagedIdentityIdType::ClientId, "clientId"); + EXPECT_EQ(miType.GetId(), "clientId"); + EXPECT_EQ(miType.GetManagedIdentityIdType(), ManagedIdentityIdType::ClientId); + } + { + ManagedIdentityType const miType(ManagedIdentityIdType::ObjectId, "objectId"); + EXPECT_EQ(miType.GetId(), "objectId"); + EXPECT_EQ(miType.GetManagedIdentityIdType(), ManagedIdentityIdType::ObjectId); + } + { + ManagedIdentityType const miType(ManagedIdentityIdType::ResourceId, "resourceId"); + EXPECT_EQ(miType.GetId(), "resourceId"); + EXPECT_EQ(miType.GetManagedIdentityIdType(), ManagedIdentityIdType::ResourceId); + } + { + ManagedIdentityCredentialOptions options; + EXPECT_EQ(options.IdentityType.GetId(), ""); + EXPECT_EQ( + options.IdentityType.GetManagedIdentityIdType(), ManagedIdentityIdType::SystemAssigned); + } +} + +TEST(ManagedIdentityType, Invalid) +{ + EXPECT_THROW( + ManagedIdentityType(ManagedIdentityIdType::SystemAssigned, "clientId"), + std::invalid_argument); +} + TEST(ManagedIdentityCredential, GetCredentialName) { CredentialTestHelper::EnvironmentOverride const env({ @@ -331,6 +377,97 @@ TEST(ManagedIdentityCredential, AppServiceV2019ResourceId) EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } +TEST(ManagedIdentityCredential, AppServiceV2019ObjectId) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ObjectId, "abcdef01-2345-6789-0876-543210fedcba"); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", "https://microsoft.com/"}, + {"MSI_SECRET", "CLIENTSECRET1"}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", "CLIENTSECRET2"}, + {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, + }); + + return std::make_unique(options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); + + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba"); // cspell:disable-line + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("X-IDENTITY-HEADER"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("X-IDENTITY-HEADER"), "CLIENTSECRET2"); + + EXPECT_NE(request1.Headers.find("X-IDENTITY-HEADER"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("X-IDENTITY-HEADER"), "CLIENTSECRET2"); + + EXPECT_NE(request2.Headers.find("X-IDENTITY-HEADER"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("X-IDENTITY-HEADER"), "CLIENTSECRET2"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); +} + TEST(ManagedIdentityCredential, AppServiceV2019InvalidUrl) { using Azure::Core::Diagnostics::Logger; @@ -703,6 +840,97 @@ TEST(ManagedIdentityCredential, AppServiceV2017ResourceId) EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } +TEST(ManagedIdentityCredential, AppServiceV2017ObjectId) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ObjectId, "abcdef01-2345-6789-0876-543210fedcba"); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", "https://microsoft.com/"}, + {"MSI_SECRET", "CLIENTSECRET1"}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", "CLIENTSECRET2"}, + {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, + }); + + return std::make_unique(options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); + + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "https://microsoft.com" + "?api-version=2017-09-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://microsoft.com" + "?api-version=2017-09-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "https://microsoft.com" + "?api-version=2017-09-01" + "&principal_id=abcdef01-2345-6789-0876-543210fedcba"); // cspell:disable-line + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("secret"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("secret"), "CLIENTSECRET1"); + + EXPECT_NE(request1.Headers.find("secret"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("secret"), "CLIENTSECRET1"); + + EXPECT_NE(request2.Headers.find("secret"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("secret"), "CLIENTSECRET1"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); +} + TEST(ManagedIdentityCredential, AppServiceV2017InvalidUrl) { using Azure::Core::Credentials::AccessToken; @@ -931,6 +1159,38 @@ TEST(ManagedIdentityCredential, CloudShellResourceId) {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); } +TEST(ManagedIdentityCredential, CloudShellObjectId) +{ + using Azure::Core::Credentials::AuthenticationException; + + static_cast(CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ObjectId, "abcdef01-2345-6789-0876-543210fedcba"); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", "https://microsoft.com/"}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, + }); + + std::unique_ptr cloudShellManagedIdentityCredential; + EXPECT_THROW( + cloudShellManagedIdentityCredential + = std::make_unique(options), + AuthenticationException); + + return cloudShellManagedIdentityCredential; + }, + {}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); +} + TEST(ManagedIdentityCredential, CloudShellScope) { auto const actual = CredentialTestHelper::SimulateTokenRequest( @@ -1325,6 +1585,39 @@ TEST(ManagedIdentityCredential, AzureArcResourceId) using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; + static_cast(CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ObjectId, "abcdef01-2345-6789-0876-543210fedcba"); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", "0123456789abcdef0123456789abcdef01234567"}, + }); + + std::unique_ptr azureArcManagedIdentityCredential; + EXPECT_THROW( + azureArcManagedIdentityCredential + = std::make_unique(options), + AuthenticationException); + + return azureArcManagedIdentityCredential; + }, + {}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); +} + +TEST(ManagedIdentityCredential, AzureArcObjectId) +{ + using Azure::Core::Credentials::AccessToken; + using Azure::Core::Credentials::AuthenticationException; + static_cast(CredentialTestHelper::SimulateTokenRequest( [](auto transport) { TokenCredentialOptions options; @@ -1760,276 +2053,727 @@ TEST(ManagedIdentityCredential, Imds) 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) { - TokenCredentialOptions options; - options.Transport.Transport = transport; - - CredentialTestHelper::EnvironmentOverride const env({ - {"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(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{ - "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", - "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", - "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}", - "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN4\", \"refresh_in\":9999}", - "{\"expires_in\":7199, \"access_token\":\"ACCESSTOKEN5\"}", - "{\"expires_in\":7202, \"access_token\":\"ACCESSTOKEN6\"}"}); - - EXPECT_EQ(actual.Requests.size(), 6U); - EXPECT_EQ(actual.Responses.size(), 6U); - - auto const& request0 = actual.Requests.at(0); - auto const& request1 = actual.Requests.at(1); - auto const& request2 = actual.Requests.at(2); - auto const& request3 = actual.Requests.at(3); - auto const& request4 = actual.Requests.at(4); - auto const& request5 = actual.Requests.at(5); - - auto const& response0 = actual.Responses.at(0); - auto const& response1 = actual.Responses.at(1); - auto const& response2 = actual.Responses.at(2); - auto const& response3 = actual.Responses.at(3); - auto const& response4 = actual.Responses.at(4); - auto const& response5 = actual.Responses.at(5); - - EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request3.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request4.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request5.HttpMethod, HttpMethod::Get); - - EXPECT_EQ( - request0.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01" - "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line - - EXPECT_EQ( - request1.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01" - "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line - - EXPECT_EQ( - request2.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01"); - - EXPECT_EQ( - request3.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01"); - - EXPECT_EQ( - request4.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01"); - - EXPECT_EQ( - request5.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01"); - - EXPECT_TRUE(request0.Body.empty()); - EXPECT_TRUE(request1.Body.empty()); - EXPECT_TRUE(request2.Body.empty()); - EXPECT_TRUE(request3.Body.empty()); - EXPECT_TRUE(request4.Body.empty()); - EXPECT_TRUE(request5.Body.empty()); - { - EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); - EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [&](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; - EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); - EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); - EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); - EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + auto credential = std::make_unique(options); - EXPECT_NE(request3.Headers.find("Metadata"), request3.Headers.end()); - EXPECT_EQ(request3.Headers.at("Metadata"), "true"); + EXPECT_EQ(log.size(), LogMsgVec::size_type(5)); - EXPECT_NE(request4.Headers.find("Metadata"), request4.Headers.end()); - EXPECT_EQ(request4.Headers.at("Metadata"), "true"); + 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_NE(request5.Headers.find("Metadata"), request5.Headers.end()); - EXPECT_EQ(request5.Headers.at("Metadata"), "true"); + 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{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN4\", \"refresh_in\":9999}", + "{\"expires_in\":7199, \"access_token\":\"ACCESSTOKEN5\"}", + "{\"expires_in\":7202, \"access_token\":\"ACCESSTOKEN6\"}"}); + + EXPECT_EQ(actual.Requests.size(), 6U); + EXPECT_EQ(actual.Responses.size(), 6U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + auto const& request3 = actual.Requests.at(3); + auto const& request4 = actual.Requests.at(4); + auto const& request5 = actual.Requests.at(5); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + auto const& response3 = actual.Responses.at(3); + auto const& response4 = actual.Responses.at(4); + auto const& response5 = actual.Responses.at(5); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request3.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request4.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request5.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request3.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request4.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request5.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + EXPECT_TRUE(request3.Body.empty()); + EXPECT_TRUE(request4.Body.empty()); + EXPECT_TRUE(request5.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + + EXPECT_NE(request3.Headers.find("Metadata"), request3.Headers.end()); + EXPECT_EQ(request3.Headers.at("Metadata"), "true"); + + EXPECT_NE(request4.Headers.find("Metadata"), request4.Headers.end()); + EXPECT_EQ(request4.Headers.at("Metadata"), "true"); + + EXPECT_NE(request5.Headers.find("Metadata"), request5.Headers.end()); + EXPECT_EQ(request5.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + EXPECT_EQ(response3.AccessToken.Token, "ACCESSTOKEN4"); + EXPECT_EQ(response4.AccessToken.Token, "ACCESSTOKEN5"); + EXPECT_EQ(response5.AccessToken.Token, "ACCESSTOKEN6"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + + EXPECT_GE(response3.AccessToken.ExpiresOn, response3.EarliestExpiration + 9999s); + EXPECT_LE(response3.AccessToken.ExpiresOn, response3.LatestExpiration + 9999s); + + EXPECT_GE(response4.AccessToken.ExpiresOn, response4.EarliestExpiration + 7199s); + EXPECT_LE(response4.AccessToken.ExpiresOn, response4.LatestExpiration + 7199s); + + EXPECT_GE(response5.AccessToken.ExpiresOn, response5.EarliestExpiration + 3601s); + EXPECT_LE(response5.AccessToken.ExpiresOn, response5.LatestExpiration + 3601s); } + { + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [&](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType(ManagedIdentityIdType::SystemAssigned, ""); - EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); - EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); - EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); - EXPECT_EQ(response3.AccessToken.Token, "ACCESSTOKEN4"); - EXPECT_EQ(response4.AccessToken.Token, "ACCESSTOKEN5"); - EXPECT_EQ(response5.AccessToken.Token, "ACCESSTOKEN6"); + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); - using namespace std::chrono_literals; - EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); - EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + log.clear(); - EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); - EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + auto credential = std::make_unique(options); - EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); - EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + EXPECT_EQ(log.size(), LogMsgVec::size_type(5)); - EXPECT_GE(response3.AccessToken.ExpiresOn, response3.EarliestExpiration + 9999s); - EXPECT_LE(response3.AccessToken.ExpiresOn, response3.LatestExpiration + 9999s); + 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_GE(response4.AccessToken.ExpiresOn, response4.EarliestExpiration + 7199s); - EXPECT_LE(response4.AccessToken.ExpiresOn, response4.LatestExpiration + 7199s); + 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_GE(response5.AccessToken.ExpiresOn, response5.EarliestExpiration + 3601s); - EXPECT_LE(response5.AccessToken.ExpiresOn, response5.LatestExpiration + 3601s); + 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{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN4\", \"refresh_in\":9999}", + "{\"expires_in\":7199, \"access_token\":\"ACCESSTOKEN5\"}", + "{\"expires_in\":7202, \"access_token\":\"ACCESSTOKEN6\"}"}); + + EXPECT_EQ(actual.Requests.size(), 6U); + EXPECT_EQ(actual.Responses.size(), 6U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + auto const& request3 = actual.Requests.at(3); + auto const& request4 = actual.Requests.at(4); + auto const& request5 = actual.Requests.at(5); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + auto const& response3 = actual.Responses.at(3); + auto const& response4 = actual.Responses.at(4); + auto const& response5 = actual.Responses.at(5); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request3.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request4.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request5.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request3.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request4.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_EQ( + request5.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + EXPECT_TRUE(request3.Body.empty()); + EXPECT_TRUE(request4.Body.empty()); + EXPECT_TRUE(request5.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + + EXPECT_NE(request3.Headers.find("Metadata"), request3.Headers.end()); + EXPECT_EQ(request3.Headers.at("Metadata"), "true"); + + EXPECT_NE(request4.Headers.find("Metadata"), request4.Headers.end()); + EXPECT_EQ(request4.Headers.at("Metadata"), "true"); + + EXPECT_NE(request5.Headers.find("Metadata"), request5.Headers.end()); + EXPECT_EQ(request5.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + EXPECT_EQ(response3.AccessToken.Token, "ACCESSTOKEN4"); + EXPECT_EQ(response4.AccessToken.Token, "ACCESSTOKEN5"); + EXPECT_EQ(response5.AccessToken.Token, "ACCESSTOKEN6"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + + EXPECT_GE(response3.AccessToken.ExpiresOn, response3.EarliestExpiration + 9999s); + EXPECT_LE(response3.AccessToken.ExpiresOn, response3.LatestExpiration + 9999s); + + EXPECT_GE(response4.AccessToken.ExpiresOn, response4.EarliestExpiration + 7199s); + EXPECT_LE(response4.AccessToken.ExpiresOn, response4.LatestExpiration + 7199s); + + EXPECT_GE(response5.AccessToken.ExpiresOn, response5.EarliestExpiration + 3601s); + EXPECT_LE(response5.AccessToken.ExpiresOn, response5.LatestExpiration + 3601s); + } Logger::SetListener(nullptr); } TEST(ManagedIdentityCredential, ImdsClientId) { - auto const actual = CredentialTestHelper::SimulateTokenRequest( - [](auto transport) { - TokenCredentialOptions options; - options.Transport.Transport = transport; - - CredentialTestHelper::EnvironmentOverride const env({ - {"MSI_ENDPOINT", ""}, - {"MSI_SECRET", ""}, - {"IDENTITY_ENDPOINT", ""}, - {"IMDS_ENDPOINT", ""}, - {"IDENTITY_HEADER", ""}, - {"IDENTITY_SERVER_THUMBPRINT", ""}, - }); - - return std::make_unique( - "fedcba98-7654-3210-0123-456789abcdef", options); - }, - {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, - std::vector{ - "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", - "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", - "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); - - EXPECT_EQ(actual.Requests.size(), 3U); - EXPECT_EQ(actual.Responses.size(), 3U); - - auto const& request0 = actual.Requests.at(0); - auto const& request1 = actual.Requests.at(1); - auto const& request2 = actual.Requests.at(2); - - auto const& response0 = actual.Responses.at(0); - auto const& response1 = actual.Responses.at(1); - auto const& response2 = actual.Responses.at(2); - - EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); - EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); - - EXPECT_EQ( - request0.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01" - "&client_id=fedcba98-7654-3210-0123-456789abcdef" - "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line - - EXPECT_EQ( - request1.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01" - "&client_id=fedcba98-7654-3210-0123-456789abcdef" - "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line - - EXPECT_EQ( - request2.AbsoluteUrl, - "http://169.254.169.254/metadata/identity/oauth2/token" - "?api-version=2018-02-01" - "&client_id=fedcba98-7654-3210-0123-456789abcdef"); - - EXPECT_TRUE(request0.Body.empty()); - EXPECT_TRUE(request1.Body.empty()); - EXPECT_TRUE(request2.Body.empty()); - { - EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); - EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; - EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); - EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); - EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); - EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + return std::make_unique( + "fedcba98-7654-3210-0123-456789abcdef", options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); + + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } + { + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ClientId, "fedcba98-7654-3210-0123-456789abcdef"); - EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); - EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); - EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); - using namespace std::chrono_literals; - EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); - EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + return std::make_unique(options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); - EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); - EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); - EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); - EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=fedcba98-7654-3210-0123-456789abcdef"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + } } TEST(ManagedIdentityCredential, ImdsResourceId) +{ + { + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); + + return std::make_unique( + ResourceIdentifier("abcdef01-2345-6789-9876-543210fedcba"), options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); + + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + } + { + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ManagedIdentityCredentialOptions options; + options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ResourceId, "abcdef01-2345-6789-9876-543210fedcba"); + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + }); + + return std::make_unique(options); + }, + {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}", + "{\"expires_in\":9999, \"access_token\":\"ACCESSTOKEN3\"}"}); + + EXPECT_EQ(actual.Requests.size(), 3U); + EXPECT_EQ(actual.Responses.size(), 3U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + auto const& request2 = actual.Requests.at(2); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + auto const& response2 = actual.Responses.at(2); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + EXPECT_EQ( + request0.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba"); + + EXPECT_TRUE(request0.Body.empty()); + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + EXPECT_NE(request0.Headers.find("Metadata"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Metadata"), "true"); + + EXPECT_NE(request1.Headers.find("Metadata"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Metadata"), "true"); + + EXPECT_NE(request2.Headers.find("Metadata"), request2.Headers.end()); + EXPECT_EQ(request2.Headers.at("Metadata"), "true"); + } + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN3"); + + using namespace std::chrono_literals; + EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s); + EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s); + + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 4999s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); + } +} + +TEST(ManagedIdentityCredential, ImdsObjectId) { auto const actual = CredentialTestHelper::SimulateTokenRequest( [](auto transport) { - TokenCredentialOptions options; + ManagedIdentityCredentialOptions options; options.Transport.Transport = transport; + options.IdentityType = ManagedIdentityType( + ManagedIdentityIdType::ObjectId, "abcdef01-2345-6789-0876-543210fedcba"); CredentialTestHelper::EnvironmentOverride const env({ {"MSI_ENDPOINT", ""}, @@ -2040,8 +2784,7 @@ TEST(ManagedIdentityCredential, ImdsResourceId) {"IDENTITY_SERVER_THUMBPRINT", ""}, }); - return std::make_unique( - ResourceIdentifier("abcdef01-2345-6789-9876-543210fedcba"), options); + return std::make_unique(options); }, {{"https://azure.com/.default"}, {"https://outlook.com/.default"}, {}}, std::vector{ @@ -2068,21 +2811,21 @@ TEST(ManagedIdentityCredential, ImdsResourceId) request0.AbsoluteUrl, "http://169.254.169.254/metadata/identity/oauth2/token" "?api-version=2018-02-01" - "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&object_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line EXPECT_EQ( request1.AbsoluteUrl, "http://169.254.169.254/metadata/identity/oauth2/token" "?api-version=2018-02-01" - "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&object_id=abcdef01-2345-6789-0876-543210fedcba" // cspell:disable-line "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line EXPECT_EQ( request2.AbsoluteUrl, "http://169.254.169.254/metadata/identity/oauth2/token" "?api-version=2018-02-01" - "&msi_res_id=abcdef01-2345-6789-9876-543210fedcba"); + "&object_id=abcdef01-2345-6789-0876-543210fedcba"); // cspell:disable-line EXPECT_TRUE(request0.Body.empty()); EXPECT_TRUE(request1.Body.empty());