diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 08c6614b0..c60054a05 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for `WorkloadIdentityCredential`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/CMakeLists.txt b/sdk/identity/azure-identity/CMakeLists.txt index 6ad5628a7..755320f42 100644 --- a/sdk/identity/azure-identity/CMakeLists.txt +++ b/sdk/identity/azure-identity/CMakeLists.txt @@ -57,6 +57,7 @@ set( inc/azure/identity/environment_credential.hpp inc/azure/identity/managed_identity_credential.hpp inc/azure/identity/rtti.hpp + inc/azure/identity/workload_identity_credential.hpp inc/azure/identity.hpp ) @@ -80,6 +81,7 @@ set( src/tenant_id_resolver.cpp src/token_cache.cpp src/token_credential_impl.cpp + src/workload_identity_credential.cpp ) add_library(azure-identity ${AZURE_IDENTITY_HEADER} ${AZURE_IDENTITY_SOURCE}) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 6e33573df..390e5c436 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -127,6 +127,7 @@ Configuration is attempted in the above order. For example, if values for a clie |`ChainedTokenCredential`|Allows users to define custom authentication flows composing multiple credentials. |`ManagedIdentityCredential`|Authenticates the managed identity of an Azure resource. |`EnvironmentCredential`|Authenticates a service principal or user via credential information specified in environment variables. +|`WorkloadIdentityCredential`|Authenticate a workload identity on Kubernetes. ### Authenticate service principals |Credential | Usage diff --git a/sdk/identity/azure-identity/inc/azure/identity.hpp b/sdk/identity/azure-identity/inc/azure/identity.hpp index b1eb9963d..a258b213d 100644 --- a/sdk/identity/azure-identity/inc/azure/identity.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity.hpp @@ -17,3 +17,4 @@ #include "azure/identity/environment_credential.hpp" #include "azure/identity/managed_identity_credential.hpp" #include "azure/identity/rtti.hpp" +#include "azure/identity/workload_identity_credential.hpp" diff --git a/sdk/identity/azure-identity/inc/azure/identity/workload_identity_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/workload_identity_credential.hpp new file mode 100644 index 000000000..9da4c4a68 --- /dev/null +++ b/sdk/identity/azure-identity/inc/azure/identity/workload_identity_credential.hpp @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file + * @brief Workload Identity Credential and options. + */ + +#pragma once + +#include "azure/identity/detail/client_credential_core.hpp" +#include "azure/identity/detail/token_cache.hpp" + +#include + +#include +#include + +namespace Azure { namespace Identity { + namespace _detail { + class TokenCredentialImpl; + } // namespace _detail + + /** + * @brief Options for workload identity credential. + * + */ + struct WorkloadIdentityCredentialOptions final : public Core::Credentials::TokenCredentialOptions + { + /** + * @brief Authentication authority URL. + * @note Default value is Azure AD global authority (https://login.microsoftonline.com/). + * + * @note Example of an authority host string: "https://login.microsoftonline.us/". See national + * clouds' Azure AD authentication endpoints: + * https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud. + */ + std::string AuthorityHost = _detail::ClientCredentialCore::AadGlobalAuthority; + + /** + * @brief For multi-tenant applications, specifies additional tenants for which the credential + * may acquire tokens. Add the wildcard value `"*"` to allow the credential to acquire tokens + * for any tenant in which the application is installed. + */ + std::vector AdditionallyAllowedTenants; + }; + + /** + * @brief Workload Identity Credential supports Azure workload identity authentication on + * Kubernetes and other hosts supporting workload identity. See the Azure Kubernetes Service + * documentation at https://learn.microsoft.com/azure/aks/workload-identity-overview for more + * information. + * + */ + class WorkloadIdentityCredential final : public Core::Credentials::TokenCredential { + private: + _detail::TokenCache m_tokenCache; + _detail::ClientCredentialCore m_clientCredentialCore; + std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl; + std::string m_requestBody; + std::string m_tokenFilePath; + + explicit WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + std::string const& authorityHost, + std::vector additionallyAllowedTenants, + Core::Credentials::TokenCredentialOptions const& options); + + public: + /** + * @brief Constructs a Workload Identity Credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param tokenFilePath Path of a file containing a Kubernetes service account token. + * @param options Options for token retrieval. + */ + explicit WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + Core::Credentials::TokenCredentialOptions const& options + = Core::Credentials::TokenCredentialOptions()); + + /** + * @brief Constructs a Workload Identity Credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param tokenFilePath Path of a file containing a Kubernetes service account token. + * @param options Options for token retrieval. + */ + explicit WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + WorkloadIdentityCredentialOptions const& options); + + /** + * @brief Destructs `%WorkloadIdentityCredential`. + * + */ + ~WorkloadIdentityCredential() override; + + /** + * @brief Gets an authentication token. + * + * @param tokenRequestContext A context to get the token in. + * @param context A context to control the request lifetime. + * + * @throw Azure::Core::Credentials::AuthenticationException Authentication error occurred. + */ + Core::Credentials::AccessToken GetToken( + Core::Credentials::TokenRequestContext const& tokenRequestContext, + Core::Context const& context) const override; + }; + +}} // namespace Azure::Identity diff --git a/sdk/identity/azure-identity/src/workload_identity_credential.cpp b/sdk/identity/azure-identity/src/workload_identity_credential.cpp new file mode 100644 index 000000000..7284f929a --- /dev/null +++ b/sdk/identity/azure-identity/src/workload_identity_credential.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/identity/workload_identity_credential.hpp" + +#include "private/tenant_id_resolver.hpp" +#include "private/token_credential_impl.hpp" + +#include +#include + +using Azure::Identity::WorkloadIdentityCredential; + +using Azure::Core::Context; +using Azure::Core::Url; +using Azure::Core::Credentials::AccessToken; +using Azure::Core::Credentials::TokenRequestContext; +using Azure::Core::Http::HttpMethod; +using Azure::Identity::_detail::TenantIdResolver; +using Azure::Identity::_detail::TokenCredentialImpl; + +WorkloadIdentityCredential::WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + std::string const& authorityHost, + std::vector additionallyAllowedTenants, + Core::Credentials::TokenCredentialOptions const& options) + : TokenCredential("WorkloadIdentityCredential"), + m_clientCredentialCore(tenantId, authorityHost, additionallyAllowedTenants), + m_tokenCredentialImpl(std::make_unique(options)), + m_requestBody( + std::string( + "grant_type=client_credentials" + "&client_assertion_type=" + "urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" // cspell:disable-line + "&client_id=") + + Url::Encode(clientId)), + m_tokenFilePath(tokenFilePath) +{ +} + +WorkloadIdentityCredential::WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + WorkloadIdentityCredentialOptions const& options) + : WorkloadIdentityCredential( + tenantId, + clientId, + tokenFilePath, + options.AuthorityHost, + options.AdditionallyAllowedTenants, + options) +{ +} + +WorkloadIdentityCredential::WorkloadIdentityCredential( + std::string tenantId, + std::string const& clientId, + std::string const& tokenFilePath, + Core::Credentials::TokenCredentialOptions const& options) + : WorkloadIdentityCredential( + tenantId, + clientId, + tokenFilePath, + WorkloadIdentityCredentialOptions{}.AuthorityHost, + WorkloadIdentityCredentialOptions{}.AdditionallyAllowedTenants, + options) +{ +} + +WorkloadIdentityCredential::~WorkloadIdentityCredential() = default; + +AccessToken WorkloadIdentityCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const +{ + auto const tenantId = TenantIdResolver::Resolve( + m_clientCredentialCore.GetTenantId(), + tokenRequestContext, + m_clientCredentialCore.GetAdditionallyAllowedTenants()); + + auto const scopesStr + = m_clientCredentialCore.GetScopesString(tenantId, tokenRequestContext.Scopes); + + // TokenCache::GetToken() and m_tokenCredentialImpl->GetToken() can only use the lambda argument + // when they are being executed. They are not supposed to keep a reference to lambda argument to + // call it later. Therefore, any capture made here will outlive the possible time frame when the + // lambda might get called. + return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() { + return m_tokenCredentialImpl->GetToken(context, [&]() { + auto body = m_requestBody; + if (!scopesStr.empty()) + { + body += "&scope=" + scopesStr; + } + + auto const requestUrl = m_clientCredentialCore.GetRequestUrl(tenantId); + + // Read the specified file's content, which is expected to be a Kubernetes service account + // token. Kubernetes is responsible for updating the file as service account tokens expire. + std::ifstream azureFederatedTokenFile(m_tokenFilePath); + std::string assertion( + (std::istreambuf_iterator(azureFederatedTokenFile)), + std::istreambuf_iterator()); + + body += "&client_assertion=" + Azure::Core::Url::Encode(assertion); + + auto request + = std::make_unique(HttpMethod::Post, requestUrl, body); + + request->HttpRequest.SetHeader("Host", requestUrl.GetHost()); + + return request; + }); + }); +} diff --git a/sdk/identity/azure-identity/test/ut/CMakeLists.txt b/sdk/identity/azure-identity/test/ut/CMakeLists.txt index 74ee00d38..2b8f17ca2 100644 --- a/sdk/identity/azure-identity/test/ut/CMakeLists.txt +++ b/sdk/identity/azure-identity/test/ut/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable ( token_cache_test.cpp token_credential_impl_test.cpp token_credential_test.cpp + workload_identity_credential_test.cpp ) create_per_service_target_build(identity azure-identity-test) diff --git a/sdk/identity/azure-identity/test/ut/workload_identity_credential_test.cpp b/sdk/identity/azure-identity/test/ut/workload_identity_credential_test.cpp new file mode 100644 index 000000000..3484c2467 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/workload_identity_credential_test.cpp @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "azure/identity/workload_identity_credential.hpp" +#include "credential_test_helper.hpp" + +#include +#include + +#include + +using Azure::Core::Http::HttpMethod; +using Azure::Identity::WorkloadIdentityCredential; +using Azure::Identity::WorkloadIdentityCredentialOptions; +using Azure::Identity::Test::_detail::CredentialTestHelper; + +namespace { +struct TempCertFile final +{ + static const char* const Path; + ~TempCertFile(); + TempCertFile(); +}; +} // namespace + +TEST(WorkloadIdentityCredential, GetCredentialName) +{ + TempCertFile const tempCertFile; + WorkloadIdentityCredential const cred( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path); + + EXPECT_EQ(cred.GetCredentialName(), "WorkloadIdentityCredential"); +} + +TEST(WorkloadIdentityCredential, Regular) +{ + TempCertFile const tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + WorkloadIdentityCredentialOptions options; + options.Transport.Transport = transport; + + return std::make_unique( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path, + options); + }, + {{{"https://azure.com/.default"}}, {{}}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + EXPECT_EQ(actual.Requests.size(), 2U); + EXPECT_EQ(actual.Responses.size(), 2U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Post); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Post); + + EXPECT_EQ( + request0.AbsoluteUrl, + "https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + { + constexpr char expectedBodyStart0[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&scope=https%3A%2F%2Fazure.com%2F.default" + "&client_assertion="; // cspell:enable + + constexpr char expectedBodyStart1[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_assertion="; // cspell:enable + + EXPECT_GT(request0.Body.size(), (sizeof(expectedBodyStart0) - 1)); + EXPECT_GT(request1.Body.size(), (sizeof(expectedBodyStart1) - 1)); + + EXPECT_EQ(request0.Body.substr(0, (sizeof(expectedBodyStart0) - 1)), expectedBodyStart0); + EXPECT_EQ(request1.Body.substr(0, (sizeof(expectedBodyStart1) - 1)), expectedBodyStart1); + + EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end()); + EXPECT_GT( + std::stoi(request0.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart0) - 1)); + + EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end()); + EXPECT_GT( + std::stoi(request1.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart1) - 1)); + } + + EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + + 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 + 7200s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s); +} + +TEST(WorkloadIdentityCredential, AzureStack) +{ + TempCertFile const tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + WorkloadIdentityCredentialOptions options; + options.Transport.Transport = transport; + + return std::make_unique( + "adfs", "fedcba98-7654-3210-0123-456789abcdef", TempCertFile::Path, options); + }, + {{{"https://azure.com/.default"}}, {{}}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + EXPECT_EQ(actual.Requests.size(), 2U); + EXPECT_EQ(actual.Responses.size(), 2U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Post); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Post); + + EXPECT_EQ(request0.AbsoluteUrl, "https://login.microsoftonline.com/adfs/oauth2/token"); + + EXPECT_EQ(request1.AbsoluteUrl, "https://login.microsoftonline.com/adfs/oauth2/token"); + + { + constexpr char expectedBodyStart0[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&scope=https%3A%2F%2Fazure.com" + "&client_assertion="; // cspell:enable + + constexpr char expectedBodyStart1[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_assertion="; // cspell:enable + + EXPECT_GT(request0.Body.size(), (sizeof(expectedBodyStart0) - 1)); + EXPECT_GT(request1.Body.size(), (sizeof(expectedBodyStart1) - 1)); + + EXPECT_EQ(request0.Body.substr(0, (sizeof(expectedBodyStart0) - 1)), expectedBodyStart0); + EXPECT_EQ(request1.Body.substr(0, (sizeof(expectedBodyStart1) - 1)), expectedBodyStart1); + + EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end()); + EXPECT_GT( + std::stoi(request0.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart0) - 1)); + + EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end()); + EXPECT_GT( + std::stoi(request1.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart1) - 1)); + } + + EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + + 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 + 7200s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s); +} + +TEST(WorkloadIdentityCredential, Authority) +{ + TempCertFile const tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + WorkloadIdentityCredentialOptions options; + options.AuthorityHost = "https://microsoft.com/"; + options.Transport.Transport = transport; + + return std::make_unique( + "01234567-89ab-cdef-fedc-ba8976543210", + "fedcba98-7654-3210-0123-456789abcdef", + TempCertFile::Path, + options); + }, + {{{"https://azure.com/.default"}}, {{}}}, + std::vector{ + "{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}", + "{\"expires_in\":7200, \"access_token\":\"ACCESSTOKEN2\"}"}); + + EXPECT_EQ(actual.Requests.size(), 2U); + EXPECT_EQ(actual.Responses.size(), 2U); + + auto const& request0 = actual.Requests.at(0); + auto const& request1 = actual.Requests.at(1); + + auto const& response0 = actual.Responses.at(0); + auto const& response1 = actual.Responses.at(1); + + EXPECT_EQ(request0.HttpMethod, HttpMethod::Post); + EXPECT_EQ(request1.HttpMethod, HttpMethod::Post); + + EXPECT_EQ( + request0.AbsoluteUrl, + "https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + EXPECT_EQ( + request1.AbsoluteUrl, + "https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); + + { + constexpr char expectedBodyStart0[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&scope=https%3A%2F%2Fazure.com%2F.default" + "&client_assertion="; // cspell:enable + + constexpr char expectedBodyStart1[] // cspell:disable + = "grant_type=client_credentials" + "&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" + "&client_id=fedcba98-7654-3210-0123-456789abcdef" + "&client_assertion="; // cspell:enable + + EXPECT_GT(request0.Body.size(), (sizeof(expectedBodyStart0) - 1)); + EXPECT_GT(request1.Body.size(), (sizeof(expectedBodyStart1) - 1)); + + EXPECT_EQ(request0.Body.substr(0, (sizeof(expectedBodyStart0) - 1)), expectedBodyStart0); + EXPECT_EQ(request1.Body.substr(0, (sizeof(expectedBodyStart1) - 1)), expectedBodyStart1); + + EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end()); + EXPECT_GT( + std::stoi(request0.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart0) - 1)); + + EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end()); + EXPECT_GT( + std::stoi(request1.Headers.at("Content-Length")), + static_cast(sizeof(expectedBodyStart1) - 1)); + } + + EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end()); + EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end()); + EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); + + EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1"); + EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2"); + + 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 + 7200s); + EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s); +} + +namespace { +const char* const TempCertFile::Path = "azure-identity-test.pem"; + +TempCertFile::~TempCertFile() { std::remove(Path); } + +TempCertFile::TempCertFile() +{ + std::ofstream cert(Path, std::ios_base::out | std::ios_base::trunc); + + // The file contents is the following text, encoded as base64: + // "Base64 encoded JSON text to simulate a client assertion" + cert << // cspell:disable + "QmFzZTY0IGVuY29kZWQgSlNPTiB0ZXh0IHRvIHNpbXVsYXRlIGEgY2xpZW50IGFzc2VydGlvbg==\n"; + // cspell:enable +} +} // namespace