Added `ClientAssertionCredential` to enable applications to authenticate with custom client assertions. (#5789)
* Added `ClientAssertionCredential` to enable applications to authenticate with custom client assertions. * Rename test file. * Update client assertion credential tests. * Fix typo. * Address PR feedback - pass in function by value and some comment fixup. * Update log messages to use credential name as a prefix.
This commit is contained in:
Родитель
e47e3168fe
Коммит
7e9906f884
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
### Features Added
|
### Features Added
|
||||||
|
|
||||||
|
- Added `ClientAssertionCredential` to enable applications to authenticate with custom client assertions.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
### Bugs Fixed
|
### Bugs Fixed
|
||||||
|
|
|
@ -50,6 +50,7 @@ set(
|
||||||
inc/azure/identity/azure_cli_credential.hpp
|
inc/azure/identity/azure_cli_credential.hpp
|
||||||
inc/azure/identity/azure_pipelines_credential.hpp
|
inc/azure/identity/azure_pipelines_credential.hpp
|
||||||
inc/azure/identity/chained_token_credential.hpp
|
inc/azure/identity/chained_token_credential.hpp
|
||||||
|
inc/azure/identity/client_assertion_credential.hpp
|
||||||
inc/azure/identity/client_certificate_credential.hpp
|
inc/azure/identity/client_certificate_credential.hpp
|
||||||
inc/azure/identity/client_secret_credential.hpp
|
inc/azure/identity/client_secret_credential.hpp
|
||||||
inc/azure/identity/default_azure_credential.hpp
|
inc/azure/identity/default_azure_credential.hpp
|
||||||
|
@ -67,6 +68,7 @@ set(
|
||||||
src/azure_cli_credential.cpp
|
src/azure_cli_credential.cpp
|
||||||
src/azure_pipelines_credential.cpp
|
src/azure_pipelines_credential.cpp
|
||||||
src/chained_token_credential.cpp
|
src/chained_token_credential.cpp
|
||||||
|
src/client_assertion_credential.cpp
|
||||||
src/client_certificate_credential.cpp
|
src/client_certificate_credential.cpp
|
||||||
src/client_credential_core.cpp
|
src/client_credential_core.cpp
|
||||||
src/client_secret_credential.cpp
|
src/client_secret_credential.cpp
|
||||||
|
|
|
@ -135,6 +135,7 @@ Configuration is attempted in the above order. For example, if values for a clie
|
||||||
|Credential | Usage
|
|Credential | Usage
|
||||||
|-|-
|
|-|-
|
||||||
|`AzurePipelinesCredential`|Supports [Microsoft Entra Workload ID](https://learn.microsoft.com/azure/devops/pipelines/release/configure-workload-identity?view=azure-devops) on Azure Pipelines.
|
|`AzurePipelinesCredential`|Supports [Microsoft Entra Workload ID](https://learn.microsoft.com/azure/devops/pipelines/release/configure-workload-identity?view=azure-devops) on Azure Pipelines.
|
||||||
|
|`ClientAssertionCredential`|Authenticates a service principal using a signed client assertion.
|
||||||
|`ClientSecretCredential`|Authenticates a service principal [using a secret](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
|
|`ClientSecretCredential`|Authenticates a service principal [using a secret](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
|
||||||
|`ClientCertificateCredential`|Authenticates a service principal [using a certificate](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
|
|`ClientCertificateCredential`|Authenticates a service principal [using a certificate](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "azure/identity/azure_cli_credential.hpp"
|
#include "azure/identity/azure_cli_credential.hpp"
|
||||||
#include "azure/identity/azure_pipelines_credential.hpp"
|
#include "azure/identity/azure_pipelines_credential.hpp"
|
||||||
#include "azure/identity/chained_token_credential.hpp"
|
#include "azure/identity/chained_token_credential.hpp"
|
||||||
|
#include "azure/identity/client_assertion_credential.hpp"
|
||||||
#include "azure/identity/client_certificate_credential.hpp"
|
#include "azure/identity/client_certificate_credential.hpp"
|
||||||
#include "azure/identity/client_secret_credential.hpp"
|
#include "azure/identity/client_secret_credential.hpp"
|
||||||
#include "azure/identity/default_azure_credential.hpp"
|
#include "azure/identity/default_azure_credential.hpp"
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* @brief Client Assertion Credential and options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "azure/identity/detail/client_credential_core.hpp"
|
||||||
|
#include "azure/identity/detail/token_cache.hpp"
|
||||||
|
|
||||||
|
#include <azure/core/credentials/token_credential_options.hpp>
|
||||||
|
#include <azure/core/http/http.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Azure { namespace Identity {
|
||||||
|
namespace _detail {
|
||||||
|
class TokenCredentialImpl;
|
||||||
|
} // namespace _detail
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Options used to configure the Client Assertion credential.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
struct ClientAssertionCredentialOptions final : public Core::Credentials::TokenCredentialOptions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Authentication authority URL.
|
||||||
|
* @note Defaults to the value of the environment variable 'AZURE_AUTHORITY_HOST'. If that's not
|
||||||
|
* set, the default value is Microsoft Entra global authority
|
||||||
|
* (https://login.microsoftonline.com/).
|
||||||
|
*
|
||||||
|
* @note Example of an authority host string: "https://login.microsoftonline.us/". See national
|
||||||
|
* clouds' Microsoft Entra authentication endpoints:
|
||||||
|
* https://learn.microsoft.com/entra/identity-platform/authentication-national-cloud.
|
||||||
|
*/
|
||||||
|
std::string AuthorityHost = _detail::DefaultOptionValues::GetAuthorityHost();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief For multi-tenant applications, specifies additional tenants for which the credential
|
||||||
|
* may acquire tokens. Add the wildcard value `"*"` to allow the credential to acquire tokens
|
||||||
|
* for any tenant in which the application is installed.
|
||||||
|
*/
|
||||||
|
std::vector<std::string> AdditionallyAllowedTenants;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Credential which authenticates a Microsoft Entra service principal using a signed client
|
||||||
|
* assertion.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class ClientAssertionCredential final : public Core::Credentials::TokenCredential {
|
||||||
|
private:
|
||||||
|
std::function<std::string(Core::Context const&)> m_assertionCallback;
|
||||||
|
_detail::ClientCredentialCore m_clientCredentialCore;
|
||||||
|
std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl;
|
||||||
|
std::string m_requestBody;
|
||||||
|
_detail::TokenCache m_tokenCache;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Creates an instance of the Client Assertion Credential with a callback that provides a
|
||||||
|
* signed client assertion to authenticate against Microsoft Entra ID.
|
||||||
|
*
|
||||||
|
* @param tenantId The Microsoft Entra tenant (directory) ID of the service principal.
|
||||||
|
* @param clientId The client (application) ID of the service principal.
|
||||||
|
* @param assertionCallback A callback returning a valid client assertion used to authenticate
|
||||||
|
* the service principal.
|
||||||
|
* @param options Options that allow to configure the management of the requests sent to
|
||||||
|
* Microsoft Entra ID for token retrieval.
|
||||||
|
*/
|
||||||
|
explicit ClientAssertionCredential(
|
||||||
|
std::string tenantId,
|
||||||
|
std::string clientId,
|
||||||
|
std::function<std::string(Core::Context const&)> assertionCallback,
|
||||||
|
ClientAssertionCredentialOptions const& options = {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destructs `%ClientAssertionCredential`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
~ClientAssertionCredential() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Obtains an authentication token from Microsoft Entra ID, by calling the
|
||||||
|
* assertionCallback specified when constructing the credential to obtain a client assertion for
|
||||||
|
* authentication.
|
||||||
|
*
|
||||||
|
* @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
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "azure/identity/client_assertion_credential.hpp"
|
||||||
|
|
||||||
|
#include "private/identity_log.hpp"
|
||||||
|
#include "private/package_version.hpp"
|
||||||
|
#include "private/tenant_id_resolver.hpp"
|
||||||
|
#include "private/token_credential_impl.hpp"
|
||||||
|
|
||||||
|
#include <azure/core/internal/json/json.hpp>
|
||||||
|
|
||||||
|
using Azure::Identity::ClientAssertionCredential;
|
||||||
|
using Azure::Identity::ClientAssertionCredentialOptions;
|
||||||
|
|
||||||
|
using Azure::Core::Context;
|
||||||
|
using Azure::Core::Url;
|
||||||
|
using Azure::Core::_internal::StringExtensions;
|
||||||
|
using Azure::Core::Credentials::AccessToken;
|
||||||
|
using Azure::Core::Credentials::AuthenticationException;
|
||||||
|
using Azure::Core::Credentials::TokenRequestContext;
|
||||||
|
using Azure::Core::Http::HttpMethod;
|
||||||
|
using Azure::Identity::_detail::IdentityLog;
|
||||||
|
using Azure::Identity::_detail::TenantIdResolver;
|
||||||
|
using Azure::Identity::_detail::TokenCredentialImpl;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool IsValidTenantId(std::string const& tenantId)
|
||||||
|
{
|
||||||
|
const std::string allowedChars = ".-";
|
||||||
|
if (tenantId.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (auto const c : tenantId)
|
||||||
|
{
|
||||||
|
if (allowedChars.find(c) != std::string::npos)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!StringExtensions::IsAlphaNumeric(c))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ClientAssertionCredential::ClientAssertionCredential(
|
||||||
|
std::string tenantId,
|
||||||
|
std::string clientId,
|
||||||
|
std::function<std::string(Context const&)> assertionCallback,
|
||||||
|
ClientAssertionCredentialOptions const& options)
|
||||||
|
: TokenCredential("ClientAssertionCredential"),
|
||||||
|
m_assertionCallback(std::move(assertionCallback)),
|
||||||
|
m_clientCredentialCore(tenantId, options.AuthorityHost, options.AdditionallyAllowedTenants)
|
||||||
|
{
|
||||||
|
bool isTenantIdValid = IsValidTenantId(tenantId);
|
||||||
|
if (!isTenantIdValid)
|
||||||
|
{
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Warning,
|
||||||
|
GetCredentialName()
|
||||||
|
+ ": Invalid tenant ID provided. The tenant ID must be a non-empty string containing "
|
||||||
|
"only alphanumeric characters, periods, or hyphens. You can locate your tenant ID by "
|
||||||
|
"following the instructions listed here: "
|
||||||
|
"https://learn.microsoft.com/partner-center/find-ids-and-domain-names");
|
||||||
|
}
|
||||||
|
if (clientId.empty())
|
||||||
|
{
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Warning, GetCredentialName() + ": No client ID specified.");
|
||||||
|
}
|
||||||
|
if (!m_assertionCallback)
|
||||||
|
{
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Warning,
|
||||||
|
GetCredentialName()
|
||||||
|
+ ": The assertionCallback must be a valid function that returns assertions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTenantIdValid && !clientId.empty() && m_assertionCallback)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Informational, GetCredentialName() + " was created successfully.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rather than throwing an exception in the ctor, following the pattern in existing credentials
|
||||||
|
// to log the errors, and defer throwing an exception to the first call of GetToken(). This is
|
||||||
|
// primarily needed for credentials that are part of the DefaultAzureCredential, which this
|
||||||
|
// credential is not intended for.
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Warning, GetCredentialName() + " was not initialized correctly.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientAssertionCredential::~ClientAssertionCredential() = default;
|
||||||
|
|
||||||
|
AccessToken ClientAssertionCredential::GetToken(
|
||||||
|
TokenRequestContext const& tokenRequestContext,
|
||||||
|
Context const& context) const
|
||||||
|
{
|
||||||
|
if (!m_tokenCredentialImpl)
|
||||||
|
{
|
||||||
|
auto const AuthUnavailable = GetCredentialName() + " authentication unavailable. ";
|
||||||
|
|
||||||
|
IdentityLog::Write(
|
||||||
|
IdentityLog::Level::Warning,
|
||||||
|
AuthUnavailable + "See earlier " + GetCredentialName() + " log messages for details.");
|
||||||
|
|
||||||
|
throw AuthenticationException(AuthUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const tenantId = TenantIdResolver::Resolve(
|
||||||
|
m_clientCredentialCore.GetTenantId(),
|
||||||
|
tokenRequestContext,
|
||||||
|
m_clientCredentialCore.GetAdditionallyAllowedTenants());
|
||||||
|
|
||||||
|
auto const scopesStr
|
||||||
|
= m_clientCredentialCore.GetScopesString(tenantId, 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 call it later. Therefore, any capture made here will outlive the possible time
|
||||||
|
// frame when the lambda might get called.
|
||||||
|
return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() {
|
||||||
|
return m_tokenCredentialImpl->GetToken(context, false, [&]() {
|
||||||
|
auto body = m_requestBody;
|
||||||
|
if (!scopesStr.empty())
|
||||||
|
{
|
||||||
|
body += "&scope=" + scopesStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the request url before calling m_assertionCallback to validate the authority host
|
||||||
|
// scheme (GetRequestUrl() will throw if validation fails). This is to avoid calling the
|
||||||
|
// assertion callback if the authority host scheme is invalid.
|
||||||
|
auto const requestUrl = m_clientCredentialCore.GetRequestUrl(tenantId);
|
||||||
|
|
||||||
|
const std::string assertion = m_assertionCallback(context);
|
||||||
|
|
||||||
|
body += "&client_assertion=" + Azure::Core::Url::Encode(assertion);
|
||||||
|
|
||||||
|
auto request
|
||||||
|
= std::make_unique<TokenCredentialImpl::TokenRequest>(HttpMethod::Post, requestUrl, body);
|
||||||
|
|
||||||
|
request->HttpRequest.SetHeader("Host", requestUrl.GetHost());
|
||||||
|
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ add_executable (
|
||||||
azure_cli_credential_test.cpp
|
azure_cli_credential_test.cpp
|
||||||
azure_pipelines_credential_test.cpp
|
azure_pipelines_credential_test.cpp
|
||||||
chained_token_credential_test.cpp
|
chained_token_credential_test.cpp
|
||||||
|
client_assertion_credential_test.cpp
|
||||||
client_certificate_credential_test.cpp
|
client_certificate_credential_test.cpp
|
||||||
client_secret_credential_test.cpp
|
client_secret_credential_test.cpp
|
||||||
credential_test_helper.cpp
|
credential_test_helper.cpp
|
||||||
|
|
|
@ -0,0 +1,406 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "azure/identity/client_assertion_credential.hpp"
|
||||||
|
#include "credential_test_helper.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
using Azure::Core::_internal::Environment;
|
||||||
|
using Azure::Core::Credentials::AccessToken;
|
||||||
|
using Azure::Core::Credentials::AuthenticationException;
|
||||||
|
using Azure::Core::Credentials::TokenRequestContext;
|
||||||
|
using Azure::Core::Http::HttpMethod;
|
||||||
|
using Azure::Identity::ClientAssertionCredential;
|
||||||
|
using Azure::Identity::ClientAssertionCredentialOptions;
|
||||||
|
using Azure::Identity::Test::_detail::CredentialTestHelper;
|
||||||
|
|
||||||
|
std::string GetAssertion_Throw(Azure::Core::Context const&)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
"The test is not expected to call this function used for assertion callback.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetAssertion_Test(Azure::Core::Context const&) { return "sample-assertion"; }
|
||||||
|
|
||||||
|
TEST(ClientAssertionCredential, GetCredentialName)
|
||||||
|
{
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
std::string serviceConnectionId = "abc";
|
||||||
|
std::string systemAccessToken = "123";
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred(tenantId, clientId, GetAssertion_Throw);
|
||||||
|
|
||||||
|
EXPECT_EQ(cred.GetCredentialName(), "ClientAssertionCredential");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ClientAssertionCredential, GetOptionsFromEnvironment)
|
||||||
|
{
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> envVars = {{"AZURE_AUTHORITY_HOST", ""}};
|
||||||
|
CredentialTestHelper::EnvironmentOverride const env(envVars);
|
||||||
|
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
ClientAssertionCredential const cred(tenantId, clientId, GetAssertion_Throw, options);
|
||||||
|
EXPECT_EQ(cred.GetCredentialName(), "ClientAssertionCredential");
|
||||||
|
|
||||||
|
EXPECT_EQ(options.AuthorityHost, "https://login.microsoftonline.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> envVars = {{"AZURE_AUTHORITY_HOST", "foo"}};
|
||||||
|
CredentialTestHelper::EnvironmentOverride const env(envVars);
|
||||||
|
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
options.AuthorityHost = "bar";
|
||||||
|
EXPECT_EQ(options.AuthorityHost, "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> envVars
|
||||||
|
= {{"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}};
|
||||||
|
CredentialTestHelper::EnvironmentOverride const env(envVars);
|
||||||
|
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
EXPECT_EQ(options.AuthorityHost, "https://microsoft.com/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ClientAssertionCredential, InvalidArgs)
|
||||||
|
{
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
|
||||||
|
// Empty Tenant ID
|
||||||
|
{
|
||||||
|
TokenRequestContext trc;
|
||||||
|
trc.Scopes.push_back("https://storage.azure.com/.default");
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred("", clientId, GetAssertion_Throw);
|
||||||
|
EXPECT_THROW(cred.GetToken(trc, {}), AuthenticationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid Tenant ID
|
||||||
|
{
|
||||||
|
TokenRequestContext trc;
|
||||||
|
trc.Scopes.push_back("https://storage.azure.com/.default");
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred("!=invalidTenantId=!", clientId, GetAssertion_Throw);
|
||||||
|
EXPECT_THROW(cred.GetToken(trc, {}), AuthenticationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty client ID
|
||||||
|
{
|
||||||
|
TokenRequestContext trc;
|
||||||
|
trc.Scopes.push_back("https://storage.azure.com/.default");
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred(tenantId, "", GetAssertion_Throw);
|
||||||
|
EXPECT_THROW(cred.GetToken(trc, {}), AuthenticationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty assertion callback
|
||||||
|
{
|
||||||
|
TokenRequestContext trc;
|
||||||
|
trc.Scopes.push_back("https://storage.azure.com/.default");
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred(tenantId, clientId, nullptr);
|
||||||
|
EXPECT_THROW(cred.GetToken(trc, {}), AuthenticationException);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::function<std::string(Azure::Core::Context const&)> emptyCallBack;
|
||||||
|
TokenRequestContext trc;
|
||||||
|
trc.Scopes.push_back("https://storage.azure.com/.default");
|
||||||
|
|
||||||
|
ClientAssertionCredential const cred(tenantId, clientId, emptyCallBack);
|
||||||
|
EXPECT_THROW(cred.GetToken(trc, {}), AuthenticationException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ClientAssertionCredential, Regular)
|
||||||
|
{
|
||||||
|
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||||
|
[](auto transport) {
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
options.Transport.Transport = transport;
|
||||||
|
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
|
||||||
|
return std::make_unique<ClientAssertionCredential>(
|
||||||
|
tenantId, clientId, GetAssertion_Test, 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=sample-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=sample-assertion"; // cspell:enable
|
||||||
|
|
||||||
|
EXPECT_EQ(request0.Body.size(), (sizeof(expectedBodyStart0) - 1));
|
||||||
|
EXPECT_EQ(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_EQ(
|
||||||
|
std::stoi(request0.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart0) - 1));
|
||||||
|
|
||||||
|
EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end());
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::stoi(request1.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart1) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ClientAssertionCredential, AzureStack)
|
||||||
|
{
|
||||||
|
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||||
|
[](auto transport) {
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
options.Transport.Transport = transport;
|
||||||
|
|
||||||
|
std::string tenantId = "adfs";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
|
||||||
|
return std::make_unique<ClientAssertionCredential>(
|
||||||
|
tenantId, clientId, GetAssertion_Test, 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=sample-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=sample-assertion"; // cspell:enable
|
||||||
|
|
||||||
|
EXPECT_EQ(request0.Body.size(), (sizeof(expectedBodyStart0) - 1));
|
||||||
|
EXPECT_EQ(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_EQ(
|
||||||
|
std::stoi(request0.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart0) - 1));
|
||||||
|
|
||||||
|
EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end());
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::stoi(request1.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart1) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ClientAssertionCredential, Authority)
|
||||||
|
{
|
||||||
|
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||||
|
[](auto transport) {
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
options.Transport.Transport = transport;
|
||||||
|
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
options.AuthorityHost = "https://microsoft.com/";
|
||||||
|
|
||||||
|
return std::make_unique<ClientAssertionCredential>(
|
||||||
|
tenantId, clientId, GetAssertion_Test, 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=sample-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=sample-assertion"; // cspell:enable
|
||||||
|
|
||||||
|
EXPECT_EQ(request0.Body.size(), (sizeof(expectedBodyStart0) - 1));
|
||||||
|
EXPECT_EQ(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_EQ(
|
||||||
|
std::stoi(request0.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart0) - 1));
|
||||||
|
|
||||||
|
EXPECT_NE(request1.Headers.find("Content-Length"), request1.Headers.end());
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::stoi(request1.Headers.at("Content-Length")),
|
||||||
|
static_cast<int>(sizeof(expectedBodyStart1) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ClientAssertionCredential, HttpSchemeNotSupported)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::string> envVars = {{"AZURE_AUTHORITY_HOST", "http://microsoft.com/"}};
|
||||||
|
CredentialTestHelper::EnvironmentOverride const env(envVars);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||||
|
[](auto transport) {
|
||||||
|
ClientAssertionCredentialOptions options;
|
||||||
|
options.Transport.Transport = transport;
|
||||||
|
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
|
||||||
|
return std::make_unique<ClientAssertionCredential>(
|
||||||
|
tenantId, clientId, GetAssertion_Throw, options);
|
||||||
|
},
|
||||||
|
{{{"https://azure.com/.default"}}},
|
||||||
|
std::vector<std::string>{"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"});
|
||||||
|
}
|
||||||
|
catch (AuthenticationException const& e)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(std::string(e.what()).find("https") != std::string::npos) << e.what();
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче