ClientCertificateCredential: Add AuthorityHost override and Azure Stack support (#4181)
* ClientCertificateCredential: Add AuthorityHost override and Azure Stack support * Use RAII types for handles Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com>
This commit is contained in:
parent
7a729bed81
commit
40f2fde457
1
.vscode/cspell.json
vendored
1
.vscode/cspell.json
vendored
@ -91,6 +91,7 @@
|
||||
"LPWSTR",
|
||||
"MHSM",
|
||||
"moxygen",
|
||||
"MSAL",
|
||||
"MSRC",
|
||||
"ncus",
|
||||
"Niels",
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
### Features Added
|
||||
|
||||
- Added Azure CLI Credential.
|
||||
- Added authority host overriding support for `ClientCertificateCredential`.
|
||||
- Added Azure Stack support for `ClientCertificateCredential`.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ endif()
|
||||
|
||||
set(
|
||||
AZURE_IDENTITY_HEADER
|
||||
inc/azure/identity/detail/client_credential_core.hpp
|
||||
inc/azure/identity/detail/token_cache.hpp
|
||||
inc/azure/identity/azure_cli_credential.hpp
|
||||
inc/azure/identity/chained_token_credential.hpp
|
||||
@ -66,6 +67,7 @@ set(
|
||||
src/azure_cli_credential.cpp
|
||||
src/chained_token_credential.cpp
|
||||
src/client_certificate_credential.cpp
|
||||
src/client_credential_core.cpp
|
||||
src/client_secret_credential.cpp
|
||||
src/environment_credential.cpp
|
||||
src/managed_identity_credential.cpp
|
||||
|
||||
@ -8,10 +8,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "azure/identity/detail/client_credential_core.hpp"
|
||||
#include "azure/identity/detail/token_cache.hpp"
|
||||
|
||||
#include <azure/core/credentials/credentials.hpp>
|
||||
#include <azure/core/credentials/token_credential_options.hpp>
|
||||
#include <azure/core/internal/unique_handle.hpp>
|
||||
#include <azure/core/url.hpp>
|
||||
|
||||
#include <memory>
|
||||
@ -20,6 +22,17 @@
|
||||
namespace Azure { namespace Identity {
|
||||
namespace _detail {
|
||||
class TokenCredentialImpl;
|
||||
|
||||
void FreePkeyImpl(void* pkey);
|
||||
|
||||
template <typename> struct UniquePkeyHelper;
|
||||
template <> struct UniquePkeyHelper<void*>
|
||||
{
|
||||
static void FreePkey(void* pkey) { FreePkeyImpl(pkey); }
|
||||
using type = Azure::Core::_internal::BasicUniqueHandle<void, FreePkey>;
|
||||
};
|
||||
|
||||
using UniquePkeyHandle = Azure::Core::_internal::UniqueHandle<void*, UniquePkeyHelper>;
|
||||
} // namespace _detail
|
||||
|
||||
/**
|
||||
@ -28,6 +41,15 @@ namespace Azure { namespace Identity {
|
||||
*/
|
||||
struct ClientCertificateCredentialOptions final : public Core::Credentials::TokenCredentialOptions
|
||||
{
|
||||
/**
|
||||
* @brief Authentication authority URL.
|
||||
* @note Default value is Azure AD global authority (https://login.microsoftonline.com/).
|
||||
*
|
||||
* @note Example of an authority host string: "https://login.microsoftonline.us/". See national
|
||||
* clouds' Azure AD authentication endpoints:
|
||||
* https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud.
|
||||
*/
|
||||
std::string AuthorityHost = _detail::ClientCredentialCore::AadGlobalAuthority;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -38,12 +60,19 @@ namespace Azure { namespace Identity {
|
||||
class ClientCertificateCredential final : public Core::Credentials::TokenCredential {
|
||||
private:
|
||||
_detail::TokenCache m_tokenCache;
|
||||
_detail::ClientCredentialCore m_clientCredentialCore;
|
||||
std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl;
|
||||
Core::Url m_requestUrl;
|
||||
std::string m_requestBody;
|
||||
std::string m_tokenHeaderEncoded;
|
||||
std::string m_tokenPayloadStaticPart;
|
||||
void* m_pkey;
|
||||
std::string m_tokenHeaderEncoded;
|
||||
_detail::UniquePkeyHandle m_pkey;
|
||||
|
||||
explicit ClientCertificateCredential(
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
std::string const& authorityHost,
|
||||
Core::Credentials::TokenCredentialOptions const& options);
|
||||
|
||||
public:
|
||||
/**
|
||||
@ -55,7 +84,7 @@ namespace Azure { namespace Identity {
|
||||
* @param options Options for token retrieval.
|
||||
*/
|
||||
explicit ClientCertificateCredential(
|
||||
std::string const& tenantId,
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
Core::Credentials::TokenCredentialOptions const& options
|
||||
@ -70,7 +99,7 @@ namespace Azure { namespace Identity {
|
||||
* @param options Options for token retrieval.
|
||||
*/
|
||||
explicit ClientCertificateCredential(
|
||||
std::string const& tenantId,
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
ClientCertificateCredentialOptions const& options);
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "azure/identity/detail/client_credential_core.hpp"
|
||||
#include "azure/identity/detail/token_cache.hpp"
|
||||
#include "azure/identity/dll_import_export.hpp"
|
||||
|
||||
#include <azure/core/credentials/credentials.hpp>
|
||||
#include <azure/core/credentials/token_credential_options.hpp>
|
||||
@ -21,7 +21,6 @@
|
||||
namespace Azure { namespace Identity {
|
||||
namespace _detail {
|
||||
class TokenCredentialImpl;
|
||||
AZ_IDENTITY_DLLEXPORT extern std::string const g_aadGlobalAuthority;
|
||||
} // namespace _detail
|
||||
|
||||
/**
|
||||
@ -34,11 +33,11 @@ namespace Azure { namespace Identity {
|
||||
* @brief Authentication authority URL.
|
||||
* @note Default value is Azure AD global authority (https://login.microsoftonline.com/).
|
||||
*
|
||||
* @note Example of a \p authority string: "https://login.microsoftonline.us/". See national
|
||||
* @note Example of an authority host string: "https://login.microsoftonline.us/". See national
|
||||
* clouds' Azure AD authentication endpoints:
|
||||
* https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud.
|
||||
*/
|
||||
std::string AuthorityHost = _detail::g_aadGlobalAuthority;
|
||||
std::string AuthorityHost = _detail::ClientCredentialCore::AadGlobalAuthority;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -49,21 +48,15 @@ namespace Azure { namespace Identity {
|
||||
class ClientSecretCredential final : public Core::Credentials::TokenCredential {
|
||||
private:
|
||||
_detail::TokenCache m_tokenCache;
|
||||
_detail::ClientCredentialCore m_clientCredentialCore;
|
||||
std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl;
|
||||
Core::Url m_requestUrl;
|
||||
std::string m_requestBody;
|
||||
|
||||
std::string m_tenantId;
|
||||
std::string m_clientId;
|
||||
std::string m_authorityHost;
|
||||
|
||||
bool m_isAdfs;
|
||||
|
||||
ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
std::string authorityHost,
|
||||
std::string const& authorityHost,
|
||||
Core::Credentials::TokenCredentialOptions const& options);
|
||||
|
||||
public:
|
||||
@ -77,7 +70,7 @@ namespace Azure { namespace Identity {
|
||||
*/
|
||||
explicit ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
ClientSecretCredentialOptions const& options);
|
||||
|
||||
@ -91,7 +84,7 @@ namespace Azure { namespace Identity {
|
||||
*/
|
||||
explicit ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
Core::Credentials::TokenCredentialOptions const& options
|
||||
= Core::Credentials::TokenCredentialOptions());
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "azure/identity/dll_import_export.hpp"
|
||||
|
||||
#include <azure/core/credentials/credentials.hpp>
|
||||
#include <azure/core/url.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Azure { namespace Identity { namespace _detail {
|
||||
class ClientCredentialCore final {
|
||||
Core::Url m_authorityHost;
|
||||
std::string m_tenantId;
|
||||
bool m_isAdfs;
|
||||
|
||||
public:
|
||||
AZ_IDENTITY_DLLEXPORT static std::string const AadGlobalAuthority;
|
||||
|
||||
explicit ClientCredentialCore(std::string tenantId, std::string const& authorityHost);
|
||||
|
||||
Core::Url GetRequestUrl() const;
|
||||
|
||||
std::string GetScopesString(decltype(Core::Credentials::TokenRequestContext::Scopes)
|
||||
const& scopes) const;
|
||||
};
|
||||
}}} // namespace Azure::Identity::_detail
|
||||
@ -34,8 +34,11 @@ namespace Azure { namespace Identity {
|
||||
* - AZURE_CLIENT_ID
|
||||
* - AZURE_CLIENT_SECRET
|
||||
* - AZURE_CLIENT_CERTIFICATE_PATH
|
||||
* - AZURE_CLIENT_CERTIFICATE_PASSWORD
|
||||
* - AZURE_CLIENT_SEND_CERTIFICATE_CHAIN
|
||||
* - AZURE_USERNAME
|
||||
* - AZURE_PASSWORD
|
||||
* - AZURE_AUTHORITY_HOST
|
||||
*/
|
||||
explicit EnvironmentCredential(
|
||||
Azure::Core::Credentials::TokenCredentialOptions options
|
||||
|
||||
@ -157,14 +157,7 @@ namespace {
|
||||
template <typename> struct UniqueHandleHelper;
|
||||
template <> struct UniqueHandleHelper<HANDLE>
|
||||
{
|
||||
static void CloseWin32Handle(HANDLE handle)
|
||||
{
|
||||
if (handle != nullptr)
|
||||
{
|
||||
static_cast<void>(CloseHandle(handle));
|
||||
}
|
||||
}
|
||||
|
||||
static void CloseWin32Handle(HANDLE handle) { static_cast<void>(CloseHandle(handle)); }
|
||||
using type = Azure::Core::_internal::BasicUniqueHandle<void, CloseWin32Handle>;
|
||||
};
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
@ -22,7 +21,19 @@
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
using namespace Azure::Identity;
|
||||
using Azure::Identity::ClientCertificateCredential;
|
||||
|
||||
using Azure::Core::Context;
|
||||
using Azure::Core::Url;
|
||||
using Azure::Core::Uuid;
|
||||
using Azure::Core::_internal::Base64Url;
|
||||
using Azure::Core::_internal::PosixTimeConverter;
|
||||
using Azure::Core::Credentials::AccessToken;
|
||||
using Azure::Core::Credentials::AuthenticationException;
|
||||
using Azure::Core::Credentials::TokenCredentialOptions;
|
||||
using Azure::Core::Credentials::TokenRequestContext;
|
||||
using Azure::Core::Http::HttpMethod;
|
||||
using Azure::Identity::_detail::TokenCredentialImpl;
|
||||
|
||||
namespace {
|
||||
template <typename T> std::vector<uint8_t> ToUInt8Vector(T const& in)
|
||||
@ -36,146 +47,128 @@ template <typename T> std::vector<uint8_t> ToUInt8Vector(T const& in)
|
||||
|
||||
return outVec;
|
||||
}
|
||||
|
||||
template <typename> struct UniqueHandleHelper;
|
||||
|
||||
template <> struct UniqueHandleHelper<BIO>
|
||||
{
|
||||
using type = Azure::Core::_internal::BasicUniqueHandle<BIO, BIO_free_all>;
|
||||
};
|
||||
|
||||
template <> struct UniqueHandleHelper<X509>
|
||||
{
|
||||
using type = Azure::Core::_internal::BasicUniqueHandle<X509, X509_free>;
|
||||
};
|
||||
|
||||
template <> struct UniqueHandleHelper<EVP_MD_CTX>
|
||||
{
|
||||
using type = Azure::Core::_internal::BasicUniqueHandle<EVP_MD_CTX, EVP_MD_CTX_free>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using UniqueHandle = Azure::Core::_internal::UniqueHandle<T, UniqueHandleHelper>;
|
||||
} // namespace
|
||||
|
||||
ClientCertificateCredential::ClientCertificateCredential(
|
||||
std::string const& tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
Azure::Core::Credentials::TokenCredentialOptions const& options)
|
||||
: m_tokenCredentialImpl(std::make_unique<_detail::TokenCredentialImpl>(options)),
|
||||
m_pkey(nullptr)
|
||||
void Azure::Identity::_detail::FreePkeyImpl(void* pkey)
|
||||
{
|
||||
BIO* bio = nullptr;
|
||||
X509* x509 = nullptr;
|
||||
try
|
||||
{
|
||||
{
|
||||
using Azure::Core::Credentials::AuthenticationException;
|
||||
|
||||
// Open certificate file, then get private key and X509:
|
||||
if ((bio = BIO_new_file(clientCertificatePath.c_str(), "r")) == nullptr)
|
||||
{
|
||||
throw AuthenticationException("Failed to open certificate file.");
|
||||
}
|
||||
|
||||
if ((m_pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr)) == nullptr)
|
||||
{
|
||||
throw AuthenticationException("Failed to read certificate private key.");
|
||||
}
|
||||
|
||||
if ((x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) == nullptr)
|
||||
{
|
||||
static_cast<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;
|
||||
}
|
||||
EVP_PKEY_free(static_cast<EVP_PKEY*>(pkey));
|
||||
}
|
||||
|
||||
ClientCertificateCredential::ClientCertificateCredential(
|
||||
std::string const& tenantId,
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
std::string const& authorityHost,
|
||||
TokenCredentialOptions const& options)
|
||||
: m_clientCredentialCore(tenantId, authorityHost),
|
||||
m_tokenCredentialImpl(std::make_unique<TokenCredentialImpl>(options)),
|
||||
m_requestBody(
|
||||
std::string(
|
||||
"grant_type=client_credentials"
|
||||
"&client_assertion_type="
|
||||
"urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" // cspell:disable-line
|
||||
"&client_id=")
|
||||
+ Url::Encode(clientId)),
|
||||
m_tokenPayloadStaticPart(
|
||||
"\",\"iss\":\"" + clientId + "\",\"sub\":\"" + clientId + "\",\"jti\":\"")
|
||||
{
|
||||
std::string thumbprintHexStr;
|
||||
std::string thumbprintBase64Str;
|
||||
|
||||
{
|
||||
std::vector<unsigned char> mdVec(EVP_MAX_MD_SIZE);
|
||||
{
|
||||
UniqueHandle<X509> x509;
|
||||
{
|
||||
// Open certificate file, then get private key and X509:
|
||||
UniqueHandle<BIO> bio(BIO_new_file(clientCertificatePath.c_str(), "r"));
|
||||
if (!bio)
|
||||
{
|
||||
throw AuthenticationException("Failed to open certificate file.");
|
||||
}
|
||||
|
||||
m_pkey.reset(PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr));
|
||||
if (!m_pkey)
|
||||
{
|
||||
throw AuthenticationException("Failed to read certificate private key.");
|
||||
}
|
||||
|
||||
x509.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
|
||||
if (!x509)
|
||||
{
|
||||
static_cast<void>(BIO_seek(bio.get(), 0));
|
||||
x509.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
|
||||
if (!x509)
|
||||
{
|
||||
throw AuthenticationException("Failed to read X509 section.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get certificate thumbprint:
|
||||
unsigned int mdLen = 0;
|
||||
const auto digestResult = X509_digest(x509.get(), EVP_sha1(), mdVec.data(), &mdLen);
|
||||
|
||||
if (!digestResult)
|
||||
{
|
||||
throw AuthenticationException("Failed to get certificate thumbprint.");
|
||||
}
|
||||
|
||||
// Drop unused buffer space:
|
||||
const auto mdLenSz = static_cast<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));
|
||||
}
|
||||
|
||||
ClientCertificateCredential::ClientCertificateCredential(
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
ClientCertificateCredentialOptions const& options)
|
||||
@ -183,28 +176,32 @@ ClientCertificateCredential::ClientCertificateCredential(
|
||||
tenantId,
|
||||
clientId,
|
||||
clientCertificatePath,
|
||||
static_cast<Azure::Core::Credentials::TokenCredentialOptions const&>(options))
|
||||
options.AuthorityHost,
|
||||
options)
|
||||
{
|
||||
}
|
||||
|
||||
ClientCertificateCredential::~ClientCertificateCredential()
|
||||
ClientCertificateCredential::ClientCertificateCredential(
|
||||
std::string tenantId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientCertificatePath,
|
||||
TokenCredentialOptions const& options)
|
||||
: ClientCertificateCredential(
|
||||
tenantId,
|
||||
clientId,
|
||||
clientCertificatePath,
|
||||
ClientCertificateCredentialOptions{}.AuthorityHost,
|
||||
options)
|
||||
{
|
||||
EVP_PKEY_free(static_cast<EVP_PKEY*>(m_pkey));
|
||||
}
|
||||
|
||||
Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken(
|
||||
Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext,
|
||||
Azure::Core::Context const& context) const
|
||||
ClientCertificateCredential::~ClientCertificateCredential() = default;
|
||||
|
||||
AccessToken ClientCertificateCredential::GetToken(
|
||||
TokenRequestContext const& tokenRequestContext,
|
||||
Context const& context) const
|
||||
{
|
||||
using _detail::TokenCredentialImpl;
|
||||
std::string scopesStr;
|
||||
{
|
||||
auto const& scopes = tokenRequestContext.Scopes;
|
||||
if (!scopes.empty())
|
||||
{
|
||||
scopesStr = TokenCredentialImpl::FormatScopes(scopes, false);
|
||||
}
|
||||
}
|
||||
auto const scopesStr = m_clientCredentialCore.GetScopesString(tokenRequestContext.Scopes);
|
||||
|
||||
// TokenCache::GetToken() and m_tokenCredentialImpl->GetToken() can only use the lambda argument
|
||||
// when they are being executed. They are not supposed to keep a reference to lambda argument to
|
||||
@ -212,38 +209,31 @@ Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken(
|
||||
// lambda might get called.
|
||||
return m_tokenCache.GetToken(scopesStr, tokenRequestContext.MinimumExpiration, [&]() {
|
||||
return m_tokenCredentialImpl->GetToken(context, [&]() {
|
||||
using Azure::Core::Http::HttpMethod;
|
||||
|
||||
std::ostringstream body;
|
||||
body << m_requestBody;
|
||||
auto body = m_requestBody;
|
||||
if (!scopesStr.empty())
|
||||
{
|
||||
if (!scopesStr.empty())
|
||||
{
|
||||
body << "&scope=" << scopesStr;
|
||||
}
|
||||
body += "&scope=" + scopesStr;
|
||||
}
|
||||
|
||||
auto const requestUrl = m_clientCredentialCore.GetRequestUrl();
|
||||
|
||||
std::string assertion = m_tokenHeaderEncoded;
|
||||
{
|
||||
using Azure::Core::_internal::Base64Url;
|
||||
// Form the assertion to sign.
|
||||
{
|
||||
std::string payloadStr;
|
||||
// Add GUID, current time, and expiration time to the payload
|
||||
{
|
||||
using Azure::Core::Uuid;
|
||||
using Azure::Core::_internal::PosixTimeConverter;
|
||||
// MSAL has JWT token expiration hardcoded as 10 minutes, without further explanations
|
||||
// anywhere nearby the constant.
|
||||
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/01ecd12464007fc1988b6a127aa0b1b980bca1ed/src/client/Microsoft.Identity.Client/Internal/JsonWebTokenConstants.cs#L8
|
||||
DateTime const now = std::chrono::system_clock::now();
|
||||
DateTime const exp = now + std::chrono::minutes(10);
|
||||
|
||||
std::ostringstream payloadStream;
|
||||
|
||||
const Azure::DateTime now = std::chrono::system_clock::now();
|
||||
const Azure::DateTime exp = now + std::chrono::minutes(10);
|
||||
|
||||
payloadStream << m_tokenPayloadStaticPart << Uuid::CreateUuid().ToString()
|
||||
<< "\",\"nbf\":" << PosixTimeConverter::DateTimeToPosixTime(now)
|
||||
<< ",\"exp\":" << PosixTimeConverter::DateTimeToPosixTime(exp) << "}";
|
||||
|
||||
payloadStr = payloadStream.str();
|
||||
payloadStr = std::string("{\"aud\":\"") + requestUrl.GetAbsoluteUrl()
|
||||
+ m_tokenPayloadStaticPart + Uuid::CreateUuid().ToString()
|
||||
+ "\",\"nbf\":" + std::to_string(PosixTimeConverter::DateTimeToPosixTime(now))
|
||||
+ ",\"exp\":" + std::to_string(PosixTimeConverter::DateTimeToPosixTime(exp)) + "}";
|
||||
}
|
||||
|
||||
// Concatenate JWT token header + "." + encoded payload
|
||||
@ -255,53 +245,52 @@ Azure::Core::Credentials::AccessToken ClientCertificateCredential::GetToken(
|
||||
|
||||
// Get assertion signature.
|
||||
std::string signature;
|
||||
if (auto mdCtx = EVP_MD_CTX_new())
|
||||
{
|
||||
try
|
||||
UniqueHandle<EVP_MD_CTX> mdCtx(EVP_MD_CTX_new());
|
||||
if (mdCtx)
|
||||
{
|
||||
EVP_PKEY_CTX* signCtx = nullptr;
|
||||
if ((EVP_DigestSignInit(
|
||||
mdCtx, &signCtx, EVP_sha256(), nullptr, static_cast<EVP_PKEY*>(m_pkey))
|
||||
mdCtx.get(),
|
||||
&signCtx,
|
||||
EVP_sha256(),
|
||||
nullptr,
|
||||
static_cast<EVP_PKEY*>(m_pkey.get()))
|
||||
== 1)
|
||||
&& (EVP_PKEY_CTX_set_rsa_padding(signCtx, RSA_PKCS1_PADDING) == 1))
|
||||
{
|
||||
size_t sigLen = 0;
|
||||
if (EVP_DigestSign(mdCtx, nullptr, &sigLen, nullptr, 0) == 1)
|
||||
if (EVP_DigestSign(mdCtx.get(), nullptr, &sigLen, nullptr, 0) == 1)
|
||||
{
|
||||
const auto bufToSign = reinterpret_cast<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)
|
||||
if (EVP_DigestSign(mdCtx.get(), sigVec.data(), &sigLen, bufToSign, bufToSignLen)
|
||||
== 1)
|
||||
{
|
||||
signature = Base64Url::Base64UrlEncode(ToUInt8Vector(sigVec));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signature.empty())
|
||||
{
|
||||
throw Azure::Core::Credentials::AuthenticationException(
|
||||
"Failed to sign token request.");
|
||||
}
|
||||
|
||||
EVP_MD_CTX_free(mdCtx);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
EVP_MD_CTX_free(mdCtx);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (signature.empty())
|
||||
{
|
||||
throw Azure::Core::Credentials::AuthenticationException("Failed to sign token request.");
|
||||
}
|
||||
|
||||
// Add signature to the end of assertion
|
||||
assertion += std::string(".") + signature;
|
||||
}
|
||||
|
||||
body << "&client_assertion=" << Azure::Core::Url::Encode(assertion);
|
||||
body += "&client_assertion=" + Azure::Core::Url::Encode(assertion);
|
||||
|
||||
auto request = std::make_unique<TokenCredentialImpl::TokenRequest>(
|
||||
HttpMethod::Post, m_requestUrl, body.str());
|
||||
auto request
|
||||
= std::make_unique<TokenCredentialImpl::TokenRequest>(HttpMethod::Post, requestUrl, body);
|
||||
|
||||
request->HttpRequest.SetHeader("Host", requestUrl.GetHost());
|
||||
|
||||
return request;
|
||||
});
|
||||
|
||||
38
sdk/identity/azure-identity/src/client_credential_core.cpp
Normal file
38
sdk/identity/azure-identity/src/client_credential_core.cpp
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "azure/identity/detail/client_credential_core.hpp"
|
||||
|
||||
#include "private/token_credential_impl.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
using Azure::Identity::_detail::ClientCredentialCore;
|
||||
|
||||
using Azure::Core::Url;
|
||||
using Azure::Core::Credentials::TokenRequestContext;
|
||||
using Azure::Identity::_detail::TokenCredentialImpl;
|
||||
|
||||
decltype(ClientCredentialCore::AadGlobalAuthority) ClientCredentialCore::AadGlobalAuthority
|
||||
= "https://login.microsoftonline.com/";
|
||||
|
||||
ClientCredentialCore::ClientCredentialCore(std::string tenantId, std::string const& authorityHost)
|
||||
: m_authorityHost(Url(authorityHost)), m_tenantId(std::move(tenantId))
|
||||
{
|
||||
m_isAdfs = m_tenantId == "adfs";
|
||||
}
|
||||
|
||||
Url ClientCredentialCore::GetRequestUrl() const
|
||||
{
|
||||
auto requestUrl = m_authorityHost;
|
||||
requestUrl.AppendPath(m_tenantId);
|
||||
requestUrl.AppendPath(m_isAdfs ? "oauth2/token" : "oauth2/v2.0/token");
|
||||
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
std::string ClientCredentialCore::GetScopesString(decltype(TokenRequestContext::Scopes)
|
||||
const& scopes) const
|
||||
{
|
||||
return scopes.empty() ? std::string() : TokenCredentialImpl::FormatScopes(scopes, m_isAdfs);
|
||||
}
|
||||
@ -5,42 +5,33 @@
|
||||
|
||||
#include "private/token_credential_impl.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
using Azure::Identity::ClientSecretCredential;
|
||||
|
||||
using namespace Azure::Identity;
|
||||
|
||||
std::string const Azure::Identity::_detail::g_aadGlobalAuthority
|
||||
= "https://login.microsoftonline.com/";
|
||||
using Azure::Core::Context;
|
||||
using Azure::Core::Url;
|
||||
using Azure::Core::Credentials::AccessToken;
|
||||
using Azure::Core::Credentials::TokenCredentialOptions;
|
||||
using Azure::Core::Credentials::TokenRequestContext;
|
||||
using Azure::Core::Http::HttpMethod;
|
||||
using Azure::Identity::_detail::TokenCredentialImpl;
|
||||
|
||||
ClientSecretCredential::ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
std::string authorityHost,
|
||||
Azure::Core::Credentials::TokenCredentialOptions const& options)
|
||||
: m_tokenCredentialImpl(std::make_unique<_detail::TokenCredentialImpl>(options)),
|
||||
m_tenantId(std::move(tenantId)), m_clientId(std::move(clientId)),
|
||||
m_authorityHost(std::move(authorityHost))
|
||||
std::string const& authorityHost,
|
||||
TokenCredentialOptions const& options)
|
||||
: m_clientCredentialCore(tenantId, authorityHost),
|
||||
m_tokenCredentialImpl(std::make_unique<TokenCredentialImpl>(options)),
|
||||
m_requestBody(
|
||||
std::string("grant_type=client_credentials&client_id=") + Url::Encode(clientId)
|
||||
+ "&client_secret=" + Url::Encode(clientSecret))
|
||||
{
|
||||
using Azure::Core::Url;
|
||||
|
||||
m_isAdfs = (m_tenantId == "adfs");
|
||||
|
||||
m_requestUrl = Url(m_authorityHost);
|
||||
m_requestUrl.AppendPath(m_tenantId);
|
||||
m_requestUrl.AppendPath(m_isAdfs ? "oauth2/token" : "oauth2/v2.0/token");
|
||||
|
||||
std::ostringstream body;
|
||||
body << "grant_type=client_credentials&client_id=" << Url::Encode(m_clientId)
|
||||
<< "&client_secret=" << Url::Encode(clientSecret);
|
||||
|
||||
m_requestBody = body.str();
|
||||
}
|
||||
|
||||
ClientSecretCredential::ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
ClientSecretCredentialOptions const& options)
|
||||
: ClientSecretCredential(tenantId, clientId, clientSecret, options.AuthorityHost, options)
|
||||
@ -49,34 +40,25 @@ ClientSecretCredential::ClientSecretCredential(
|
||||
|
||||
ClientSecretCredential::ClientSecretCredential(
|
||||
std::string tenantId,
|
||||
std::string clientId,
|
||||
std::string const& clientId,
|
||||
std::string const& clientSecret,
|
||||
Core::Credentials::TokenCredentialOptions const& options)
|
||||
: ClientSecretCredential(
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
_detail::g_aadGlobalAuthority,
|
||||
ClientSecretCredentialOptions{}.AuthorityHost,
|
||||
options)
|
||||
{
|
||||
}
|
||||
|
||||
ClientSecretCredential::~ClientSecretCredential() = default;
|
||||
|
||||
Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken(
|
||||
Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext,
|
||||
Azure::Core::Context const& context) const
|
||||
AccessToken ClientSecretCredential::GetToken(
|
||||
TokenRequestContext const& tokenRequestContext,
|
||||
Context const& context) const
|
||||
{
|
||||
using _detail::TokenCredentialImpl;
|
||||
|
||||
std::string scopesStr;
|
||||
{
|
||||
auto const& scopes = tokenRequestContext.Scopes;
|
||||
if (!scopes.empty())
|
||||
{
|
||||
scopesStr = TokenCredentialImpl::FormatScopes(scopes, m_isAdfs);
|
||||
}
|
||||
}
|
||||
auto const scopesStr = m_clientCredentialCore.GetScopesString(tokenRequestContext.Scopes);
|
||||
|
||||
// TokenCache::GetToken() and m_tokenCredentialImpl->GetToken() can only use the lambda argument
|
||||
// when they are being executed. They are not supposed to keep a reference to lambda argument to
|
||||
@ -84,20 +66,19 @@ Azure::Core::Credentials::AccessToken ClientSecretCredential::GetToken(
|
||||
// lambda might get called.
|
||||
return m_tokenCache.GetToken(scopesStr, tokenRequestContext.MinimumExpiration, [&]() {
|
||||
return m_tokenCredentialImpl->GetToken(context, [&]() {
|
||||
using Azure::Core::Http::HttpMethod;
|
||||
|
||||
std::ostringstream body;
|
||||
body << m_requestBody;
|
||||
auto body = m_requestBody;
|
||||
|
||||
if (!scopesStr.empty())
|
||||
{
|
||||
body << "&scope=" << scopesStr;
|
||||
body += "&scope=" + scopesStr;
|
||||
}
|
||||
|
||||
auto request = std::make_unique<TokenCredentialImpl::TokenRequest>(
|
||||
HttpMethod::Post, m_requestUrl, body.str());
|
||||
auto const requestUrl = m_clientCredentialCore.GetRequestUrl();
|
||||
|
||||
request->HttpRequest.SetHeader("Host", m_requestUrl.GetHost());
|
||||
auto request
|
||||
= std::make_unique<TokenCredentialImpl::TokenRequest>(HttpMethod::Post, requestUrl, body);
|
||||
|
||||
request->HttpRequest.SetHeader("Host", requestUrl.GetHost());
|
||||
|
||||
return request;
|
||||
});
|
||||
|
||||
@ -7,13 +7,17 @@
|
||||
|
||||
#include <azure/core/internal/environment.hpp>
|
||||
|
||||
using namespace Azure::Identity;
|
||||
using Azure::Identity::EnvironmentCredential;
|
||||
|
||||
EnvironmentCredential::EnvironmentCredential(
|
||||
Azure::Core::Credentials::TokenCredentialOptions options)
|
||||
using Azure::Core::Context;
|
||||
using Azure::Core::_internal::Environment;
|
||||
using Azure::Core::Credentials::AccessToken;
|
||||
using Azure::Core::Credentials::AuthenticationException;
|
||||
using Azure::Core::Credentials::TokenCredentialOptions;
|
||||
using Azure::Core::Credentials::TokenRequestContext;
|
||||
|
||||
EnvironmentCredential::EnvironmentCredential(TokenCredentialOptions options)
|
||||
{
|
||||
using Azure::Core::_internal::Environment;
|
||||
|
||||
auto tenantId = Environment::GetVariable("AZURE_TENANT_ID");
|
||||
auto clientId = Environment::GetVariable("AZURE_CLIENT_ID");
|
||||
|
||||
@ -31,7 +35,6 @@ EnvironmentCredential::EnvironmentCredential(
|
||||
{
|
||||
if (!authority.empty())
|
||||
{
|
||||
using namespace Azure::Core::Credentials;
|
||||
ClientSecretCredentialOptions clientSecretCredentialOptions;
|
||||
static_cast<TokenCredentialOptions&>(clientSecretCredentialOptions) = options;
|
||||
clientSecretCredentialOptions.AuthorityHost = authority;
|
||||
@ -53,18 +56,28 @@ EnvironmentCredential::EnvironmentCredential(
|
||||
// }
|
||||
else if (!clientCertificatePath.empty())
|
||||
{
|
||||
m_credentialImpl.reset(
|
||||
new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options));
|
||||
if (!authority.empty())
|
||||
{
|
||||
ClientCertificateCredentialOptions clientCertificateCredentialOptions;
|
||||
static_cast<TokenCredentialOptions&>(clientCertificateCredentialOptions) = options;
|
||||
clientCertificateCredentialOptions.AuthorityHost = authority;
|
||||
|
||||
m_credentialImpl.reset(new ClientCertificateCredential(
|
||||
tenantId, clientId, clientCertificatePath, clientCertificateCredentialOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_credentialImpl.reset(
|
||||
new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Azure::Core::Credentials::AccessToken EnvironmentCredential::GetToken(
|
||||
Azure::Core::Credentials::TokenRequestContext const& tokenRequestContext,
|
||||
Azure::Core::Context const& context) const
|
||||
AccessToken EnvironmentCredential::GetToken(
|
||||
TokenRequestContext const& tokenRequestContext,
|
||||
Context const& context) const
|
||||
{
|
||||
using namespace Azure::Core::Credentials;
|
||||
|
||||
if (!m_credentialImpl)
|
||||
{
|
||||
throw AuthenticationException("EnvironmentCredential authentication unavailable. "
|
||||
|
||||
@ -165,6 +165,269 @@ TEST(ClientCertificateCredential, Regular)
|
||||
EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s);
|
||||
}
|
||||
|
||||
TEST(ClientCertificateCredential, AzureStack)
|
||||
{
|
||||
TempCertFile tempCertFile;
|
||||
|
||||
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||
[](auto transport) {
|
||||
ClientCertificateCredentialOptions options;
|
||||
options.Transport.Transport = transport;
|
||||
|
||||
return std::make_unique<ClientCertificateCredential>(
|
||||
"adfs", "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/adfs/oauth2/token");
|
||||
|
||||
EXPECT_EQ(request1.AbsoluteUrl, "https://login.microsoftonline.com/adfs/oauth2/token");
|
||||
|
||||
{
|
||||
constexpr char expectedBodyStart0[] // cspell:disable
|
||||
= "grant_type=client_credentials"
|
||||
"&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
"&client_id=fedcba98-7654-3210-0123-456789abcdef"
|
||||
"&scope=https%3A%2F%2Fazure.com"
|
||||
"&client_assertion="; // cspell:enable
|
||||
|
||||
constexpr char expectedBodyStart1[] // cspell:disable
|
||||
= "grant_type=client_credentials"
|
||||
"&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
"&client_id=fedcba98-7654-3210-0123-456789abcdef"
|
||||
"&client_assertion="; // cspell:enable
|
||||
|
||||
EXPECT_GT(request0.Body.size(), (sizeof(expectedBodyStart0) - 1));
|
||||
EXPECT_GT(request1.Body.size(), (sizeof(expectedBodyStart1) - 1));
|
||||
|
||||
EXPECT_EQ(request0.Body.substr(0, (sizeof(expectedBodyStart0) - 1)), expectedBodyStart0);
|
||||
EXPECT_EQ(request1.Body.substr(0, (sizeof(expectedBodyStart1) - 1)), expectedBodyStart1);
|
||||
|
||||
EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end());
|
||||
EXPECT_GT(
|
||||
std::stoi(request0.Headers.at("Content-Length")),
|
||||
static_cast<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/adfs/oauth2/token\","
|
||||
"\"iss\":\"fedcba98-7654-3210-0123-456789abcdef\","
|
||||
"\"sub\":\"fedcba98-7654-3210-0123-456789abcdef\",\"jti\":\"";
|
||||
|
||||
EXPECT_EQ(payload0.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart);
|
||||
EXPECT_EQ(payload1.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart);
|
||||
|
||||
EXPECT_EQ(Base64Url::Base64UrlDecode(signature0).size(), 256U);
|
||||
EXPECT_EQ(Base64Url::Base64UrlDecode(signature1).size(), 256U);
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end());
|
||||
EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded");
|
||||
|
||||
EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end());
|
||||
EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded");
|
||||
|
||||
EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1");
|
||||
EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s);
|
||||
EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s);
|
||||
|
||||
EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 7200s);
|
||||
EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s);
|
||||
}
|
||||
|
||||
TEST(ClientCertificateCredential, Authority)
|
||||
{
|
||||
TempCertFile tempCertFile;
|
||||
|
||||
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||
[](auto transport) {
|
||||
ClientCertificateCredentialOptions options;
|
||||
options.AuthorityHost = "https://microsoft.com/";
|
||||
options.Transport.Transport = transport;
|
||||
|
||||
return std::make_unique<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://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token");
|
||||
|
||||
EXPECT_EQ(
|
||||
request1.AbsoluteUrl,
|
||||
"https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token");
|
||||
|
||||
{
|
||||
constexpr char expectedBodyStart0[] // cspell:disable
|
||||
= "grant_type=client_credentials"
|
||||
"&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
"&client_id=fedcba98-7654-3210-0123-456789abcdef"
|
||||
"&scope=https%3A%2F%2Fazure.com%2F.default"
|
||||
"&client_assertion="; // cspell:enable
|
||||
|
||||
constexpr char expectedBodyStart1[] // cspell:disable
|
||||
= "grant_type=client_credentials"
|
||||
"&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
"&client_id=fedcba98-7654-3210-0123-456789abcdef"
|
||||
"&client_assertion="; // cspell:enable
|
||||
|
||||
EXPECT_GT(request0.Body.size(), (sizeof(expectedBodyStart0) - 1));
|
||||
EXPECT_GT(request1.Body.size(), (sizeof(expectedBodyStart1) - 1));
|
||||
|
||||
EXPECT_EQ(request0.Body.substr(0, (sizeof(expectedBodyStart0) - 1)), expectedBodyStart0);
|
||||
EXPECT_EQ(request1.Body.substr(0, (sizeof(expectedBodyStart1) - 1)), expectedBodyStart1);
|
||||
|
||||
EXPECT_NE(request0.Headers.find("Content-Length"), request0.Headers.end());
|
||||
EXPECT_GT(
|
||||
std::stoi(request0.Headers.at("Content-Length")),
|
||||
static_cast<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://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/"
|
||||
"oauth2/v2.0/token\","
|
||||
"\"iss\":\"fedcba98-7654-3210-0123-456789abcdef\","
|
||||
"\"sub\":\"fedcba98-7654-3210-0123-456789abcdef\",\"jti\":\"";
|
||||
|
||||
EXPECT_EQ(payload0.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart);
|
||||
EXPECT_EQ(payload1.substr(0, (sizeof(ExpectedPayloadStart) - 1)), ExpectedPayloadStart);
|
||||
|
||||
EXPECT_EQ(Base64Url::Base64UrlDecode(signature0).size(), 256U);
|
||||
EXPECT_EQ(Base64Url::Base64UrlDecode(signature1).size(), 256U);
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_NE(request0.Headers.find("Content-Type"), request0.Headers.end());
|
||||
EXPECT_EQ(request0.Headers.at("Content-Type"), "application/x-www-form-urlencoded");
|
||||
|
||||
EXPECT_NE(request1.Headers.find("Content-Type"), request1.Headers.end());
|
||||
EXPECT_EQ(request1.Headers.at("Content-Type"), "application/x-www-form-urlencoded");
|
||||
|
||||
EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1");
|
||||
EXPECT_EQ(response1.AccessToken.Token, "ACCESSTOKEN2");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s);
|
||||
EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s);
|
||||
|
||||
EXPECT_GE(response1.AccessToken.ExpiresOn, response1.EarliestExpiration + 7200s);
|
||||
EXPECT_LE(response1.AccessToken.ExpiresOn, response1.LatestExpiration + 7200s);
|
||||
}
|
||||
|
||||
namespace {
|
||||
const char* const TempCertFile::Path = "azure-identity-test.pem";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user