From b606ff60dcaad6a0076a8cb8617f4101bf5fa1e5 Mon Sep 17 00:00:00 2001 From: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> Date: Tue, 30 Mar 2021 00:26:42 +0000 Subject: [PATCH] Add ADFS support for ClientSecretCredential (#1947) --- sdk/core/azure-core/CMakeLists.txt | 1 + .../credentials/token_credential_options.hpp | 20 ++ sdk/core/azure-core/src/datetime.cpp | 16 +- .../identity/client_secret_credential.hpp | 26 +- .../azure/identity/environment_credential.hpp | 6 +- .../src/client_secret_credential.cpp | 54 +++- .../src/environment_credential.cpp | 30 +- .../azure-identity/test/ut/CMakeLists.txt | 3 + .../test/ut/client_secret_credential.cpp | 146 ++++++++++ .../test/ut/environment_credential.cpp | 257 ++++++++++++++++++ .../azure-identity/test/ut/test_transport.hpp | 29 ++ 11 files changed, 559 insertions(+), 29 deletions(-) create mode 100644 sdk/core/azure-core/inc/azure/core/credentials/token_credential_options.hpp create mode 100644 sdk/identity/azure-identity/test/ut/client_secret_credential.cpp create mode 100644 sdk/identity/azure-identity/test/ut/environment_credential.cpp create mode 100644 sdk/identity/azure-identity/test/ut/test_transport.hpp diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index deeefce63..5d68fe840 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -46,6 +46,7 @@ set( ${CURL_TRANSPORT_ADAPTER_INC} ${WIN_TRANSPORT_ADAPTER_INC} inc/azure/core/credentials/credentials.hpp + inc/azure/core/credentials/token_credential_options.hpp inc/azure/core/cryptography/hash.hpp inc/azure/core/diagnostics/logger.hpp inc/azure/core/http/http.hpp diff --git a/sdk/core/azure-core/inc/azure/core/credentials/token_credential_options.hpp b/sdk/core/azure-core/inc/azure/core/credentials/token_credential_options.hpp new file mode 100644 index 000000000..6bd598675 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/credentials/token_credential_options.hpp @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Options for #Azure::Core::Credentials::TokenCredential. + */ + +#pragma once + +#include "azure/core/internal/client_options.hpp" + +namespace Azure { namespace Core { namespace Credentials { + /** + * @brief Defines options for #Azure::Core::Credentials::TokenCredential. + */ + struct TokenCredentialOptions : public Azure::Core::_internal::ClientOptions + { + }; +}}} // namespace Azure::Core::Credentials diff --git a/sdk/core/azure-core/src/datetime.cpp b/sdk/core/azure-core/src/datetime.cpp index f5b6bff9e..ebec2ac61 100644 --- a/sdk/core/azure-core/src/datetime.cpp +++ b/sdk/core/azure-core/src/datetime.cpp @@ -40,6 +40,20 @@ DateTime GetSystemClockEpoch() static_cast(systemClockEpochUtcStructTm->tm_sec)); } +DateTime GetMaxDateTime() +{ + auto const systemClockMax = std::chrono::duration_cast( + std::chrono::system_clock::time_point::max().time_since_epoch()) + .count(); + + auto const systemClockEpoch = GetSystemClockEpoch().time_since_epoch().count(); + + constexpr auto repMax = std::numeric_limits::max(); + + return DateTime(DateTime::time_point( + DateTime::duration(systemClockMax + std::min(systemClockEpoch, (repMax - systemClockMax))))); +} + template void ValidateDateElementRange( T value, @@ -409,7 +423,7 @@ DateTime::DateTime( DateTime::operator std::chrono::system_clock::time_point() const { static DateTime SystemClockMin(std::chrono::system_clock::time_point::min()); - static DateTime SystemClockMax(std::chrono::system_clock::time_point::max()); + static DateTime SystemClockMax(GetMaxDateTime()); auto outOfRange = 0; if (*this < SystemClockMin) diff --git a/sdk/identity/azure-identity/inc/azure/identity/client_secret_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/client_secret_credential.hpp index 27045127d..76e0d8583 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/client_secret_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/client_secret_credential.hpp @@ -11,8 +11,8 @@ #include "azure/identity/dll_import_export.hpp" #include +#include #include -#include #include #include @@ -25,7 +25,7 @@ namespace Azure { namespace Identity { /** * @brief Defines options for token authentication. */ - struct ClientSecretCredentialOptions : public Azure::Core::_internal::ClientOptions + struct ClientSecretCredentialOptions : public Azure::Core::Credentials::TokenCredentialOptions { public: /** @@ -64,12 +64,32 @@ namespace Azure { namespace Identity { std::string tenantId, std::string clientId, std::string clientSecret, - ClientSecretCredentialOptions options = ClientSecretCredentialOptions()) + ClientSecretCredentialOptions options) : m_tenantId(std::move(tenantId)), m_clientId(std::move(clientId)), m_clientSecret(std::move(clientSecret)), m_options(std::move(options)) { } + /** + * @brief Construct a Client Secret credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param clientSecret Client Secret. + * @param options #Azure::Core::Credentials::TokenCredentialOptions. + */ + explicit ClientSecretCredential( + std::string tenantId, + std::string clientId, + std::string clientSecret, + Azure::Core::Credentials::TokenCredentialOptions const& options + = Azure::Core::Credentials::TokenCredentialOptions()) + : m_tenantId(std::move(tenantId)), m_clientId(std::move(clientId)), + m_clientSecret(std::move(clientSecret)) + { + static_cast(m_options) = options; + } + Core::Credentials::AccessToken GetToken( Core::Credentials::TokenRequestContext const& tokenRequestContext, Core::Context const& context) const override; diff --git a/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp index b80648cf0..e5dee93f0 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp @@ -9,11 +9,11 @@ #pragma once #include +#include #include namespace Azure { namespace Identity { - /** * @brief An environment credential. */ @@ -32,7 +32,9 @@ namespace Azure { namespace Identity { * - AZURE_USERNAME * - AZURE_PASSWORD */ - explicit EnvironmentCredential(); + explicit EnvironmentCredential( + Azure::Core::Credentials::TokenCredentialOptions options + = Azure::Core::Credentials::TokenCredentialOptions()); Core::Credentials::AccessToken GetToken( Core::Credentials::TokenRequestContext const& tokenRequestContext, diff --git a/sdk/identity/azure-identity/src/client_secret_credential.cpp b/sdk/identity/azure-identity/src/client_secret_credential.cpp index b5a300c00..b22633094 100644 --- a/sdk/identity/azure-identity/src/client_secret_credential.cpp +++ b/sdk/identity/azure-identity/src/client_secret_credential.cpp @@ -9,6 +9,40 @@ #include #include +namespace { +// Assumes !scopes.empty() +std::string FormatScopes(std::vector const& scopes, bool asResource) +{ + if (asResource && scopes.size() == 1) + { + auto resource = scopes[0]; + constexpr char suffix[] = "/.default"; + constexpr int suffixLen = sizeof(suffix) - 1; + auto const resourceLen = resource.length(); + + // If scopes[0] ends with '/.default', remove it. + if (resourceLen >= suffixLen + && resource.find(suffix, resourceLen - suffixLen) != std::string::npos) + { + resource = resource.substr(0, resourceLen - suffixLen); + } + + return Azure::Core::Url::Encode(resource); + } + + auto scopesIter = scopes.begin(); + auto scopesStr = Azure::Core::Url::Encode(*scopesIter); + + auto const scopesEnd = scopes.end(); + for (++scopesIter; scopesIter != scopesEnd; ++scopesIter) + { + scopesStr += std::string(" ") + Azure::Core::Url::Encode(*scopesIter); + } + + return scopesStr; +} +} // namespace + using namespace Azure::Identity; std::string const Azure::Identity::_detail::g_aadGlobalAuthority @@ -27,24 +61,21 @@ Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken( static std::string const errorMsgPrefix("ClientSecretCredential::GetToken: "); try { + auto const isAdfs = m_tenantId == "adfs"; + Url url(m_options.AuthorityHost); url.AppendPath(m_tenantId); - url.AppendPath("oauth2/v2.0/token"); + url.AppendPath(isAdfs ? "oauth2/token" : "oauth2/v2.0/token"); std::ostringstream body; body << "grant_type=client_credentials&client_id=" << Url::Encode(m_clientId) << "&client_secret=" << Url::Encode(m_clientSecret); - auto const& scopes = tokenRequestContext.Scopes; - if (!scopes.empty()) { - auto scopesIter = scopes.begin(); - body << "&scope=" << Url::Encode(*scopesIter); - - auto const scopesEnd = scopes.end(); - for (++scopesIter; scopesIter != scopesEnd; ++scopesIter) + auto const& scopes = tokenRequestContext.Scopes; + if (!scopes.empty()) { - body << " " << *scopesIter; + body << "&scope=" << FormatScopes(scopes, isAdfs); } } @@ -58,6 +89,11 @@ Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken( request.SetHeader("Content-Type", "application/x-www-form-urlencoded"); request.SetHeader("Content-Length", std::to_string(bodyString.size())); + if (isAdfs) + { + request.SetHeader("Host", url.GetHost()); + } + HttpPipeline httpPipeline(m_options, "Identity-client-secret-credential", "", {}, {}); std::shared_ptr response = httpPipeline.Send(request, context); diff --git a/sdk/identity/azure-identity/src/environment_credential.cpp b/sdk/identity/azure-identity/src/environment_credential.cpp index da37c6804..ee7bd27c1 100644 --- a/sdk/identity/azure-identity/src/environment_credential.cpp +++ b/sdk/identity/azure-identity/src/environment_credential.cpp @@ -21,7 +21,8 @@ using namespace Azure::Identity; -EnvironmentCredential::EnvironmentCredential() +EnvironmentCredential::EnvironmentCredential( + Azure::Core::Credentials::TokenCredentialOptions options) { #if !defined(WINAPI_PARTITION_DESKTOP) \ || WINAPI_PARTITION_DESKTOP // See azure/core/platform.hpp for explanation. @@ -53,28 +54,29 @@ EnvironmentCredential::EnvironmentCredential() { if (authority != nullptr) { - ClientSecretCredentialOptions options; - options.AuthorityHost = authority; + ClientSecretCredentialOptions clientSecretCredentialOptions; + static_cast(clientSecretCredentialOptions) = options; + clientSecretCredentialOptions.AuthorityHost = authority; - m_credentialImpl.reset( - new ClientSecretCredential(tenantId, clientId, clientSecret, options)); + m_credentialImpl.reset(new ClientSecretCredential( + tenantId, clientId, clientSecret, clientSecretCredentialOptions)); } else { - m_credentialImpl.reset(new ClientSecretCredential(tenantId, clientId, clientSecret)); + m_credentialImpl.reset( + new ClientSecretCredential(tenantId, clientId, clientSecret, options)); } } // TODO: These credential types are not implemented. Uncomment when implemented. // else if (username != nullptr && password != nullptr) - //{ - // m_credentialImpl.reset( - // new UsernamePasswordCredential(username, password, tenantId, clientId)); - //} + // { + // m_credentialImpl.reset( + // new UsernamePasswordCredential(tenantId, clientId, username, password, options)); + // } // else if (clientCertificatePath != nullptr) - //{ - // m_credentialImpl.reset( - // new ClientCertificateCredential(tenantId, clientId, clientCertificatePath)); - //} + // { + // m_credentialImpl.reset(new ClientCertificateCredential(tenantId, clientId, options)); + // } } #endif } diff --git a/sdk/identity/azure-identity/test/ut/CMakeLists.txt b/sdk/identity/azure-identity/test/ut/CMakeLists.txt index 913fbebdd..d67e924c1 100644 --- a/sdk/identity/azure-identity/test/ut/CMakeLists.txt +++ b/sdk/identity/azure-identity/test/ut/CMakeLists.txt @@ -13,9 +13,12 @@ include(GoogleTest) add_executable ( azure-identity-test + client_secret_credential.cpp + environment_credential.cpp macro_guard.cpp main.cpp simplified_header.cpp + test_transport.hpp ) if (MSVC) diff --git a/sdk/identity/azure-identity/test/ut/client_secret_credential.cpp b/sdk/identity/azure-identity/test/ut/client_secret_credential.cpp new file mode 100644 index 000000000..a348f3938 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/client_secret_credential.cpp @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/client_secret_credential.hpp" + +#include + +#include "test_transport.hpp" + +#include + +using namespace Azure::Identity; + +namespace { + +struct CredentialResult +{ + struct RequestInfo + { + std::string AbsoluteUrl; + Azure::Core::CaseInsensitiveMap Headers; + std::string Body; + } Request; + + struct + { + std::chrono::system_clock::time_point Earliest; + std::chrono::system_clock::time_point Latest; + Azure::Core::Credentials::AccessToken AccessToken; + } Response; +}; + +CredentialResult TestClientSecretCredential( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientSecret, + ClientSecretCredentialOptions credentialOptions, + Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext, + std::string const& responseBody) +{ + CredentialResult result; + + auto responseVec = std::vector(responseBody.begin(), responseBody.end()); + credentialOptions.Transport.Transport = std::make_shared([&](auto request, auto) { + auto const bodyVec = request.GetBodyStream()->ReadToEnd(Azure::Core::Context()); + + result.Request + = {request.GetUrl().GetAbsoluteUrl(), + request.GetHeaders(), + std::string(bodyVec.begin(), bodyVec.end())}; + + auto response = std::make_unique( + 1, 1, Azure::Core::Http::HttpStatusCode::Ok, "OK"); + + response->SetBodyStream(std::make_unique(responseVec)); + + result.Response.Earliest = std::chrono::system_clock::now(); + return response; + }); + + ClientSecretCredential credential(tenantId, clientId, clientSecret, credentialOptions); + result.Response.AccessToken = credential.GetToken(tokenRequestContext, Azure::Core::Context()); + result.Response.Latest = std::chrono::system_clock::now(); + + return result; +} +} // namespace + +TEST(ClientSecretCredential, Regular) +{ + ClientSecretCredentialOptions options; + options.AuthorityHost = "https://microsoft.com/"; + auto const actual = TestClientSecretCredential( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + "CLIENTSECRET", + options, + {{"https://azure.com/.default"}}, + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"); + + EXPECT_EQ( + actual.Request.AbsoluteUrl, + "https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + { + constexpr char expectedBody[] = "grant_type=client_credentials" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_secret=CLIENTSECRET" + "&scope=https%3A%2F%2Fazure.com%2F.default"; + + EXPECT_EQ(actual.Request.Body, expectedBody); + + EXPECT_NE(actual.Request.Headers.find("Content-Length"), actual.Request.Headers.end()); + EXPECT_EQ( + actual.Request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); + } + + EXPECT_NE(actual.Request.Headers.find("Content-Type"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(actual.Response.AccessToken.Token, "ACCESSTOKEN1"); + + using namespace std::chrono_literals; + EXPECT_GT(actual.Response.AccessToken.ExpiresOn, actual.Response.Earliest + 3600s); + EXPECT_LT(actual.Response.AccessToken.ExpiresOn, actual.Response.Latest + 3600s); +} + +TEST(ClientSecretCredential, AzureStack) +{ + ClientSecretCredentialOptions options; + options.AuthorityHost = "https://microsoft.com/"; + auto const actual = TestClientSecretCredential( + "adfs", + "fedcba98-7654-3210-0123-456789abcdef", + "CLIENTSECRET", + options, + {{"https://azure.com/.default"}}, + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"); + + EXPECT_EQ(actual.Request.AbsoluteUrl, "https://microsoft.com/adfs/oauth2/token"); + + { + constexpr char expectedBody[] = "grant_type=client_credentials" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_secret=CLIENTSECRET" + "&scope=https%3A%2F%2Fazure.com"; + + EXPECT_EQ(actual.Request.Body, expectedBody); + + EXPECT_NE(actual.Request.Headers.find("Content-Length"), actual.Request.Headers.end()); + EXPECT_EQ( + actual.Request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); + } + + EXPECT_NE(actual.Request.Headers.find("Content-Type"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(actual.Request.Headers.find("Host"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Host"), "microsoft.com"); + + EXPECT_EQ(actual.Response.AccessToken.Token, "ACCESSTOKEN1"); + + using namespace std::chrono_literals; + EXPECT_GT(actual.Response.AccessToken.ExpiresOn, actual.Response.Earliest + 3600s); + EXPECT_LT(actual.Response.AccessToken.ExpiresOn, actual.Response.Latest + 3600s); +} diff --git a/sdk/identity/azure-identity/test/ut/environment_credential.cpp b/sdk/identity/azure-identity/test/ut/environment_credential.cpp new file mode 100644 index 000000000..9863c273a --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/environment_credential.cpp @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/environment_credential.hpp" + +#include +#include + +#include "test_transport.hpp" + +#include +#include + +#if defined(AZ_PLATFORM_WINDOWS) +#if !defined(WIN32_LEAN_AND_MEAN) +#define WIN32_LEAN_AND_MEAN +#endif +#if !defined(NOMINMAX) +#define NOMINMAX +#endif + +#include +#endif + +#if !defined(WINAPI_PARTITION_DESKTOP) \ + || WINAPI_PARTITION_DESKTOP // See azure/core/platform.hpp for explanation. + +using namespace Azure::Identity; + +namespace { +class EnvironmentOverride { + class Environment { + static void SetVariable(std::string const& name, std::string const& value) + { +#if defined(_MSC_VER) + static_cast(_putenv((name + "=" + value).c_str())); +#else + if (value.empty()) + { + static_cast(unsetenv(name.c_str())); + } + else + { + static_cast(setenv(name.c_str(), value.c_str(), 1)); + } +#endif + } + + public: + static std::string GetVariable(std::string const& name) + { +#if defined(_MSC_VER) +#pragma warning(push) +// warning C4996: 'getenv': This function or variable may be unsafe. Consider using _dupenv_s +// instead. +#pragma warning(disable : 4996) +#endif + auto const result = std::getenv(name.c_str()); + return result != nullptr ? result : ""; +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + } + + static void SetVariables(std::map const& vars) + { + for (auto var : vars) + { + SetVariable(var.first, var.second); + } + } + }; + + std::map m_originalEnv; + +public: + ~EnvironmentOverride() { Environment::SetVariables(m_originalEnv); } + + EnvironmentOverride( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientSecret, + std::string const& authorityHost, + std::string const& username, + std::string const& password, + std::string const& clientCertificatePath) + { + std::map const NewEnv = { + {"AZURE_TENANT_ID", tenantId}, + {"AZURE_CLIENT_ID", clientId}, + {"AZURE_CLIENT_SECRET", clientSecret}, + {"AZURE_AUTHORITY_HOST", authorityHost}, + {"AZURE_USERNAME", username}, + {"AZURE_PASSWORD", password}, + {"AZURE_CLIENT_CERTIFICATE_PATH", clientCertificatePath}, + }; + + for (auto var : NewEnv) + { + m_originalEnv[var.first] = Environment::GetVariable(var.first); + } + + try + { + Environment::SetVariables(NewEnv); + } + catch (...) + { + Environment::SetVariables(m_originalEnv); + throw; + } + } +}; + +struct CredentialResult +{ + struct + { + std::string AbsoluteUrl; + Azure::Core::CaseInsensitiveMap Headers; + std::string Body; + } Request; + + struct + { + std::chrono::system_clock::time_point Earliest; + std::chrono::system_clock::time_point Latest; + Azure::Core::Credentials::AccessToken AccessToken; + } Response; +}; + +CredentialResult TestEnvironmentCredential( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientSecret, + std::string const& authorityHost, + std::string const& username, + std::string const& password, + std::string const& clientCertificatePath, + Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext, + std::string const& responseBody) +{ + CredentialResult result; + + auto responseVec = std::vector(responseBody.begin(), responseBody.end()); + + Azure::Core::Credentials::TokenCredentialOptions credentialOptions; + credentialOptions.Transport.Transport = std::make_shared([&](auto request, auto) { + auto const bodyVec = request.GetBodyStream()->ReadToEnd(Azure::Core::Context()); + + result.Request + = {request.GetUrl().GetAbsoluteUrl(), + request.GetHeaders(), + std::string(bodyVec.begin(), bodyVec.end())}; + + auto response = std::make_unique( + 1, 1, Azure::Core::Http::HttpStatusCode::Ok, "OK"); + + response->SetBodyStream(std::make_unique(responseVec)); + + result.Response.Earliest = std::chrono::system_clock::now(); + return response; + }); + + EnvironmentOverride env( + tenantId, clientId, clientSecret, authorityHost, username, password, clientCertificatePath); + + EnvironmentCredential credential(credentialOptions); + result.Response.AccessToken = credential.GetToken(tokenRequestContext, Azure::Core::Context()); + result.Response.Latest = std::chrono::system_clock::now(); + + return result; +} +} // namespace + +TEST(EnvironmentCredential, RegularClientSecretCredential) +{ + auto const actual = TestEnvironmentCredential( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + "CLIENTSECRET", + "https://microsoft.com/", + "", + "", + "", + {{"https://azure.com/.default"}}, + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"); + + EXPECT_EQ( + actual.Request.AbsoluteUrl, + "https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + { + constexpr char expectedBody[] = "grant_type=client_credentials" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_secret=CLIENTSECRET" + "&scope=https%3A%2F%2Fazure.com%2F.default"; + + EXPECT_EQ(actual.Request.Body, expectedBody); + + EXPECT_NE(actual.Request.Headers.find("Content-Length"), actual.Request.Headers.end()); + EXPECT_EQ( + actual.Request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); + } + + EXPECT_NE(actual.Request.Headers.find("Content-Type"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(actual.Response.AccessToken.Token, "ACCESSTOKEN1"); + + using namespace std::chrono_literals; + EXPECT_GT(actual.Response.AccessToken.ExpiresOn, actual.Response.Earliest + 3600s); + EXPECT_LT(actual.Response.AccessToken.ExpiresOn, actual.Response.Latest + 3600s); +} + +TEST(EnvironmentCredential, AzureStackClientSecretCredential) +{ + auto const actual = TestEnvironmentCredential( + "adfs", + "fedcba98-7654-3210-0123-456789abcdef", + "CLIENTSECRET", + "https://microsoft.com/", + "", + "", + "", + {{"https://azure.com/.default"}}, + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"); + + EXPECT_EQ(actual.Request.AbsoluteUrl, "https://microsoft.com/adfs/oauth2/token"); + + { + constexpr char expectedBody[] = "grant_type=client_credentials" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_secret=CLIENTSECRET" + "&scope=https%3A%2F%2Fazure.com"; + + EXPECT_EQ(actual.Request.Body, expectedBody); + + EXPECT_NE(actual.Request.Headers.find("Content-Length"), actual.Request.Headers.end()); + EXPECT_EQ( + actual.Request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); + } + + EXPECT_NE(actual.Request.Headers.find("Content-Type"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(actual.Request.Headers.find("Host"), actual.Request.Headers.end()); + EXPECT_EQ(actual.Request.Headers.at("Host"), "microsoft.com"); + + EXPECT_EQ(actual.Response.AccessToken.Token, "ACCESSTOKEN1"); + + using namespace std::chrono_literals; + EXPECT_GT(actual.Response.AccessToken.ExpiresOn, actual.Response.Earliest + 3600s); + EXPECT_LT(actual.Response.AccessToken.ExpiresOn, actual.Response.Latest + 3600s); +} + +#endif diff --git a/sdk/identity/azure-identity/test/ut/test_transport.hpp b/sdk/identity/azure-identity/test/ut/test_transport.hpp new file mode 100644 index 000000000..581330ec8 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/test_transport.hpp @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include + +class TestTransport : public Azure::Core::Http::HttpTransport { +public: + typedef std::function( + Azure::Core::Http::Request& request, + Azure::Core::Context const& context)> + SendCallback; + +private: + SendCallback m_sendCallback; + +public: + TestTransport(SendCallback send) : m_sendCallback(send) {} + + std::unique_ptr Send( + Azure::Core::Http::Request& request, + Azure::Core::Context const& context) override + { + return m_sendCallback(request, context); + } +};