From 5cb60868e158ed54b9d0a1d732f225e4c38ce916 Mon Sep 17 00:00:00 2001 From: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:47:45 -0700 Subject: [PATCH] Add ClientCertificateCredential (#3578) * Add ClientCertificateCredential * Update unit test * cspell * Update Readme * Cosmetic fixes * Changelog to mention env cred update * Fix warning * cspell * Tell CI to install openssl * openssl for all Windows * update dependency manifest * Re-phrase changelog * Clang warnings * Clang warning * Clang warning - 2 * Ubuntu18 warning * Update sdk/identity/azure-identity/CHANGELOG.md Co-authored-by: Victor Vazquez * PR feedback Co-authored-by: Anton Kolesnyk Co-authored-by: Victor Vazquez --- .../templates/stages/platform-matrix.json | 5 +- sdk/identity/azure-identity/CHANGELOG.md | 2 + sdk/identity/azure-identity/CMakeLists.txt | 5 + sdk/identity/azure-identity/README.md | 33 +- .../azure-identity/inc/azure/identity.hpp | 1 + .../client_certificate_credential.hpp | 96 ++++++ .../azure-identity/samples/CMakeLists.txt | 5 + .../samples/client_certificate_credential.cpp | 42 +++ .../src/client_certificate_credential.cpp | 295 ++++++++++++++++++ .../src/client_secret_credential.cpp | 3 +- .../src/environment_credential.cpp | 16 +- .../azure-identity/test/ut/CMakeLists.txt | 1 + .../ut/client_certificate_credential_test.cpp | 292 +++++++++++++++++ sdk/identity/azure-identity/vcpkg.json | 3 +- .../azure-identity/vcpkg/Config.cmake.in | 2 + sdk/identity/azure-identity/vcpkg/vcpkg.json | 3 + sdk/identity/ci.yml | 4 +- 17 files changed, 793 insertions(+), 15 deletions(-) create mode 100644 sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp create mode 100644 sdk/identity/azure-identity/samples/client_certificate_credential.cpp create mode 100644 sdk/identity/azure-identity/src/client_certificate_credential.cpp create mode 100644 sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp diff --git a/eng/pipelines/templates/stages/platform-matrix.json b/eng/pipelines/templates/stages/platform-matrix.json index 5dbf5885f..1ca588119 100644 --- a/eng/pipelines/templates/stages/platform-matrix.json +++ b/eng/pipelines/templates/stages/platform-matrix.json @@ -68,6 +68,7 @@ { "StaticConfigs": { "Windows2019": { + "VcpkgInstall": "openssl", "OSVmImage": "MMS2019", "Pool": "azsdk-pool-mms-win-2019-general", "CMAKE_GENERATOR": "Visual Studio 16 2019", @@ -77,13 +78,11 @@ }, "TargetPlatform": { "UWP_debug": { - "VcpkgInstall": "openssl", - "CMAKE_SYSTEM_NAME": "WindowsStore", + "CMAKE_SYSTEM_NAME": "WindowsStore", "CMAKE_SYSTEM_VERSION": "10.0", "BuildArgs": "--parallel 8 --config Debug" }, "UWP_release": { - "VcpkgInstall": "openssl", "CMAKE_SYSTEM_NAME": "WindowsStore", "CMAKE_SYSTEM_VERSION": "10.0", "BuildArgs": "--parallel 8 --config Release" diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 092df68ff..2dbf4d1b9 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `ClientCertificateCredential`, and updated `EnvironmentCredential` to support client certificate authentication. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/CMakeLists.txt b/sdk/identity/azure-identity/CMakeLists.txt index 4a9d097b2..f4760cfb0 100644 --- a/sdk/identity/azure-identity/CMakeLists.txt +++ b/sdk/identity/azure-identity/CMakeLists.txt @@ -47,6 +47,7 @@ endif() set( AZURE_IDENTITY_HEADER inc/azure/identity/chained_token_credential.hpp + inc/azure/identity/client_certificate_credential.hpp inc/azure/identity/client_secret_credential.hpp inc/azure/identity/dll_import_export.hpp inc/azure/identity/environment_credential.hpp @@ -60,6 +61,7 @@ set( src/private/package_version.hpp src/private/token_credential_impl.hpp src/chained_token_credential.cpp + src/client_certificate_credential.cpp src/client_secret_credential.cpp src/environment_credential.cpp src/managed_identity_credential.cpp @@ -85,6 +87,9 @@ target_include_directories( target_link_libraries(azure-identity PUBLIC Azure::azure-core) +find_package(OpenSSL REQUIRED) +target_link_libraries(azure-identity PRIVATE OpenSSL::Crypto) + get_az_version("${CMAKE_CURRENT_SOURCE_DIR}/src/private/package_version.hpp") generate_documentation(azure-identity ${AZ_LIBRARY_VERSION}) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 30087d533..02c391d78 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -45,11 +45,16 @@ The Azure Identity library focuses on OAuth authentication with Azure Active dir authenticates a service principal using a secret Service principal authentication + + ClientCertificateCredential + authenticates a service principal using a certificate + Service principal authentication + ## Environment Variables -`EnvironmentCredential` can be configured with environment variables. +`EnvironmentCredential` can be configured with environment variables. Each type of authentication requires values for specific variables: #### Service principal with secret @@ -75,6 +80,32 @@ The Azure Identity library focuses on OAuth authentication with Azure Active dir
+#### Service principal with certificate + + + + + + + + + + + + + + + + + + + + + +
variable namevalue
AZURE_CLIENT_IDid of an Azure Active Directory application
AZURE_TENANT_IDid of the application's Azure Active Directory tenant
AZURE_CLIENT_CERTIFICATE_PATHpath to a PEM-encoded certificate file including private key (without password protection)
+ +Configuration is attempted in the above order. For example, if values for a client secret and certificate are both present, the client secret will be used. + ## Managed Identity Support The [Managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) is supported via the `ManagedIdentityCredential` for the following Azure Services: * [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token) diff --git a/sdk/identity/azure-identity/inc/azure/identity.hpp b/sdk/identity/azure-identity/inc/azure/identity.hpp index 284d1934d..0ef523065 100644 --- a/sdk/identity/azure-identity/inc/azure/identity.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity.hpp @@ -9,6 +9,7 @@ #pragma once #include "azure/identity/chained_token_credential.hpp" +#include "azure/identity/client_certificate_credential.hpp" #include "azure/identity/client_secret_credential.hpp" #include "azure/identity/dll_import_export.hpp" #include "azure/identity/environment_credential.hpp" 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 new file mode 100644 index 000000000..620eedbea --- /dev/null +++ b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Client Certificate Credential and options. + */ + +#pragma once + +#include "azure/identity/dll_import_export.hpp" + +#include +#include +#include + +#include +#include + +namespace Azure { namespace Identity { + namespace _detail { + class TokenCredentialImpl; + } // namespace _detail + + /** + * @brief Options for client certificate authentication. + * + */ + struct ClientCertificateCredentialOptions final : public Core::Credentials::TokenCredentialOptions + { + }; + + /** + * @brief Client Certificate Credential authenticates with the Azure services using a Tenant ID, + * Client ID and a client certificate. + * + */ + class ClientCertificateCredential final : public Core::Credentials::TokenCredential { + private: + 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; + + public: + /** + * @brief Constructs a Client Secret Credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param clientCertificatePath Client certificate path. + * @param options Options for token retrieval. + */ + explicit ClientCertificateCredential( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + Core::Credentials::TokenCredentialOptions const& options + = Core::Credentials::TokenCredentialOptions()); + + /** + * @brief Constructs a Client Secret Credential. + * + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param clientCertificatePath Client certificate path. + * @param options Options for token retrieval. + */ + explicit ClientCertificateCredential( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + ClientCertificateCredentialOptions const& options); + + /** + * @brief Destructs `%ClientCertificateCredential`. + * + */ + ~ClientCertificateCredential() 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/samples/CMakeLists.txt b/sdk/identity/azure-identity/samples/CMakeLists.txt index 14e989f30..c22e87d3c 100644 --- a/sdk/identity/azure-identity/samples/CMakeLists.txt +++ b/sdk/identity/azure-identity/samples/CMakeLists.txt @@ -12,6 +12,11 @@ target_link_libraries(chained_token_credential_sample PRIVATE azure-identity) target_include_directories(chained_token_credential_sample PRIVATE .) create_per_service_target_build_for_sample(identity chained_token_credential_sample) +add_executable(client_certificate_credential_sample client_certificate_credential.cpp) +target_link_libraries(client_certificate_credential_sample PRIVATE azure-identity) +target_include_directories(client_certificate_credential_sample PRIVATE .) +create_per_service_target_build_for_sample(identity client_certificate_credential_sample) + add_executable(client_secret_credential_sample client_secret_credential.cpp) target_link_libraries(client_secret_credential_sample PRIVATE azure-identity) target_include_directories(client_secret_credential_sample PRIVATE .) diff --git a/sdk/identity/azure-identity/samples/client_certificate_credential.cpp b/sdk/identity/azure-identity/samples/client_certificate_credential.cpp new file mode 100644 index 000000000..f5a79c389 --- /dev/null +++ b/sdk/identity/azure-identity/samples/client_certificate_credential.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include + +#include + +// These functions should be getting the real Tenant ID, Client ID, and the Client Certificate to +// authenticate. +std::string GetTenantId() { return std::string(); } +std::string GetClientId() { return std::string(); } +std::string GetClientCertificatePath() { return std::string(); } + +int main() +{ + try + { + // Step 1: Initialize Client Certificate Credential. + auto clientCertificateCredential + = std::make_shared( + GetTenantId(), GetClientId(), GetClientCertificatePath()); + + // Step 2: Pass the credential to an Azure Service Client. + Azure::Service::Client azureServiceClient("serviceUrl", clientCertificateCredential); + + // Step 3: Start using the Azure Service Client. + azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext); + + std::cout << "Success!" << std::endl; + } + catch (const Azure::Core::Credentials::AuthenticationException& exception) + { + // Step 4: Handle authentication errors, if needed + // (invalid credential parameters, insufficient permissions). + std::cout << "Authentication error: " << exception.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/sdk/identity/azure-identity/src/client_certificate_credential.cpp b/sdk/identity/azure-identity/src/client_certificate_credential.cpp new file mode 100644 index 000000000..9ecbbd2d3 --- /dev/null +++ b/sdk/identity/azure-identity/src/client_certificate_credential.cpp @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/client_certificate_credential.hpp" + +#include "private/token_credential_impl.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace Azure::Identity; + +namespace { +template std::vector ToUInt8Vector(T const& in) +{ + const size_t size = in.size(); + std::vector outVec(size); + for (size_t i = 0; i < size; ++i) + { + outVec[i] = static_cast(in[i]); + } + + return outVec; +} +} // 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) +{ + 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; + } +} + +ClientCertificateCredential::ClientCertificateCredential( + std::string const& tenantId, + std::string const& clientId, + std::string const& clientCertificatePath, + ClientCertificateCredentialOptions const& options) + : ClientCertificateCredential( + tenantId, + clientId, + clientCertificatePath, + static_cast(options)) +{ +} + +ClientCertificateCredential::~ClientCertificateCredential() +{ + 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 +{ + return m_tokenCredentialImpl->GetToken(context, [&]() { + using _detail::TokenCredentialImpl; + using Azure::Core::Http::HttpMethod; + + std::ostringstream body; + body << m_requestBody; + { + auto const& scopes = tokenRequestContext.Scopes; + if (!scopes.empty()) + { + body << "&scope=" << TokenCredentialImpl::FormatScopes(scopes, false); + } + } + + 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; + + 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(); + } + + // Concatenate JWT token header + "." + encoded payload + const auto payloadVec + = std::vector(payloadStr.begin(), payloadStr.end()); + + assertion += std::string(".") + Base64Url::Base64UrlEncode(ToUInt8Vector(payloadVec)); + } + + // Get assertion signature. + std::string signature; + if (auto mdCtx = EVP_MD_CTX_new()) + { + try + { + EVP_PKEY_CTX* signCtx = nullptr; + if ((EVP_DigestSignInit( + mdCtx, &signCtx, EVP_sha256(), nullptr, static_cast(m_pkey)) + == 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) + { + 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) + { + 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; + } + } + + // Add signature to the end of assertion + assertion += std::string(".") + signature; + } + + body << "&client_assertion=" << Azure::Core::Url::Encode(assertion); + + auto request = std::make_unique( + HttpMethod::Post, m_requestUrl, body.str()); + + return request; + }); +} diff --git a/sdk/identity/azure-identity/src/client_secret_credential.cpp b/sdk/identity/azure-identity/src/client_secret_credential.cpp index 2b895f745..5b04416c5 100644 --- a/sdk/identity/azure-identity/src/client_secret_credential.cpp +++ b/sdk/identity/azure-identity/src/client_secret_credential.cpp @@ -18,7 +18,8 @@ ClientSecretCredential::ClientSecretCredential( std::string const& clientSecret, std::string const& authorityHost, Azure::Core::Credentials::TokenCredentialOptions const& options) - : m_tokenCredentialImpl(new _detail::TokenCredentialImpl(options)), m_isAdfs(tenantId == "adfs") + : m_tokenCredentialImpl(std::make_unique<_detail::TokenCredentialImpl>(options)), + m_isAdfs(tenantId == "adfs") { using Azure::Core::Url; m_requestUrl = Url(authorityHost); diff --git a/sdk/identity/azure-identity/src/environment_credential.cpp b/sdk/identity/azure-identity/src/environment_credential.cpp index f7f3660de..5c775874b 100644 --- a/sdk/identity/azure-identity/src/environment_credential.cpp +++ b/sdk/identity/azure-identity/src/environment_credential.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT #include "azure/identity/environment_credential.hpp" +#include "azure/identity/client_certificate_credential.hpp" #include "azure/identity/client_secret_credential.hpp" #include @@ -21,8 +22,8 @@ EnvironmentCredential::EnvironmentCredential( // auto username = Environment::GetVariable("AZURE_USERNAME"); // auto password = Environment::GetVariable("AZURE_PASSWORD"); - // - // auto clientCertificatePath = Environment::GetVariable("AZURE_CLIENT_CERTIFICATE_PATH"); + + auto clientCertificatePath = Environment::GetVariable("AZURE_CLIENT_CERTIFICATE_PATH"); if (!tenantId.empty() && !clientId.empty()) { @@ -44,16 +45,17 @@ EnvironmentCredential::EnvironmentCredential( new ClientSecretCredential(tenantId, clientId, clientSecret, options)); } } - // TODO: These credential types are not implemented. Uncomment when implemented. + // TODO: UsernamePasswordCredential is not implemented. Uncomment when implemented. // else if (!username.empty() && !password.empty()) // { // m_credentialImpl.reset( // new UsernamePasswordCredential(tenantId, clientId, username, password, options)); // } - // else if (!clientCertificatePath.empty()) - // { - // m_credentialImpl.reset(new ClientCertificateCredential(tenantId, clientId, options)); - // } + else if (!clientCertificatePath.empty()) + { + m_credentialImpl.reset( + new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options)); + } } } diff --git a/sdk/identity/azure-identity/test/ut/CMakeLists.txt b/sdk/identity/azure-identity/test/ut/CMakeLists.txt index 512825359..784e32018 100644 --- a/sdk/identity/azure-identity/test/ut/CMakeLists.txt +++ b/sdk/identity/azure-identity/test/ut/CMakeLists.txt @@ -17,6 +17,7 @@ add_compile_definitions(AZURE_TEST_RECORDING_DIR="${CMAKE_CURRENT_LIST_DIR}") add_executable ( azure-identity-test chained_token_credential_test.cpp + client_certificate_credential_test.cpp client_secret_credential_test.cpp credential_test_helper.cpp credential_test_helper.hpp 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 new file mode 100644 index 000000000..6ed07c011 --- /dev/null +++ b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/identity/client_certificate_credential.hpp" + +#include "credential_test_helper.hpp" + +#include + +#include +#include + +#include + +using Azure::Core::Http::HttpMethod; +using Azure::Identity::ClientCertificateCredential; +using Azure::Identity::ClientCertificateCredentialOptions; +using Azure::Identity::Test::_detail::CredentialTestHelper; + +namespace { +struct TempCertFile final +{ + static const char* const Path; + ~TempCertFile(); + TempCertFile(); +}; + +std::vector SplitString(const std::string& s, char separator); + +std::string ToString(std::vector const& vec); +} // namespace + +TEST(ClientCertificateCredential, Regular) +{ + TempCertFile tempCertFile; + + auto const actual = CredentialTestHelper::SimulateTokenRequest( + [](auto transport) { + ClientCertificateCredentialOptions 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)); + + { + 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/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"; + +TempCertFile::~TempCertFile() { std::remove(Path); } + +TempCertFile::TempCertFile() +{ + std::ofstream cert(Path, std::ios_base::out | std::ios_base::trunc); + + cert << // cspell:disable + "Bag Attributes\n" + " Microsoft Local Key set: \n" + " localKeyID: 01 00 00 00 \n" + " friendlyName: te-66f5c973-4fc8-4cd3-8acc-64964d79b693\n" + " Microsoft CSP Name: Microsoft Software Key Storage Provider\n" + "Key Attributes\n" + " X509v3 Key Usage: 90 \n" + "-----BEGIN PRIVATE KEY-----\n"; + // cspell:enable + + cert << // cspell:disable + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPdm4pukO7ugEx\n" + "8wXrmo4VIEoicp7w3QsEJGA2bMx9nHMvwugG54t14QpfqBQYQWLeL1HmpcDeivVD\n" + "+15ZXeGLCPVZBHhoY8ZWGibfhAAzqQ0P9Ca1kydjvB4uJcEnF/RYtQv6n6OwmdO1\n" + "wJ22JNcRlMtZqmnb/Q0In2fjXEbdl85/GZlYzMQRdyfI0yriSRBcYV2kg0zeXCxf\n" + "mCvB3rb6I1KpoUFHlkeHtkeDwm0VHUEt4Hz8ghcB00tI5eS2fH2rPkINQKc6+0QU\n" + "C2KICQC+GzJsYDbwQOao5Vhk80H5LRuM9Ndzv+fU3lLnktYCgXgL9AX4L/R9Z4Pz\n" + "tuao/qbRAgMBAAECggEBAMQZIrooiTuZ7uVC3Ja96Y1IjyqOg3QSzAXnSFZJcuVM\n" + "i4hayC02khkjVUXjvtLKg2SW/+hvRqZUXM8cfCsm1Tkxh4/T7OhnXyMl5xahU/uA\n" + "0IsC8c/xv2rDdxeRskh8mQd8Yk1MtlIIpRgIcEqp+exxY+FmdldtkvNSkcVUBNwQ\n" + "nXi+oWPhE2guo2g1BPk2gbF0+3FvSrQ8QwGHg+uQJwrQpJ+SB9TyuQFauGR5/wSq\n" + "H93cFH5YC/+v5I7qW6ZQe0f7rEKQDybGVzkBlKJyGCVYmPn7Xa/wJriws+FZIfHz\n" + "f3m0kJigxJd/HwTrnKSg+H8oBgng7lZLdBYWHMGJhA0CgYEA48moW7szegvfLuUF\n" + "a0sHfyKuNyvOv7Wud4sa0lwdKPHS+atwL6TNUWCAGkomYADEe3qiYgMXDX9U3hlW\n"; + // cspell:enable + + cert << // cspell:disable + "6zktYFj03tnRg4iBjp8nchLBVLf3Wd5TPRw1VKu4ZW43y8BRhYWV+3Z4s1nyMEDA\n" + "NFbKRmL7LDB05oWHdJMjFK/L6YcCgYEA6ShV4v2RQiXzkW6GHSBZDIVHCeWwvIld\n" + "OlEfG7wzZW4e8wNDhfSMtXyJrzfbEyXBtVKoESdP6Nnm9W7ftcynW965S94THuy7\n" + "+ofvHo6JAm8g/0uX70wZ26LU8qhkJMTWmsONBNKLwUzkFT7VGsdaBliam1RLvjeT\n" + "URdQgnftIucCgYEA4FYamT0k1W4bv/OOAr1CBNQDABME64ni6Zj2MXbGwSxou7s8\n" + "IbANBbgkcb/VS3d2CqYchqrEaWaeDp6mG8OUDO+POmsLDJ/D+NKF5rLR9L25vahY\n" + "EjdVzq3QTRTfnqspnnaR37Yt6XUMMLmUkfdn/yo8dKjEeMPJQ+YlBpqcGMECgYBZ\n" + "rmIaxV2yC9b8AX8khOS7pCgG7opkepGZdMp6aJF8WjcdUgwO4lmdFSIAe4OQgd1Y\n" + "WUq8Dlr2PZpQnSz/SJC3DZxISksggf5sBw06u6iHfyc6C2GNccAgcylljM+4NN42\n" + "+TCswi9vUpwIb/qYKkW+WyZcyLe5mrbXYhhdlrNn0QKBgDe8aRG+MOSUTTXjAVss\n" + "bDY0Us943FN91qBmagNqDyozKAAqDoKvdRxM0IlIDnOptj4AfbpJ1JThNOJDYBpU\n" + "+Azo8UoedANgndtZ2n11RSjmlQ6TE/WGlsirHExqr6y/l71znoQm1y3E2cArbsmy\n" + "hp0P5v42PKxmAx4pR0EjNKsd\n"; + // cspell:enable + + cert << // cspell:disable + "-----END PRIVATE KEY-----\n" + "Bag Attributes\n" + " localKeyID: 01 00 00 00 \n" + " 1.3.6.1.4.1.311.17.3.71: 61 00 6E 00 74 00 6B 00 2D 00 6C 00 61 00 70 00 " + "74 00 6F 00 70 00 00 00 \n" + "subject=CN = azure-identity-test\n" + "\n" + "issuer=CN = azure-identity-test\n" + "\n" + "-----BEGIN CERTIFICATE-----\n"; + // cspell:enable + + cert << // cspell:disable + "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" + "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" + "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" + "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" + "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" + "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" + "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" + "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n"; + // cspell:enable + + cert << // cspell:disable + "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" + "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" + "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" + "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" + "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" + "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" + "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" + "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" + "RrCsEpJsnJXkx/6O\n" + "-----END CERTIFICATE-----\n"; + // cspell:enable +} + +std::vector SplitString(const std::string& s, char separator) +{ + std::vector result; + + const auto len = s.size(); + size_t start = 0; + while (start < len) + { + auto end = s.find(separator, start); + if (end == std::string::npos) + { + end = len; + } + + result.push_back(s.substr(start, end - start)); + + start = end + 1; + } + + return result; +} + +std::string ToString(std::vector const& vec) +{ + const size_t size = vec.size(); + std::string str(size, '\0'); + for (size_t i = 0; i < size; ++i) + { + str[i] = static_cast(vec[i]); + } + + return str; +} +} // namespace diff --git a/sdk/identity/azure-identity/vcpkg.json b/sdk/identity/azure-identity/vcpkg.json index 22c9f4be7..222462e84 100644 --- a/sdk/identity/azure-identity/vcpkg.json +++ b/sdk/identity/azure-identity/vcpkg.json @@ -2,6 +2,7 @@ "name": "azure-identity-cpp", "version-string": "1.0.0", "dependencies": [ - "azure-core-cpp" + "azure-core-cpp", + "openssl" ] } diff --git a/sdk/identity/azure-identity/vcpkg/Config.cmake.in b/sdk/identity/azure-identity/vcpkg/Config.cmake.in index 588fe3e91..7da70019f 100644 --- a/sdk/identity/azure-identity/vcpkg/Config.cmake.in +++ b/sdk/identity/azure-identity/vcpkg/Config.cmake.in @@ -6,6 +6,8 @@ include(CMakeFindDependencyMacro) find_dependency(azure-core-cpp "1.4.0") +find_dependency(OpenSSL) + include("${CMAKE_CURRENT_LIST_DIR}/azure-identity-cppTargets.cmake") check_required_components("azure-identity-cpp") diff --git a/sdk/identity/azure-identity/vcpkg/vcpkg.json b/sdk/identity/azure-identity/vcpkg/vcpkg.json index 27f3485de..d53c9d7fd 100644 --- a/sdk/identity/azure-identity/vcpkg/vcpkg.json +++ b/sdk/identity/azure-identity/vcpkg/vcpkg.json @@ -16,6 +16,9 @@ "default-features": false, "version>=": "1.4.0" }, + { + "name": "openssl" + }, { "name": "vcpkg-cmake", "host": true diff --git a/sdk/identity/ci.yml b/sdk/identity/ci.yml index 63862a4a6..f3d26dd38 100644 --- a/sdk/identity/ci.yml +++ b/sdk/identity/ci.yml @@ -28,8 +28,8 @@ stages: ServiceDirectory: identity CtestRegex: azure-identity. LiveTestCtestRegex: azure-identity. - LineCoverageTarget: 99 - BranchCoverageTarget: 62 + LineCoverageTarget: 95 + BranchCoverageTarget: 57 Artifacts: - Name: azure-identity Path: azure-identity