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:
Anton Kolesnyk 2022-04-28 10:47:45 -07:00 committed by GitHub
parent d1be7c8bfd
commit 5cb60868e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 793 additions and 15 deletions

View File

@ -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"

View File

@ -4,6 +4,8 @@
### Features Added
- Added `ClientCertificateCredential`, and updated `EnvironmentCredential` to support client certificate authentication.
### Breaking Changes
### Bugs Fixed

View File

@ -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})

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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 .)

View File

@ -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;
}

View File

@ -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;
});
}

View File

@ -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);

View File

@ -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));
}
}
}

View File

@ -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

View File

@ -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

View File

@ -2,6 +2,7 @@
"name": "azure-identity-cpp",
"version-string": "1.0.0",
"dependencies": [
"azure-core-cpp"
"azure-core-cpp",
"openssl"
]
}

View File

@ -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")

View File

@ -16,6 +16,9 @@
"default-features": false,
"version>=": "1.4.0"
},
{
"name": "openssl"
},
{
"name": "vcpkg-cmake",
"host": true

View File

@ -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