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 <victor.vazquez@microsoft.com> * PR feedback Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com> Co-authored-by: Victor Vazquez <victor.vazquez@microsoft.com>
This commit is contained in:
parent
d1be7c8bfd
commit
5cb60868e1
@ -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"
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
|
||||
### Features Added
|
||||
|
||||
- Added `ClientCertificateCredential`, and updated `EnvironmentCredential` to support client certificate authentication.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
@ -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})
|
||||
|
||||
|
||||
@ -45,11 +45,16 @@ The Azure Identity library focuses on OAuth authentication with Azure Active dir
|
||||
<td>authenticates a service principal using a secret</td>
|
||||
<td><a href="https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals">Service principal authentication</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ClientCertificateCredential</code></td>
|
||||
<td>authenticates a service principal using a certificate</td>
|
||||
<td><a href="https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals">Service principal authentication</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 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
|
||||
<table border="1" width="100%">
|
||||
@ -75,6 +80,32 @@ The Azure Identity library focuses on OAuth authentication with Azure Active dir
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Service principal with certificate
|
||||
<table border="1" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>variable name</th>
|
||||
<th>value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>AZURE_CLIENT_ID</code></td>
|
||||
<td>id of an Azure Active Directory application</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AZURE_TENANT_ID</code></td>
|
||||
<td>id of the application's Azure Active Directory tenant</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AZURE_CLIENT_CERTIFICATE_PATH</code></td>
|
||||
<td>path to a PEM-encoded certificate file including private key (without password protection)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 <azure/core/credentials/credentials.hpp>
|
||||
#include <azure/core/credentials/token_credential_options.hpp>
|
||||
#include <azure/core/url.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@ -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 .)
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <azure/identity/client_certificate_credential.hpp>
|
||||
|
||||
#include <azure/service/client.hpp>
|
||||
|
||||
// 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<Azure::Identity::ClientCertificateCredential>(
|
||||
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;
|
||||
}
|
||||
@ -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 <azure/core/base64.hpp>
|
||||
#include <azure/core/datetime.hpp>
|
||||
#include <azure/core/uuid.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/ossl_typ.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
using namespace Azure::Identity;
|
||||
|
||||
namespace {
|
||||
template <typename T> std::vector<uint8_t> ToUInt8Vector(T const& in)
|
||||
{
|
||||
const size_t size = in.size();
|
||||
std::vector<uint8_t> outVec(size);
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
outVec[i] = static_cast<uint8_t>(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<void>(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<void>(BIO_free(bio));
|
||||
bio = nullptr;
|
||||
|
||||
// Get certificate thumbprint:
|
||||
{
|
||||
using Azure::Core::_internal::Base64Url;
|
||||
|
||||
std::string thumbprintHexStr;
|
||||
std::string thumbprintBase64Str;
|
||||
{
|
||||
std::vector<unsigned char> 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<decltype(mdVec)::size_type>(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<int>(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<std::string::value_type>(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<void>(BIO_free(bio));
|
||||
}
|
||||
|
||||
if (x509 != nullptr)
|
||||
{
|
||||
X509_free(x509);
|
||||
}
|
||||
|
||||
if (m_pkey != nullptr)
|
||||
{
|
||||
EVP_PKEY_free(static_cast<EVP_PKEY*>(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<Azure::Core::Credentials::TokenCredentialOptions const&>(options))
|
||||
{
|
||||
}
|
||||
|
||||
ClientCertificateCredential::~ClientCertificateCredential()
|
||||
{
|
||||
EVP_PKEY_free(static_cast<EVP_PKEY*>(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<std::string::value_type>(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<EVP_PKEY*>(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<const unsigned char*>(assertion.data());
|
||||
const auto bufToSignLen = static_cast<size_t>(assertion.size());
|
||||
|
||||
std::vector<unsigned char> 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<TokenCredentialImpl::TokenRequest>(
|
||||
HttpMethod::Post, m_requestUrl, body.str());
|
||||
|
||||
return request;
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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 <azure/core/internal/environment.hpp>
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <azure/core/base64.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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<std::string> SplitString(const std::string& s, char separator);
|
||||
|
||||
std::string ToString(std::vector<uint8_t> 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<ClientCertificateCredential>(
|
||||
"01234567-89ab-cdef-fedc-ba8976543210",
|
||||
"fedcba98-7654-3210-0123-456789abcdef",
|
||||
TempCertFile::Path,
|
||||
options);
|
||||
},
|
||||
{{{"https://azure.com/.default"}}, {{}}},
|
||||
std::vector<std::string>{
|
||||
"{\"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<int>(sizeof(expectedBodyStart0) - 1));
|
||||
|
||||
EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end());
|
||||
EXPECT_GT(
|
||||
std::stoi(request1.Headers.at("Content-Length")),
|
||||
static_cast<int>(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: <No Values>\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<std::string> SplitString(const std::string& s, char separator)
|
||||
{
|
||||
std::vector<std::string> 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<uint8_t> 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<std::string::value_type>(vec[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
} // namespace
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "azure-identity-cpp",
|
||||
"version-string": "1.0.0",
|
||||
"dependencies": [
|
||||
"azure-core-cpp"
|
||||
"azure-core-cpp",
|
||||
"openssl"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
"default-features": false,
|
||||
"version>=": "1.4.0"
|
||||
},
|
||||
{
|
||||
"name": "openssl"
|
||||
},
|
||||
{
|
||||
"name": "vcpkg-cmake",
|
||||
"host": true
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user