diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 5bd1c3060..d92a1947a 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -132,6 +132,7 @@ "hlocal", "HLOCAL", "HRESULT", + "imds", "Imds", "IMDS", "immutability", 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 569f443f0..bbeec211b 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 @@ -19,6 +19,29 @@ namespace Azure { namespace Identity { class ManagedIdentitySource; } + // This will move to Azure::Core. + /** + * @brief An Azure Resource Manager resource identifier. + */ + class ResourceIdentifier final { + std::string m_resourceId; + + public: + /** + * @brief Constructs a resource identifier. + * + * @param resourceId The id string to create the ResourceIdentifier from. + */ + explicit ResourceIdentifier(std::string const& resourceId) : m_resourceId(resourceId){}; + + /** + * @brief The string representation of this resource identifier. + * + * @return The resource identifier string. + */ + std::string ToString() const { return m_resourceId; } + }; + /** * @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 @@ -48,6 +71,17 @@ namespace Azure { namespace Identity { Azure::Core::Credentials::TokenCredentialOptions const& options = Azure::Core::Credentials::TokenCredentialOptions()); + /** + * @brief Constructs an instance of ManagedIdentityCredential capable of authenticating a + * resource with a user-assigned managed identity. + * + * @param resourceId The resource ID to authenticate for a user-assigned managed identity. + * @param options Options for token retrieval. + */ + explicit ManagedIdentityCredential( + ResourceIdentifier const& resourceId, + Azure::Core::Credentials::TokenCredentialOptions const& options = {}); + /** * @brief Constructs a Managed Identity Credential. * diff --git a/sdk/identity/azure-identity/src/managed_identity_credential.cpp b/sdk/identity/azure-identity/src/managed_identity_credential.cpp index 7ebfc5cdf..bcb97d646 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& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { using namespace Azure::Core::Credentials; @@ -18,6 +19,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource( static std::unique_ptr (*managedIdentitySourceCreate[])( std::string const& credName, std::string const& clientId, + std::string const& resourceId, TokenCredentialOptions const& options) = {AppServiceV2019ManagedIdentitySource::Create, AppServiceV2017ManagedIdentitySource::Create, @@ -29,7 +31,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, options)) + if (auto source = create(credentialName, clientId, resourceId, options)) { return source; } @@ -47,7 +49,16 @@ ManagedIdentityCredential::ManagedIdentityCredential( Azure::Core::Credentials::TokenCredentialOptions const& options) : TokenCredential("ManagedIdentityCredential") { - m_managedIdentitySource = CreateManagedIdentitySource(GetCredentialName(), clientId, options); + m_managedIdentitySource = CreateManagedIdentitySource(GetCredentialName(), clientId, {}, options); +} + +ManagedIdentityCredential::ManagedIdentityCredential( + ResourceIdentifier const& resourceId, + Azure::Core::Credentials::TokenCredentialOptions const& options) + : TokenCredential("ManagedIdentityCredential") +{ + m_managedIdentitySource + = 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 1b7d6467e..fccdb26dd 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& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options, char const* endpointVarName, char const* secretVarName, @@ -154,6 +155,7 @@ std::unique_ptr AppServiceManagedIdentitySource::Create( { return std::unique_ptr(new T( clientId, + resourceId, options, ParseEndpointUrl(credName, msiEndpoint, endpointVarName, credSource), msiSecret)); @@ -165,6 +167,7 @@ std::unique_ptr AppServiceManagedIdentitySource::Create( AppServiceManagedIdentitySource::AppServiceManagedIdentitySource( std::string const& clientId, + std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options, Azure::Core::Url endpointUrl, std::string const& secret, @@ -180,10 +183,17 @@ AppServiceManagedIdentitySource::AppServiceManagedIdentitySource( url.AppendQueryParameter("api-version", apiVersion); + // Only one of clientId 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 if (!clientId.empty()) { url.AppendQueryParameter(clientIdHeaderName, clientId); } + else if (!resourceId.empty()) + { + url.AppendQueryParameter("mi_res_id", resourceId); + } } m_request.SetHeader(secretHeaderName, secret); @@ -223,24 +233,28 @@ Azure::Core::Credentials::AccessToken AppServiceManagedIdentitySource::GetToken( std::unique_ptr AppServiceV2017ManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - credName, clientId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); + credName, clientId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017"); } std::unique_ptr AppServiceV2019ManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options) { return AppServiceManagedIdentitySource::Create( - credName, clientId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER", "2019"); + credName, clientId, resourceId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER", "2019"); } +// Cloud Shell doesn't support user-assigned managed identities std::unique_ptr CloudShellManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const&, Azure::Core::Credentials::TokenCredentialOptions const& options) { constexpr auto EndpointVarName = "MSI_ENDPOINT"; @@ -315,6 +329,7 @@ Azure::Core::Credentials::AccessToken CloudShellManagedIdentitySource::GetToken( std::unique_ptr AzureArcManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { using Azure::Core::Credentials::AuthenticationException; @@ -330,11 +345,11 @@ std::unique_ptr AzureArcManagedIdentitySource::Create( return nullptr; } - if (!clientId.empty()) + if (!clientId.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 ID " + "To authenticate with the system assigned identity, omit the client or resource ID " "when constructing the ManagedIdentityCredential."); } @@ -348,7 +363,6 @@ AzureArcManagedIdentitySource::AzureArcManagedIdentitySource( : ManagedIdentitySource(std::string(), endpointUrl.GetHost(), options), m_url(std::move(endpointUrl)) { - m_url.AppendQueryParameter("api-version", "2019-11-01"); } @@ -442,6 +456,7 @@ Azure::Core::Credentials::AccessToken AzureArcManagedIdentitySource::GetToken( std::unique_ptr ImdsManagedIdentitySource::Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Azure::Core::Credentials::TokenCredentialOptions const& options) { IdentityLog::Write( @@ -449,16 +464,28 @@ std::unique_ptr ImdsManagedIdentitySource::Create( credName + " will be created" + WithSourceMessage("Azure Instance Metadata Service") + ".\nSuccessful creation does not guarantee further successful token retrieval."); - return std::unique_ptr(new ImdsManagedIdentitySource(clientId, options)); + std::string imdsHost = "http://169.254.169.254"; + std::string customImdsHost = Environment::GetVariable("AZURE_IMDS_CUSTOM_AUTHORITY_HOST"); + if (!customImdsHost.empty()) + { + IdentityLog::Write( + IdentityLog::Level::Informational, "Custom IMDS host is set to: " + customImdsHost); + imdsHost = customImdsHost; + } + Azure::Core::Url imdsUrl(imdsHost); + imdsUrl.AppendPath("/metadata/identity/oauth2/token"); + + return std::unique_ptr( + new ImdsManagedIdentitySource(clientId, resourceId, imdsUrl, options)); } ImdsManagedIdentitySource::ImdsManagedIdentitySource( std::string const& clientId, + std::string const& resourceId, + Azure::Core::Url const& imdsUrl, Azure::Core::Credentials::TokenCredentialOptions const& options) : ManagedIdentitySource(clientId, std::string(), options), - m_request( - Azure::Core::Http::HttpMethod::Get, - Azure::Core::Url("http://169.254.169.254/metadata/identity/oauth2/token")) + m_request(Azure::Core::Http::HttpMethod::Get, imdsUrl) { { using Azure::Core::Url; @@ -466,10 +493,17 @@ ImdsManagedIdentitySource::ImdsManagedIdentitySource( url.AppendQueryParameter("api-version", "2018-02-01"); + // Only one of clientId 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 (!resourceId.empty()) + { + url.AppendQueryParameter("msi_res_id", resourceId); + } } m_request.SetHeader("Metadata", "true"); 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 2b5e9e96f..b7f7e6a82 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& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, std::string const& secret, @@ -65,6 +66,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, char const* endpointVarName, char const* secretVarName, @@ -82,11 +84,13 @@ namespace Azure { namespace Identity { namespace _detail { private: explicit AppServiceV2017ManagedIdentitySource( std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, std::string const& secret) : AppServiceManagedIdentitySource( clientId, + resourceId, options, endpointUrl, secret, @@ -100,6 +104,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); }; @@ -109,11 +114,13 @@ namespace Azure { namespace Identity { namespace _detail { private: explicit AppServiceV2019ManagedIdentitySource( std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options, Core::Url endpointUrl, std::string const& secret) : AppServiceManagedIdentitySource( clientId, + resourceId, options, endpointUrl, secret, @@ -127,6 +134,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); }; @@ -144,6 +152,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( @@ -163,6 +172,7 @@ namespace Azure { namespace Identity { namespace _detail { static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( @@ -176,12 +186,15 @@ namespace Azure { namespace Identity { namespace _detail { explicit ImdsManagedIdentitySource( std::string const& clientId, + std::string const& resourceId, + Core::Url const& imdsUrl, Core::Credentials::TokenCredentialOptions const& options); public: static std::unique_ptr Create( std::string const& credName, std::string const& clientId, + std::string const& resourceId, Core::Credentials::TokenCredentialOptions const& options); Core::Credentials::AccessToken GetToken( diff --git a/sdk/identity/azure-identity/test/ut/CMakeLists.txt b/sdk/identity/azure-identity/test/ut/CMakeLists.txt index fe44d0c43..3b2bbee69 100644 --- a/sdk/identity/azure-identity/test/ut/CMakeLists.txt +++ b/sdk/identity/azure-identity/test/ut/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable ( environment_credential_test.cpp macro_guard_test.cpp managed_identity_credential_test.cpp + resource_identifier_test.cpp simplified_header_test.cpp tenant_id_resolver_test.cpp token_cache_test.cpp 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 edad7e6f4..e182a5ea8 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 @@ -30,6 +30,7 @@ using Azure::Core::Credentials::TokenCredentialOptions; using Azure::Core::Http::HttpMethod; using Azure::Core::Http::HttpStatusCode; using Azure::Identity::ManagedIdentityCredential; +using Azure::Identity::ResourceIdentifier; using Azure::Identity::Test::_detail::CredentialTestHelper; TEST(ManagedIdentityCredential, GetCredentialName) @@ -240,6 +241,96 @@ TEST(ManagedIdentityCredential, AppServiceV2019ClientId) EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } +TEST(ManagedIdentityCredential, AppServiceV2019ResourceId) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + 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( + 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, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&mi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&mi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "https://visualstudio.com" + "?api-version=2019-08-01" + "&mi_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("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; @@ -522,6 +613,96 @@ TEST(ManagedIdentityCredential, AppServiceV2017ClientId) EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } +TEST(ManagedIdentityCredential, AppServiceV2017ResourceId) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + 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( + 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, + "https://microsoft.com" + "?api-version=2017-09-01" + "&mi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://microsoft.com" + "?api-version=2017-09-01" + "&mi_res_id=abcdef01-2345-6789-9876-543210fedcba" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "https://microsoft.com" + "?api-version=2017-09-01" + "&mi_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("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; @@ -770,6 +951,84 @@ TEST(ManagedIdentityCredential, CloudShellClientId) EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 4999s); } +TEST(ManagedIdentityCredential, CloudShellResourceId) +{ + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + 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"}, + }); + + 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::Post); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Post); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Post); + + EXPECT_EQ(request0.AbsoluteUrl, "https://microsoft.com"); + EXPECT_EQ(request1.AbsoluteUrl, "https://microsoft.com"); + EXPECT_EQ(request2.AbsoluteUrl, "https://microsoft.com"); + + EXPECT_EQ(request0.Body, + "resource=https%3A%2F%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ(request1.Body, + "resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_EQ(request2.Body, ""); + + { + 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, CloudShellInvalidUrl) { using Azure::Core::Credentials::AccessToken; @@ -1082,6 +1341,37 @@ TEST(ManagedIdentityCredential, AzureArcClientId) {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); } +TEST(ManagedIdentityCredential, AzureArcResourceId) +{ + using Azure::Core::Credentials::AccessToken; + using Azure::Core::Credentials::AuthenticationException; + + static_cast(CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + 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( + ResourceIdentifier("abcdef01-2345-6789-9876-543210fedcba"), options), + AuthenticationException); + + return azureArcManagedIdentityCredential; + }, + {}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"})); +} + TEST(ManagedIdentityCredential, AzureArcAuthHeaderMissing) { using Azure::Core::Credentials::AccessToken; @@ -1755,6 +2045,96 @@ TEST(ManagedIdentityCredential, ImdsClientId) 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); +} + TEST(ManagedIdentityCredential, ImdsCreation) { auto const actual1 = CredentialTestHelper::SimulateTokenRequest( @@ -1847,3 +2227,127 @@ TEST(ManagedIdentityCredential, ImdsCreation) EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 3600s); EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 3600s); } + +TEST(ManagedIdentityCredential, ImdsCustomHost) +{ + auto const actual1 = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", "https://visualstudio.com/"}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_IMDS_CUSTOM_AUTHORITY_HOST", ""}, + }); + + return std::make_unique( + "fedcba98-7654-3210-0123-456789abcdef", options); + }, + {{"https://azure.com/.default"}}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); + + auto const actual2 = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_IMDS_CUSTOM_AUTHORITY_HOST", "https://custom.imds.endpoint/"}, + }); + + return std::make_unique( + "01234567-89ab-cdef-fedc-ba9876543210", options); + }, + {{"https://outlook.com/.default"}}, + {"{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + auto const actual3 = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; + + CredentialTestHelper::EnvironmentOverride const env({ + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", "https://xbox.com/"}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_IMDS_CUSTOM_AUTHORITY_HOST", "http://localhost:59202"}, + }); + + return std::make_unique( + "01234567-89ab-cdef-fedc-ba9876543210", options); + }, + {{"https://outlook.com/.default"}}, + {"{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + EXPECT_EQ(actual1.Requests.size(), 1U); + EXPECT_EQ(actual1.Responses.size(), 1U); + + EXPECT_EQ(actual2.Requests.size(), 1U); + EXPECT_EQ(actual2.Responses.size(), 1U); + + auto const& request1 = actual1.Requests.at(0); + auto const& response1 = actual1.Responses.at(0); + + auto const& request2 = actual2.Requests.at(0); + auto const& response2 = actual2.Responses.at(0); + + EXPECT_EQ(request1.HttpMethod, HttpMethod::Get); + EXPECT_EQ(request2.HttpMethod, HttpMethod::Get); + + 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%2Fazure.com"); // cspell:disable-line + + EXPECT_EQ( + request2.AbsoluteUrl, + "https://custom.imds.endpoint/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=01234567-89ab-cdef-fedc-ba9876543210" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + auto const& request3 = actual3.Requests.at(0); + EXPECT_EQ( + request3.AbsoluteUrl, + "http://localhost:59202/metadata/identity/oauth2/token" + "?api-version=2018-02-01" + "&client_id=01234567-89ab-cdef-fedc-ba9876543210" + "&resource=https%3A%2F%2Foutlook.com"); // cspell:disable-line + + EXPECT_TRUE(request1.Body.empty()); + EXPECT_TRUE(request2.Body.empty()); + + { + 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(response1.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response2.AccessToken.Token, "ACCESSTOKEN2"); + + using namespace std::chrono_literals; + EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 3600s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 3600s); + + EXPECT_GE(response2.AccessToken.ExpiresOn, response2.EarliestExpiration + 3600s); + EXPECT_LE(response2.AccessToken.ExpiresOn, response2.LatestExpiration + 3600s); +} diff --git a/sdk/identity/azure-identity/test/ut/resource_identifier_test.cpp b/sdk/identity/azure-identity/test/ut/resource_identifier_test.cpp new file mode 100644 index 000000000..f9bf8a280 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/resource_identifier_test.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/identity/managed_identity_credential.hpp" + +#include + +#include + +using namespace Azure::Identity; + +TEST(ResourceIdentifier, Basic) +{ + std::string resourceId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/" + "providers/Compute/virtualMachines/vm-name"; + ResourceIdentifier resourceIdentifier(resourceId); + EXPECT_EQ(resourceIdentifier.ToString(), resourceId); +}