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 name |
+ value |
+
+
+
+
+ AZURE_CLIENT_ID |
+ id of an Azure Active Directory application |
+
+
+ AZURE_TENANT_ID |
+ id of the application's Azure Active Directory tenant |
+
+
+ AZURE_CLIENT_CERTIFICATE_PATH |
+ path 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