Skip to content

Commit 7e9906f

Browse files
authored
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.
1 parent e47e316 commit 7e9906f

8 files changed

Lines changed: 678 additions & 0 deletions

File tree

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Added `ClientAssertionCredential` to enable applications to authenticate with custom client assertions.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/identity/azure-identity/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ set(
5050
inc/azure/identity/azure_cli_credential.hpp
5151
inc/azure/identity/azure_pipelines_credential.hpp
5252
inc/azure/identity/chained_token_credential.hpp
53+
inc/azure/identity/client_assertion_credential.hpp
5354
inc/azure/identity/client_certificate_credential.hpp
5455
inc/azure/identity/client_secret_credential.hpp
5556
inc/azure/identity/default_azure_credential.hpp
@@ -67,6 +68,7 @@ set(
6768
src/azure_cli_credential.cpp
6869
src/azure_pipelines_credential.cpp
6970
src/chained_token_credential.cpp
71+
src/client_assertion_credential.cpp
7072
src/client_certificate_credential.cpp
7173
src/client_credential_core.cpp
7274
src/client_secret_credential.cpp

sdk/identity/azure-identity/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Configuration is attempted in the above order. For example, if values for a clie
135135
|Credential | Usage
136136
|-|-
137137
|`AzurePipelinesCredential`|Supports [Microsoft Entra Workload ID](https://learn.microsoft.com/azure/devops/pipelines/release/configure-workload-identity?view=azure-devops) on Azure Pipelines.
138+
|`ClientAssertionCredential`|Authenticates a service principal using a signed client assertion.
138139
|`ClientSecretCredential`|Authenticates a service principal [using a secret](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
139140
|`ClientCertificateCredential`|Authenticates a service principal [using a certificate](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals).
140141

sdk/identity/azure-identity/inc/azure/identity.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "azure/identity/azure_cli_credential.hpp"
1212
#include "azure/identity/azure_pipelines_credential.hpp"
1313
#include "azure/identity/chained_token_credential.hpp"
14+
#include "azure/identity/client_assertion_credential.hpp"
1415
#include "azure/identity/client_certificate_credential.hpp"
1516
#include "azure/identity/client_secret_credential.hpp"
1617
#include "azure/identity/default_azure_credential.hpp"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @file
6+
* @brief Client Assertion Credential and options.
7+
*/
8+
9+
#pragma once
10+
11+
#include "azure/identity/detail/client_credential_core.hpp"
12+
#include "azure/identity/detail/token_cache.hpp"
13+
14+
#include <azure/core/credentials/token_credential_options.hpp>
15+
#include <azure/core/http/http.hpp>
16+
17+
#include <string>
18+
#include <vector>
19+
20+
namespace Azure { namespace Identity {
21+
namespace _detail {
22+
class TokenCredentialImpl;
23+
} // namespace _detail
24+
25+
/**
26+
* @brief Options used to configure the Client Assertion credential.
27+
*
28+
*/
29+
struct ClientAssertionCredentialOptions final : public Core::Credentials::TokenCredentialOptions
30+
{
31+
/**
32+
* @brief Authentication authority URL.
33+
* @note Defaults to the value of the environment variable 'AZURE_AUTHORITY_HOST'. If that's not
34+
* set, the default value is Microsoft Entra global authority
35+
* (https://login.microsoftonline.com/).
36+
*
37+
* @note Example of an authority host string: "https://login.microsoftonline.us/". See national
38+
* clouds' Microsoft Entra authentication endpoints:
39+
* https://learn.microsoft.com/entra/identity-platform/authentication-national-cloud.
40+
*/
41+
std::string AuthorityHost = _detail::DefaultOptionValues::GetAuthorityHost();
42+
43+
/**
44+
* @brief For multi-tenant applications, specifies additional tenants for which the credential
45+
* may acquire tokens. Add the wildcard value `"*"` to allow the credential to acquire tokens
46+
* for any tenant in which the application is installed.
47+
*/
48+
std::vector<std::string> AdditionallyAllowedTenants;
49+
};
50+
51+
/**
52+
* @brief Credential which authenticates a Microsoft Entra service principal using a signed client
53+
* assertion.
54+
*
55+
*/
56+
class ClientAssertionCredential final : public Core::Credentials::TokenCredential {
57+
private:
58+
std::function<std::string(Core::Context const&)> m_assertionCallback;
59+
_detail::ClientCredentialCore m_clientCredentialCore;
60+
std::unique_ptr<_detail::TokenCredentialImpl> m_tokenCredentialImpl;
61+
std::string m_requestBody;
62+
_detail::TokenCache m_tokenCache;
63+
64+
public:
65+
/**
66+
* @brief Creates an instance of the Client Assertion Credential with a callback that provides a
67+
* signed client assertion to authenticate against Microsoft Entra ID.
68+
*
69+
* @param tenantId The Microsoft Entra tenant (directory) ID of the service principal.
70+
* @param clientId The client (application) ID of the service principal.
71+
* @param assertionCallback A callback returning a valid client assertion used to authenticate
72+
* the service principal.
73+
* @param options Options that allow to configure the management of the requests sent to
74+
* Microsoft Entra ID for token retrieval.
75+
*/
76+
explicit ClientAssertionCredential(
77+
std::string tenantId,
78+
std::string clientId,
79+
std::function<std::string(Core::Context const&)> assertionCallback,
80+
ClientAssertionCredentialOptions const& options = {});
81+
82+
/**
83+
* @brief Destructs `%ClientAssertionCredential`.
84+
*
85+
*/
86+
~ClientAssertionCredential() override;
87+
88+
/**
89+
* @brief Obtains an authentication token from Microsoft Entra ID, by calling the
90+
* assertionCallback specified when constructing the credential to obtain a client assertion for
91+
* authentication.
92+
*
93+
* @param tokenRequestContext A context to get the token in.
94+
* @param context A context to control the request lifetime.
95+
*
96+
* @throw Azure::Core::Credentials::AuthenticationException Authentication error occurred.
97+
*/
98+
Core::Credentials::AccessToken GetToken(
99+
Core::Credentials::TokenRequestContext const& tokenRequestContext,
100+
Core::Context const& context) const override;
101+
};
102+
103+
}} // namespace Azure::Identity
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#include "azure/identity/client_assertion_credential.hpp"
5+
6+
#include "private/identity_log.hpp"
7+
#include "private/package_version.hpp"
8+
#include "private/tenant_id_resolver.hpp"
9+
#include "private/token_credential_impl.hpp"
10+
11+
#include <azure/core/internal/json/json.hpp>
12+
13+
using Azure::Identity::ClientAssertionCredential;
14+
using Azure::Identity::ClientAssertionCredentialOptions;
15+
16+
using Azure::Core::Context;
17+
using Azure::Core::Url;
18+
using Azure::Core::_internal::StringExtensions;
19+
using Azure::Core::Credentials::AccessToken;
20+
using Azure::Core::Credentials::AuthenticationException;
21+
using Azure::Core::Credentials::TokenRequestContext;
22+
using Azure::Core::Http::HttpMethod;
23+
using Azure::Identity::_detail::IdentityLog;
24+
using Azure::Identity::_detail::TenantIdResolver;
25+
using Azure::Identity::_detail::TokenCredentialImpl;
26+
27+
namespace {
28+
bool IsValidTenantId(std::string const& tenantId)
29+
{
30+
const std::string allowedChars = ".-";
31+
if (tenantId.empty())
32+
{
33+
return false;
34+
}
35+
for (auto const c : tenantId)
36+
{
37+
if (allowedChars.find(c) != std::string::npos)
38+
{
39+
continue;
40+
}
41+
if (!StringExtensions::IsAlphaNumeric(c))
42+
{
43+
return false;
44+
}
45+
}
46+
return true;
47+
}
48+
} // namespace
49+
50+
ClientAssertionCredential::ClientAssertionCredential(
51+
std::string tenantId,
52+
std::string clientId,
53+
std::function<std::string(Context const&)> assertionCallback,
54+
ClientAssertionCredentialOptions const& options)
55+
: TokenCredential("ClientAssertionCredential"),
56+
m_assertionCallback(std::move(assertionCallback)),
57+
m_clientCredentialCore(tenantId, options.AuthorityHost, options.AdditionallyAllowedTenants)
58+
{
59+
bool isTenantIdValid = IsValidTenantId(tenantId);
60+
if (!isTenantIdValid)
61+
{
62+
IdentityLog::Write(
63+
IdentityLog::Level::Warning,
64+
GetCredentialName()
65+
+ ": Invalid tenant ID provided. The tenant ID must be a non-empty string containing "
66+
"only alphanumeric characters, periods, or hyphens. You can locate your tenant ID by "
67+
"following the instructions listed here: "
68+
"https://learn.microsoft.com/partner-center/find-ids-and-domain-names");
69+
}
70+
if (clientId.empty())
71+
{
72+
IdentityLog::Write(
73+
IdentityLog::Level::Warning, GetCredentialName() + ": No client ID specified.");
74+
}
75+
if (!m_assertionCallback)
76+
{
77+
IdentityLog::Write(
78+
IdentityLog::Level::Warning,
79+
GetCredentialName()
80+
+ ": The assertionCallback must be a valid function that returns assertions.");
81+
}
82+
83+
if (isTenantIdValid && !clientId.empty() && m_assertionCallback)
84+
{
85+
m_tokenCredentialImpl = std::make_unique<TokenCredentialImpl>(options);
86+
m_requestBody
87+
= std::string(
88+
"grant_type=client_credentials"
89+
"&client_assertion_type="
90+
"urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer" // cspell:disable-line
91+
"&client_id=")
92+
+ Url::Encode(clientId);
93+
94+
IdentityLog::Write(
95+
IdentityLog::Level::Informational, GetCredentialName() + " was created successfully.");
96+
}
97+
else
98+
{
99+
// Rather than throwing an exception in the ctor, following the pattern in existing credentials
100+
// to log the errors, and defer throwing an exception to the first call of GetToken(). This is
101+
// primarily needed for credentials that are part of the DefaultAzureCredential, which this
102+
// credential is not intended for.
103+
IdentityLog::Write(
104+
IdentityLog::Level::Warning, GetCredentialName() + " was not initialized correctly.");
105+
}
106+
}
107+
108+
ClientAssertionCredential::~ClientAssertionCredential() = default;
109+
110+
AccessToken ClientAssertionCredential::GetToken(
111+
TokenRequestContext const& tokenRequestContext,
112+
Context const& context) const
113+
{
114+
if (!m_tokenCredentialImpl)
115+
{
116+
auto const AuthUnavailable = GetCredentialName() + " authentication unavailable. ";
117+
118+
IdentityLog::Write(
119+
IdentityLog::Level::Warning,
120+
AuthUnavailable + "See earlier " + GetCredentialName() + " log messages for details.");
121+
122+
throw AuthenticationException(AuthUnavailable);
123+
}
124+
125+
auto const tenantId = TenantIdResolver::Resolve(
126+
m_clientCredentialCore.GetTenantId(),
127+
tokenRequestContext,
128+
m_clientCredentialCore.GetAdditionallyAllowedTenants());
129+
130+
auto const scopesStr
131+
= m_clientCredentialCore.GetScopesString(tenantId, tokenRequestContext.Scopes);
132+
133+
// TokenCache::GetToken() and m_tokenCredentialImpl->GetToken() can only use the lambda
134+
// argument when they are being executed. They are not supposed to keep a reference to lambda
135+
// argument to call it later. Therefore, any capture made here will outlive the possible time
136+
// frame when the lambda might get called.
137+
return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() {
138+
return m_tokenCredentialImpl->GetToken(context, false, [&]() {
139+
auto body = m_requestBody;
140+
if (!scopesStr.empty())
141+
{
142+
body += "&scope=" + scopesStr;
143+
}
144+
145+
// Get the request url before calling m_assertionCallback to validate the authority host
146+
// scheme (GetRequestUrl() will throw if validation fails). This is to avoid calling the
147+
// assertion callback if the authority host scheme is invalid.
148+
auto const requestUrl = m_clientCredentialCore.GetRequestUrl(tenantId);
149+
150+
const std::string assertion = m_assertionCallback(context);
151+
152+
body += "&client_assertion=" + Azure::Core::Url::Encode(assertion);
153+
154+
auto request
155+
= std::make_unique<TokenCredentialImpl::TokenRequest>(HttpMethod::Post, requestUrl, body);
156+
157+
request->HttpRequest.SetHeader("Host", requestUrl.GetHost());
158+
159+
return request;
160+
});
161+
});
162+
}

sdk/identity/azure-identity/test/ut/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_executable (
1919
azure_cli_credential_test.cpp
2020
azure_pipelines_credential_test.cpp
2121
chained_token_credential_test.cpp
22+
client_assertion_credential_test.cpp
2223
client_certificate_credential_test.cpp
2324
client_secret_credential_test.cpp
2425
credential_test_helper.cpp

0 commit comments

Comments
 (0)