diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 0aff3b569..5488f9153 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -91,6 +91,7 @@ "LPWSTR", "MHSM", "moxygen", + "MSAL", "MSRC", "ncus", "Niels", diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 847ef2a8f..2d9cc77ba 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features Added - Added Azure CLI Credential. +- Added authority host overriding support for `ClientCertificateCredential`. +- Added Azure Stack support for `ClientCertificateCredential`. ### Breaking Changes diff --git a/sdk/identity/azure-identity/CMakeLists.txt b/sdk/identity/azure-identity/CMakeLists.txt index 97c9aa2af..381390d74 100644 --- a/sdk/identity/azure-identity/CMakeLists.txt +++ b/sdk/identity/azure-identity/CMakeLists.txt @@ -46,6 +46,7 @@ endif() set( AZURE_IDENTITY_HEADER + inc/azure/identity/detail/client_credential_core.hpp inc/azure/identity/detail/token_cache.hpp inc/azure/identity/azure_cli_credential.hpp inc/azure/identity/chained_token_credential.hpp @@ -66,6 +67,7 @@ set( src/azure_cli_credential.cpp src/chained_token_credential.cpp src/client_certificate_credential.cpp + src/client_credential_core.cpp src/client_secret_credential.cpp src/environment_credential.cpp src/managed_identity_credential.cpp diff --git a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp index e1d712a28..669a849f2 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp @@ -8,10 +8,12 @@ #pragma once +#include "azure/identity/detail/client_credential_core.hpp" #include "azure/identity/detail/token_cache.hpp" #include #include +#include #include #include @@ -20,6 +22,17 @@ namespace Azure { namespace Identity { namespace _detail { class TokenCredentialImpl; + + void FreePkeyImpl(void* pkey); + + template struct UniquePkeyHelper; + template <> struct UniquePkeyHelper + { + static void FreePkey(void* pkey) { FreePkeyImpl(pkey); } + using type = Azure::Core::_internal::BasicUniqueHandle; + }; + + using UniquePkeyHandle = Azure::Core::_internal::UniqueHandle; } // namespace _detail /** @@ -28,6 +41,15 @@ namespace Azure { namespace Identity { */ struct ClientCertificateCredentialOptions 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; }; /** @@ -38,12 +60,19 @@ namespace Azure { namespace Identity { class ClientCertificateCredential final : public Core::Credentials::TokenCredential { private: _detail::TokenCache m_tokenCache; + _detail::ClientCredentialCore m_clientCredentialCore; std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl; - Core::Url m_requestUrl; std::string m_requestBody; - std::string m_tokenHeaderEncoded; std::string m_tokenPayloadStaticPart; - void* m_pkey; + std::string m_tokenHeaderEncoded; + _detail::UniquePkeyHandle m_pkey; + + explicit ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + std::string const& authorityHost, + Core::Credentials::TokenCredentialOptions const& options); public: /** @@ -55,7 +84,7 @@ namespace Azure { namespace Identity { * @param options Options for token retrieval. */ explicit ClientCertificateCredential( - std::string const& tenantId, + std::string tenantId, std::string const& clientId, std::string const& clientCertificatePath, Core::Credentials::TokenCredentialOptions const& options @@ -70,7 +99,7 @@ namespace Azure { namespace Identity { * @param options Options for token retrieval. */ explicit ClientCertificateCredential( - std::string const& tenantId, + std::string tenantId, std::string const& clientId, std::string const& clientCertificatePath, ClientCertificateCredentialOptions const& options); 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 5a3a2cae8..494b938bd 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 @@ -8,8 +8,8 @@ #pragma once +#include "azure/identity/detail/client_credential_core.hpp" #include "azure/identity/detail/token_cache.hpp" -#include "azure/identity/dll_import_export.hpp" #include #include @@ -21,7 +21,6 @@ namespace Azure { namespace Identity { namespace _detail { class TokenCredentialImpl; - AZ_IDENTITY_DLLEXPORT extern std::string const g_aadGlobalAuthority; } // namespace _detail /** @@ -34,11 +33,11 @@ namespace Azure { namespace Identity { * @brief Authentication authority URL. * @note Default value is Azure AD global authority (https://login.microsoftonline.com/). * - * @note Example of a \p authority string: "https://login.microsoftonline.us/". See national + * @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::g_aadGlobalAuthority; + std::string AuthorityHost = _detail::ClientCredentialCore::AadGlobalAuthority; }; /** @@ -49,21 +48,15 @@ namespace Azure { namespace Identity { class ClientSecretCredential final : public Core::Credentials::TokenCredential { private: _detail::TokenCache m_tokenCache; + _detail::ClientCredentialCore m_clientCredentialCore; std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl; - Core::Url m_requestUrl; std::string m_requestBody; - std::string m_tenantId; - std::string m_clientId; - std::string m_authorityHost; - - bool m_isAdfs; - ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, - std::string authorityHost, + std::string const& authorityHost, Core::Credentials::TokenCredentialOptions const& options); public: @@ -77,7 +70,7 @@ namespace Azure { namespace Identity { */ explicit ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, ClientSecretCredentialOptions const& options); @@ -91,7 +84,7 @@ namespace Azure { namespace Identity { */ explicit ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, Core::Credentials::TokenCredentialOptions const& options = Core::Credentials::TokenCredentialOptions()); diff --git a/sdk/identity/azure-identity/inc/azure/identity/detail/client_credential_core.hpp b/sdk/identity/azure-identity/inc/azure/identity/detail/client_credential_core.hpp new file mode 100644 index 000000000..0a22b5bc7 --- /dev/null +++ b/sdk/identity/azure-identity/inc/azure/identity/detail/client_credential_core.hpp @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#pragma once + +#include "azure/identity/dll_import_export.hpp" + +#include +#include + +#include + +namespace Azure { namespace Identity { namespace _detail { + class ClientCredentialCore final { + Core::Url m_authorityHost; + std::string m_tenantId; + bool m_isAdfs; + + public: + AZ_IDENTITY_DLLEXPORT static std::string const AadGlobalAuthority; + + explicit ClientCredentialCore(std::string tenantId, std::string const& authorityHost); + + Core::Url GetRequestUrl() const; + + std::string GetScopesString(decltype(Core::Credentials::TokenRequestContext::Scopes) + const& scopes) const; + }; +}}} // namespace Azure::Identity::_detail 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 7f0b1e143..72c447b48 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/environment_credential.hpp @@ -34,8 +34,11 @@ namespace Azure { namespace Identity { * - AZURE_CLIENT_ID * - AZURE_CLIENT_SECRET * - AZURE_CLIENT_CERTIFICATE_PATH + * - AZURE_CLIENT_CERTIFICATE_PASSWORD + * - AZURE_CLIENT_SEND_CERTIFICATE_CHAIN * - AZURE_USERNAME * - AZURE_PASSWORD + * - AZURE_AUTHORITY_HOST */ explicit EnvironmentCredential( Azure::Core::Credentials::TokenCredentialOptions options diff --git a/sdk/identity/azure-identity/src/azure_cli_credential.cpp b/sdk/identity/azure-identity/src/azure_cli_credential.cpp index 41dd3280b..0a51b5c92 100644 --- a/sdk/identity/azure-identity/src/azure_cli_credential.cpp +++ b/sdk/identity/azure-identity/src/azure_cli_credential.cpp @@ -157,14 +157,7 @@ namespace { template struct UniqueHandleHelper; template <> struct UniqueHandleHelper { - static void CloseWin32Handle(HANDLE handle) - { - if (handle != nullptr) - { - static_cast(CloseHandle(handle)); - } - } - + static void CloseWin32Handle(HANDLE handle) { static_cast(CloseHandle(handle)); } using type = Azure::Core::_internal::BasicUniqueHandle; }; diff --git a/sdk/identity/azure-identity/src/client_certificate_credential.cpp b/sdk/identity/azure-identity/src/client_certificate_credential.cpp index 9c4a60540..7f41773fb 100644 --- a/sdk/identity/azure-identity/src/client_certificate_credential.cpp +++ b/sdk/identity/azure-identity/src/client_certificate_credential.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -22,7 +21,19 @@ #include #include -using namespace Azure::Identity; +using Azure::Identity::ClientCertificateCredential; + +using Azure::Core::Context; +using Azure::Core::Url; +using Azure::Core::Uuid; +using Azure::Core::_internal::Base64Url; +using Azure::Core::_internal::PosixTimeConverter; +using Azure::Core::Credentials::AccessToken; +using Azure::Core::Credentials::AuthenticationException; +using Azure::Core::Credentials::TokenCredentialOptions; +using Azure::Core::Credentials::TokenRequestContext; +using Azure::Core::Http::HttpMethod; +using Azure::Identity::_detail::TokenCredentialImpl; namespace { template std::vector ToUInt8Vector(T const& in) @@ -36,146 +47,128 @@ template std::vector ToUInt8Vector(T const& in) return outVec; } + +template struct UniqueHandleHelper; + +template <> struct UniqueHandleHelper +{ + using type = Azure::Core::_internal::BasicUniqueHandle; +}; + +template <> struct UniqueHandleHelper +{ + using type = Azure::Core::_internal::BasicUniqueHandle; +}; + +template <> struct UniqueHandleHelper +{ + using type = Azure::Core::_internal::BasicUniqueHandle; +}; + +template +using UniqueHandle = Azure::Core::_internal::UniqueHandle; } // namespace -ClientCertificateCredential::ClientCertificateCredential( - std::string const& tenantId, - std::string const& clientId, - std::string const& clientCertificatePath, - Azure::Core::Credentials::TokenCredentialOptions const& options) - : m_tokenCredentialImpl(std::make_unique<_detail::TokenCredentialImpl>(options)), - m_pkey(nullptr) +void Azure::Identity::_detail::FreePkeyImpl(void* pkey) { - BIO* bio = nullptr; - X509* x509 = nullptr; - try - { - { - using Azure::Core::Credentials::AuthenticationException; - - // Open certificate file, then get private key and X509: - if ((bio = BIO_new_file(clientCertificatePath.c_str(), "r")) == nullptr) - { - throw AuthenticationException("Failed to open certificate file."); - } - - if ((m_pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr)) == nullptr) - { - throw AuthenticationException("Failed to read certificate private key."); - } - - if ((x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) == nullptr) - { - static_cast(BIO_seek(bio, 0)); - if ((x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) == nullptr) - { - throw AuthenticationException("Failed to read certificate private key."); - } - } - - static_cast(BIO_free(bio)); - bio = nullptr; - - // Get certificate thumbprint: - { - using Azure::Core::_internal::Base64Url; - - std::string thumbprintHexStr; - std::string thumbprintBase64Str; - { - std::vector mdVec(EVP_MAX_MD_SIZE); - { - unsigned int mdLen = 0; - const auto digestResult = X509_digest(x509, EVP_sha1(), mdVec.data(), &mdLen); - - X509_free(x509); - x509 = nullptr; - - if (!digestResult) - { - throw AuthenticationException("Failed to get certificate thumbprint."); - } - - // Drop unused buffer space: - const auto mdLenSz = static_cast(mdLen); - if (mdVec.size() > mdLenSz) - { - mdVec.resize(mdLenSz); - } - - // Get thumbprint as hex string: - { - std::ostringstream thumbprintStream; - for (const auto md : mdVec) - { - thumbprintStream << std::uppercase << std::hex << std::setfill('0') << std::setw(2) - << static_cast(md); - } - thumbprintHexStr = thumbprintStream.str(); - } - } - - // Get thumbprint as Base64: - thumbprintBase64Str = Base64Url::Base64UrlEncode(ToUInt8Vector(mdVec)); - } - - // Form a JWT token: - const auto tokenHeader = std::string("{\"x5t\":\"") + thumbprintBase64Str + "\",\"kid\":\"" - + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; - - const auto tokenHeaderVec - = std::vector(tokenHeader.begin(), tokenHeader.end()); - - m_tokenHeaderEncoded = Base64Url::Base64UrlEncode(ToUInt8Vector(tokenHeaderVec)); - } - } - - using Azure::Core::Url; - { - - m_requestUrl = Url("https://login.microsoftonline.com/"); - m_requestUrl.AppendPath(tenantId); - m_requestUrl.AppendPath("oauth2/v2.0/token"); - } - - m_tokenPayloadStaticPart = std::string("{\"aud\":\"") + m_requestUrl.GetAbsoluteUrl() - + "\",\"iss\":\"" + clientId + "\",\"sub\":\"" + clientId + "\",\"jti\":\""; - - { - std::ostringstream body; - body - << "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_requestBody = body.str(); - } - } - catch (...) - { - if (bio != nullptr) - { - static_cast(BIO_free(bio)); - } - - if (x509 != nullptr) - { - X509_free(x509); - } - - if (m_pkey != nullptr) - { - EVP_PKEY_free(static_cast(m_pkey)); - } - - throw; - } + EVP_PKEY_free(static_cast(pkey)); } ClientCertificateCredential::ClientCertificateCredential( - std::string const& tenantId, + std::string tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + std::string const& authorityHost, + TokenCredentialOptions const& options) + : m_clientCredentialCore(tenantId, authorityHost), + 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_tokenPayloadStaticPart( + "\",\"iss\":\"" + clientId + "\",\"sub\":\"" + clientId + "\",\"jti\":\"") +{ + std::string thumbprintHexStr; + std::string thumbprintBase64Str; + + { + std::vector mdVec(EVP_MAX_MD_SIZE); + { + UniqueHandle x509; + { + // Open certificate file, then get private key and X509: + UniqueHandle bio(BIO_new_file(clientCertificatePath.c_str(), "r")); + if (!bio) + { + throw AuthenticationException("Failed to open certificate file."); + } + + m_pkey.reset(PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr)); + if (!m_pkey) + { + throw AuthenticationException("Failed to read certificate private key."); + } + + x509.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); + if (!x509) + { + static_cast(BIO_seek(bio.get(), 0)); + x509.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); + if (!x509) + { + throw AuthenticationException("Failed to read X509 section."); + } + } + } + + // Get certificate thumbprint: + unsigned int mdLen = 0; + const auto digestResult = X509_digest(x509.get(), EVP_sha1(), mdVec.data(), &mdLen); + + if (!digestResult) + { + throw AuthenticationException("Failed to get certificate thumbprint."); + } + + // Drop unused buffer space: + const auto mdLenSz = static_cast(mdLen); + if (mdVec.size() > mdLenSz) + { + mdVec.resize(mdLenSz); + } + } + + // Get thumbprint as hex string: + { + std::ostringstream thumbprintStream; + for (const auto md : mdVec) + { + thumbprintStream << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(md); + } + thumbprintHexStr = thumbprintStream.str(); + } + + // Get thumbprint as Base64: + thumbprintBase64Str = Base64Url::Base64UrlEncode(ToUInt8Vector(mdVec)); + } + + // Form a JWT token: + const auto tokenHeader = std::string("{\"x5t\":\"") + thumbprintBase64Str + "\",\"kid\":\"" + + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + + const auto tokenHeaderVec + = std::vector(tokenHeader.begin(), tokenHeader.end()); + + m_tokenHeaderEncoded = Base64Url::Base64UrlEncode(ToUInt8Vector(tokenHeaderVec)); +} + +ClientCertificateCredential::ClientCertificateCredential( + std::string tenantId, std::string const& clientId, std::string const& clientCertificatePath, ClientCertificateCredentialOptions const& options) @@ -183,28 +176,32 @@ ClientCertificateCredential::ClientCertificateCredential( tenantId, clientId, clientCertificatePath, - static_cast(options)) + options.AuthorityHost, + options) { } -ClientCertificateCredential::~ClientCertificateCredential() +ClientCertificateCredential::ClientCertificateCredential( + std::string tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + TokenCredentialOptions const& options) + : ClientCertificateCredential( + tenantId, + clientId, + clientCertificatePath, + ClientCertificateCredentialOptions{}.AuthorityHost, + options) { - EVP_PKEY_free(static_cast(m_pkey)); } -Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken( - Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext, - Azure::Core::Context const& context) const +ClientCertificateCredential::~ClientCertificateCredential() = default; + +AccessToken ClientCertificateCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const { - using _detail::TokenCredentialImpl; - std::string scopesStr; - { - auto const& scopes = tokenRequestContext.Scopes; - if (!scopes.empty()) - { - scopesStr = TokenCredentialImpl::FormatScopes(scopes, false); - } - } + auto const scopesStr = m_clientCredentialCore.GetScopesString(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 @@ -212,38 +209,31 @@ Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken( // lambda might get called. return m_tokenCache.GetToken(scopesStr, tokenRequestContext.MinimumExpiration, [&]() { return m_tokenCredentialImpl->GetToken(context, [&]() { - using Azure::Core::Http::HttpMethod; - - std::ostringstream body; - body << m_requestBody; + auto body = m_requestBody; + if (!scopesStr.empty()) { - if (!scopesStr.empty()) - { - body << "&scope=" << scopesStr; - } + body += "&scope=" + scopesStr; } + auto const requestUrl = m_clientCredentialCore.GetRequestUrl(); + std::string assertion = m_tokenHeaderEncoded; { - using Azure::Core::_internal::Base64Url; // Form the assertion to sign. { std::string payloadStr; // Add GUID, current time, and expiration time to the payload { - using Azure::Core::Uuid; - using Azure::Core::_internal::PosixTimeConverter; + // MSAL has JWT token expiration hardcoded as 10 minutes, without further explanations + // anywhere nearby the constant. + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/01ecd12464007fc1988b6a127aa0b1b980bca1ed/src/client/Microsoft.Identity.Client/Internal/JsonWebTokenConstants.cs#L8 + DateTime const now = std::chrono::system_clock::now(); + DateTime const exp = now + std::chrono::minutes(10); - std::ostringstream payloadStream; - - const Azure::DateTime now = std::chrono::system_clock::now(); - const Azure::DateTime exp = now + std::chrono::minutes(10); - - payloadStream << m_tokenPayloadStaticPart << Uuid::CreateUuid().ToString() - << "\",\"nbf\":" << PosixTimeConverter::DateTimeToPosixTime(now) - << ",\"exp\":" << PosixTimeConverter::DateTimeToPosixTime(exp) << "}"; - - payloadStr = payloadStream.str(); + payloadStr = std::string("{\"aud\":\"") + requestUrl.GetAbsoluteUrl() + + m_tokenPayloadStaticPart + Uuid::CreateUuid().ToString() + + "\",\"nbf\":" + std::to_string(PosixTimeConverter::DateTimeToPosixTime(now)) + + ",\"exp\":" + std::to_string(PosixTimeConverter::DateTimeToPosixTime(exp)) + "}"; } // Concatenate JWT token header + "." + encoded payload @@ -255,53 +245,52 @@ Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken( // Get assertion signature. std::string signature; - if (auto mdCtx = EVP_MD_CTX_new()) { - try + UniqueHandle mdCtx(EVP_MD_CTX_new()); + if (mdCtx) { EVP_PKEY_CTX* signCtx = nullptr; if ((EVP_DigestSignInit( - mdCtx, &signCtx, EVP_sha256(), nullptr, static_cast(m_pkey)) + mdCtx.get(), + &signCtx, + EVP_sha256(), + nullptr, + static_cast(m_pkey.get())) == 1) && (EVP_PKEY_CTX_set_rsa_padding(signCtx, RSA_PKCS1_PADDING) == 1)) { size_t sigLen = 0; - if (EVP_DigestSign(mdCtx, nullptr, &sigLen, nullptr, 0) == 1) + if (EVP_DigestSign(mdCtx.get(), nullptr, &sigLen, nullptr, 0) == 1) { const auto bufToSign = reinterpret_cast(assertion.data()); const auto bufToSignLen = static_cast(assertion.size()); std::vector sigVec(sigLen); - if (EVP_DigestSign(mdCtx, sigVec.data(), &sigLen, bufToSign, bufToSignLen) == 1) + if (EVP_DigestSign(mdCtx.get(), sigVec.data(), &sigLen, bufToSign, bufToSignLen) + == 1) { signature = Base64Url::Base64UrlEncode(ToUInt8Vector(sigVec)); } } } - - if (signature.empty()) - { - throw Azure::Core::Credentials::AuthenticationException( - "Failed to sign token request."); - } - - EVP_MD_CTX_free(mdCtx); - } - catch (...) - { - EVP_MD_CTX_free(mdCtx); - throw; } } + if (signature.empty()) + { + throw Azure::Core::Credentials::AuthenticationException("Failed to sign token request."); + } + // Add signature to the end of assertion assertion += std::string(".") + signature; } - body << "&client_assertion=" << Azure::Core::Url::Encode(assertion); + body += "&client_assertion=" + Azure::Core::Url::Encode(assertion); - auto request = std::make_unique( - HttpMethod::Post, m_requestUrl, body.str()); + auto request + = std::make_unique(HttpMethod::Post, requestUrl, body); + + request->HttpRequest.SetHeader("Host", requestUrl.GetHost()); return request; }); diff --git a/sdk/identity/azure-identity/src/client_credential_core.cpp b/sdk/identity/azure-identity/src/client_credential_core.cpp new file mode 100644 index 000000000..ac1031f4e --- /dev/null +++ b/sdk/identity/azure-identity/src/client_credential_core.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/detail/client_credential_core.hpp" + +#include "private/token_credential_impl.hpp" + +#include + +using Azure::Identity::_detail::ClientCredentialCore; + +using Azure::Core::Url; +using Azure::Core::Credentials::TokenRequestContext; +using Azure::Identity::_detail::TokenCredentialImpl; + +decltype(ClientCredentialCore::AadGlobalAuthority) ClientCredentialCore::AadGlobalAuthority + = "https://login.microsoftonline.com/"; + +ClientCredentialCore::ClientCredentialCore(std::string tenantId, std::string const& authorityHost) + : m_authorityHost(Url(authorityHost)), m_tenantId(std::move(tenantId)) +{ + m_isAdfs = m_tenantId == "adfs"; +} + +Url ClientCredentialCore::GetRequestUrl() const +{ + auto requestUrl = m_authorityHost; + requestUrl.AppendPath(m_tenantId); + requestUrl.AppendPath(m_isAdfs ? "oauth2/token" : "oauth2/v2.0/token"); + + return requestUrl; +} + +std::string ClientCredentialCore::GetScopesString(decltype(TokenRequestContext::Scopes) + const& scopes) const +{ + return scopes.empty() ? std::string() : TokenCredentialImpl::FormatScopes(scopes, m_isAdfs); +} diff --git a/sdk/identity/azure-identity/src/client_secret_credential.cpp b/sdk/identity/azure-identity/src/client_secret_credential.cpp index 8f7073f2e..42fcedfcb 100644 --- a/sdk/identity/azure-identity/src/client_secret_credential.cpp +++ b/sdk/identity/azure-identity/src/client_secret_credential.cpp @@ -5,42 +5,33 @@ #include "private/token_credential_impl.hpp" -#include -#include +using Azure::Identity::ClientSecretCredential; -using namespace Azure::Identity; - -std::string const Azure::Identity::_detail::g_aadGlobalAuthority - = "https://login.microsoftonline.com/"; +using Azure::Core::Context; +using Azure::Core::Url; +using Azure::Core::Credentials::AccessToken; +using Azure::Core::Credentials::TokenCredentialOptions; +using Azure::Core::Credentials::TokenRequestContext; +using Azure::Core::Http::HttpMethod; +using Azure::Identity::_detail::TokenCredentialImpl; ClientSecretCredential::ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, - std::string authorityHost, - Azure::Core::Credentials::TokenCredentialOptions const& options) - : m_tokenCredentialImpl(std::make_unique<_detail::TokenCredentialImpl>(options)), - m_tenantId(std::move(tenantId)), m_clientId(std::move(clientId)), - m_authorityHost(std::move(authorityHost)) + std::string const& authorityHost, + TokenCredentialOptions const& options) + : m_clientCredentialCore(tenantId, authorityHost), + m_tokenCredentialImpl(std::make_unique(options)), + m_requestBody( + std::string("grant_type=client_credentials&client_id=") + Url::Encode(clientId) + + "&client_secret=" + Url::Encode(clientSecret)) { - using Azure::Core::Url; - - m_isAdfs = (m_tenantId == "adfs"); - - m_requestUrl = Url(m_authorityHost); - m_requestUrl.AppendPath(m_tenantId); - m_requestUrl.AppendPath(m_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(clientSecret); - - m_requestBody = body.str(); } ClientSecretCredential::ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, ClientSecretCredentialOptions const& options) : ClientSecretCredential(tenantId, clientId, clientSecret, options.AuthorityHost, options) @@ -49,34 +40,25 @@ ClientSecretCredential::ClientSecretCredential( ClientSecretCredential::ClientSecretCredential( std::string tenantId, - std::string clientId, + std::string const& clientId, std::string const& clientSecret, Core::Credentials::TokenCredentialOptions const& options) : ClientSecretCredential( tenantId, clientId, clientSecret, - _detail::g_aadGlobalAuthority, + ClientSecretCredentialOptions{}.AuthorityHost, options) { } ClientSecretCredential::~ClientSecretCredential() = default; -Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken( - Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext, - Azure::Core::Context const& context) const +AccessToken ClientSecretCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const { - using _detail::TokenCredentialImpl; - - std::string scopesStr; - { - auto const& scopes = tokenRequestContext.Scopes; - if (!scopes.empty()) - { - scopesStr = TokenCredentialImpl::FormatScopes(scopes, m_isAdfs); - } - } + auto const scopesStr = m_clientCredentialCore.GetScopesString(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 @@ -84,20 +66,19 @@ Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken( // lambda might get called. return m_tokenCache.GetToken(scopesStr, tokenRequestContext.MinimumExpiration, [&]() { return m_tokenCredentialImpl->GetToken(context, [&]() { - using Azure::Core::Http::HttpMethod; - - std::ostringstream body; - body << m_requestBody; + auto body = m_requestBody; if (!scopesStr.empty()) { - body << "&scope=" << scopesStr; + body += "&scope=" + scopesStr; } - auto request = std::make_unique( - HttpMethod::Post, m_requestUrl, body.str()); + auto const requestUrl = m_clientCredentialCore.GetRequestUrl(); - request->HttpRequest.SetHeader("Host", m_requestUrl.GetHost()); + auto request + = std::make_unique(HttpMethod::Post, requestUrl, body); + + request->HttpRequest.SetHeader("Host", requestUrl.GetHost()); return request; }); diff --git a/sdk/identity/azure-identity/src/environment_credential.cpp b/sdk/identity/azure-identity/src/environment_credential.cpp index 5c775874b..8ace56ca2 100644 --- a/sdk/identity/azure-identity/src/environment_credential.cpp +++ b/sdk/identity/azure-identity/src/environment_credential.cpp @@ -7,13 +7,17 @@ #include -using namespace Azure::Identity; +using Azure::Identity::EnvironmentCredential; -EnvironmentCredential::EnvironmentCredential( - Azure::Core::Credentials::TokenCredentialOptions options) +using Azure::Core::Context; +using Azure::Core::_internal::Environment; +using Azure::Core::Credentials::AccessToken; +using Azure::Core::Credentials::AuthenticationException; +using Azure::Core::Credentials::TokenCredentialOptions; +using Azure::Core::Credentials::TokenRequestContext; + +EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options) { - using Azure::Core::_internal::Environment; - auto tenantId = Environment::GetVariable("AZURE_TENANT_ID"); auto clientId = Environment::GetVariable("AZURE_CLIENT_ID"); @@ -31,7 +35,6 @@ EnvironmentCredential::EnvironmentCredential( { if (!authority.empty()) { - using namespace Azure::Core::Credentials; ClientSecretCredentialOptions clientSecretCredentialOptions; static_cast(clientSecretCredentialOptions) = options; clientSecretCredentialOptions.AuthorityHost = authority; @@ -53,18 +56,28 @@ EnvironmentCredential::EnvironmentCredential( // } else if (!clientCertificatePath.empty()) { - m_credentialImpl.reset( - new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options)); + if (!authority.empty()) + { + ClientCertificateCredentialOptions clientCertificateCredentialOptions; + static_cast(clientCertificateCredentialOptions) = options; + clientCertificateCredentialOptions.AuthorityHost = authority; + + m_credentialImpl.reset(new ClientCertificateCredential( + tenantId, clientId, clientCertificatePath, clientCertificateCredentialOptions)); + } + else + { + m_credentialImpl.reset( + new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options)); + } } } } -Azure::Core::Credentials::AccessToken EnvironmentCredential::GetToken( - Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext, - Azure::Core::Context const& context) const +AccessToken EnvironmentCredential::GetToken( + TokenRequestContext const& tokenRequestContext, + Context const& context) const { - using namespace Azure::Core::Credentials; - if (!m_credentialImpl) { throw AuthenticationException("EnvironmentCredential authentication unavailable. " diff --git a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp index 6ed07c011..b7ff3a3b4 100644 --- a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp @@ -165,6 +165,269 @@ TEST(ClientCertificateCredential, Regular) EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s); } +TEST(ClientCertificateCredential, AzureStack) +{ + TempCertFile tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ClientCertificateCredentialOptions 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)); + + { + using Azure::Core::_internal::Base64Url; + + const auto assertion0 = request0.Body.substr((sizeof(expectedBodyStart0) - 1)); + const auto assertion1 = request1.Body.substr((sizeof(expectedBodyStart1) - 1)); + + const auto assertion0Parts = SplitString(assertion0, '.'); + const auto assertion1Parts = SplitString(assertion1, '.'); + + EXPECT_EQ(assertion0Parts.size(), 3U); + EXPECT_EQ(assertion1Parts.size(), 3U); + + const auto header0Vec = Base64Url::Base64UrlDecode(assertion0Parts[0]); + const auto header1Vec = Base64Url::Base64UrlDecode(assertion1Parts[0]); + + const auto payload0Vec = Base64Url::Base64UrlDecode(assertion0Parts[1]); + const auto payload1Vec = Base64Url::Base64UrlDecode(assertion1Parts[1]); + + const auto signature0 = assertion0Parts[2]; + const auto signature1 = assertion1Parts[2]; + + const auto header0 = ToString(header0Vec); + const auto header1 = ToString(header1Vec); + + const auto payload0 = ToString(payload0Vec); + const auto payload1 = ToString(payload1Vec); + + constexpr auto ExpectedHeader + = "{\"x5t\":\"V0pIIQwSzNn6vfSTPv-1f7Vt_Pw\",\"kid\":" + "\"574A48210C12CCD9FABDF4933EFFB57FB56DFCFC\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + + EXPECT_EQ(header0, ExpectedHeader); + EXPECT_EQ(header1, ExpectedHeader); + + constexpr char ExpectedPayloadStart[] + = "{\"aud\":\"https://login.microsoftonline.com/adfs/oauth2/token\"," + "\"iss\":\"fedcba98-7654-3210-0123-456789abcdef\"," + "\"sub\":\"fedcba98-7654-3210-0123-456789abcdef\",\"jti\":\""; + + EXPECT_EQ(payload0.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart); + EXPECT_EQ(payload1.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart); + + EXPECT_EQ(Base64Url::Base64UrlDecode(signature0).size(), 256U); + EXPECT_EQ(Base64Url::Base64UrlDecode(signature1).size(), 256U); + } + } + + 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(ClientCertificateCredential, Authority) +{ + TempCertFile tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ClientCertificateCredentialOptions 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)); + + { + using Azure::Core::_internal::Base64Url; + + const auto assertion0 = request0.Body.substr((sizeof(expectedBodyStart0) - 1)); + const auto assertion1 = request1.Body.substr((sizeof(expectedBodyStart1) - 1)); + + const auto assertion0Parts = SplitString(assertion0, '.'); + const auto assertion1Parts = SplitString(assertion1, '.'); + + EXPECT_EQ(assertion0Parts.size(), 3U); + EXPECT_EQ(assertion1Parts.size(), 3U); + + const auto header0Vec = Base64Url::Base64UrlDecode(assertion0Parts[0]); + const auto header1Vec = Base64Url::Base64UrlDecode(assertion1Parts[0]); + + const auto payload0Vec = Base64Url::Base64UrlDecode(assertion0Parts[1]); + const auto payload1Vec = Base64Url::Base64UrlDecode(assertion1Parts[1]); + + const auto signature0 = assertion0Parts[2]; + const auto signature1 = assertion1Parts[2]; + + const auto header0 = ToString(header0Vec); + const auto header1 = ToString(header1Vec); + + const auto payload0 = ToString(payload0Vec); + const auto payload1 = ToString(payload1Vec); + + constexpr auto ExpectedHeader + = "{\"x5t\":\"V0pIIQwSzNn6vfSTPv-1f7Vt_Pw\",\"kid\":" + "\"574A48210C12CCD9FABDF4933EFFB57FB56DFCFC\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + + EXPECT_EQ(header0, ExpectedHeader); + EXPECT_EQ(header1, ExpectedHeader); + + constexpr char ExpectedPayloadStart[] + = "{\"aud\":\"https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/" + "oauth2/v2.0/token\"," + "\"iss\":\"fedcba98-7654-3210-0123-456789abcdef\"," + "\"sub\":\"fedcba98-7654-3210-0123-456789abcdef\",\"jti\":\""; + + EXPECT_EQ(payload0.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart); + EXPECT_EQ(payload1.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart); + + EXPECT_EQ(Base64Url::Base64UrlDecode(signature0).size(), 256U); + EXPECT_EQ(Base64Url::Base64UrlDecode(signature1).size(), 256U); + } + } + + 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";