Skip to content

Commit 1802c57

Browse files
Add ClientAssertionCredential (#2266)
* refactor workload identity central logic into client assertion credential * add example to the readme * Remove locale from link * hopefully fix readme * fixing readme hopefully final --------- Co-authored-by: Heath Stewart <[email protected]>
1 parent 1c090fc commit 1802c57

File tree

4 files changed

+246
-77
lines changed

4 files changed

+246
-77
lines changed

sdk/identity/azure_identity/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,62 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
6969
Ok(())
7070
}
7171
```
72+
### Authenticate with `ClientAssertionCredential`
73+
74+
This example demonstrates how to use the `ClientAssertionCredential` in conjunction with `VirtualMachineManagedIdentityCredential` in order to retrieve an access token as an app registration
75+
that a virtual machine identity has been federated for, which can be used in "service to service"
76+
authentication flows. For more details on this scenario see [Configure an application to trust a managed identity](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center)
77+
78+
```rust no_run
79+
use azure_core::credentials::{AccessToken, TokenCredential};
80+
use azure_identity::{ClientAssertion, ClientAssertionCredential, ImdsId, TokenCredentialOptions, VirtualMachineManagedIdentityCredential};
81+
use std::sync::Arc;
82+
83+
#[derive(Debug)]
84+
struct VmClientAssertion {
85+
credential: Arc<dyn TokenCredential>,
86+
scope: String,
87+
}
88+
89+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
90+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
91+
impl ClientAssertion for VmClientAssertion {
92+
async fn secret(&self) -> azure_core::Result<String> {
93+
Ok(self
94+
.credential
95+
.get_token(&[&self.scope])
96+
.await?
97+
.token
98+
.secret()
99+
.to_string())
100+
}
101+
}
102+
103+
#[tokio::main]
104+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
105+
let assertion = VmClientAssertion {
106+
credential: VirtualMachineManagedIdentityCredential::new(
107+
ImdsId::SystemAssigned,
108+
TokenCredentialOptions::default(),
109+
)?,
110+
scope: String::from("api://AzureADTokenExchange/.default"),
111+
};
112+
113+
let client_assertion_credential = ClientAssertionCredential::new(
114+
azure_core::new_http_client(),
115+
azure_core::Url::parse("https://login.microsoftonline.com")?,
116+
String::from("guid-for-aad-tenant-id"),
117+
String::from("guid-for-app-id-of-client-app-registration"),
118+
assertion,
119+
)?;
120+
121+
let fic_scope = String::from("your-service-app.com/scope");
122+
let fic_token = client_assertion_credential.get_token(&[&fic_scope]).await?;
123+
Ok(())
124+
}
125+
126+
```
127+
72128

73129
## Credential classes
74130

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
use crate::{credentials::cache::TokenCache, federated_credentials_flow, TokenCredentialOptions};
5+
use azure_core::{
6+
credentials::{AccessToken, TokenCredential},
7+
error::{ErrorKind, ResultExt},
8+
HttpClient, Url,
9+
};
10+
use std::{fmt::Debug, str, sync::Arc, time::Duration};
11+
use time::OffsetDateTime;
12+
13+
const AZURE_TENANT_ID_ENV_KEY: &str = "AZURE_TENANT_ID";
14+
const AZURE_CLIENT_ID_ENV_KEY: &str = "AZURE_CLIENT_ID";
15+
16+
/// Enables authentication of a Microsoft Entra service principal using a signed client assertion.
17+
#[derive(Debug)]
18+
pub struct ClientAssertionCredential<C> {
19+
http_client: Arc<dyn HttpClient>,
20+
authority_host: Url,
21+
tenant_id: String,
22+
client_id: String,
23+
assertion: C,
24+
cache: TokenCache,
25+
}
26+
27+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
28+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
29+
/// Represents an entity capable of supplying a client assertion.
30+
pub trait ClientAssertion: Send + Sync + Debug {
31+
/// Supply the client assertion secret.
32+
async fn secret(&self) -> azure_core::Result<String>;
33+
}
34+
35+
impl<C: ClientAssertion> ClientAssertionCredential<C> {
36+
/// Create a new `ClientAssertionCredential`.
37+
pub fn new(
38+
http_client: Arc<dyn HttpClient>,
39+
authority_host: Url,
40+
tenant_id: String,
41+
client_id: String,
42+
assertion: C,
43+
) -> azure_core::Result<Arc<Self>> {
44+
Ok(Arc::new(Self::new_exclusive(
45+
http_client,
46+
authority_host,
47+
tenant_id,
48+
client_id,
49+
assertion,
50+
)?))
51+
}
52+
53+
/// Create a new `ClientAssertionCredential` without wrapping it in an
54+
/// `Arc`. Intended for use by other credentials in the crate that will
55+
/// themselves be protected by an `Arc`.
56+
pub(crate) fn new_exclusive(
57+
http_client: Arc<dyn HttpClient>,
58+
authority_host: Url,
59+
tenant_id: String,
60+
client_id: String,
61+
assertion: C,
62+
) -> azure_core::Result<Self> {
63+
Ok(Self {
64+
http_client,
65+
authority_host,
66+
tenant_id,
67+
client_id,
68+
assertion,
69+
cache: TokenCache::new(),
70+
})
71+
}
72+
73+
/// Create a new `ClientAssertionCredential` from environment variables.
74+
///
75+
/// # Variables
76+
///
77+
/// * `AZURE_TENANT_ID`
78+
/// * `AZURE_CLIENT_ID`
79+
pub fn from_env(
80+
options: impl Into<TokenCredentialOptions>,
81+
assertion: C,
82+
) -> azure_core::Result<Arc<Self>> {
83+
Ok(Arc::new(Self::from_env_exclusive(options, assertion)?))
84+
}
85+
86+
/// Create a new `ClientAssertionCredential` from environment variables,
87+
/// without wrapping it in an `Arc`. Intended for use by other credentials
88+
/// in the crate that will themselves be protected by an `Arc`.
89+
///
90+
/// # Variables
91+
///
92+
/// * `AZURE_TENANT_ID`
93+
/// * `AZURE_CLIENT_ID`
94+
pub(crate) fn from_env_exclusive(
95+
options: impl Into<TokenCredentialOptions>,
96+
assertion: C,
97+
) -> azure_core::Result<Self> {
98+
let options = options.into();
99+
let http_client = options.http_client();
100+
let authority_host = options.authority_host()?;
101+
let env = options.env();
102+
let tenant_id =
103+
env.var(AZURE_TENANT_ID_ENV_KEY)
104+
.with_context(ErrorKind::Credential, || {
105+
format!(
106+
"working identity credential requires {} environment variable",
107+
AZURE_TENANT_ID_ENV_KEY
108+
)
109+
})?;
110+
let client_id =
111+
env.var(AZURE_CLIENT_ID_ENV_KEY)
112+
.with_context(ErrorKind::Credential, || {
113+
format!(
114+
"working identity credential requires {} environment variable",
115+
AZURE_CLIENT_ID_ENV_KEY
116+
)
117+
})?;
118+
119+
ClientAssertionCredential::new_exclusive(
120+
http_client,
121+
authority_host,
122+
tenant_id,
123+
client_id,
124+
assertion,
125+
)
126+
}
127+
128+
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
129+
let token = self.assertion.secret().await?;
130+
let res: AccessToken = federated_credentials_flow::authorize(
131+
self.http_client.clone(),
132+
&self.client_id,
133+
&token,
134+
scopes,
135+
&self.tenant_id,
136+
&self.authority_host,
137+
)
138+
.await
139+
.map(|r| {
140+
AccessToken::new(
141+
r.access_token().clone(),
142+
OffsetDateTime::now_utc() + Duration::from_secs(r.expires_in),
143+
)
144+
})
145+
.context(ErrorKind::Credential, "request token error")?;
146+
Ok(res)
147+
}
148+
}
149+
150+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
151+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
152+
impl<C: ClientAssertion> TokenCredential for ClientAssertionCredential<C> {
153+
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
154+
self.cache.get_token(scopes, self.get_token(scopes)).await
155+
}
156+
157+
async fn clear_cache(&self) -> azure_core::Result<()> {
158+
self.cache.clear().await
159+
}
160+
}

sdk/identity/azure_identity/src/credentials/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod app_service_managed_identity_credential;
1212
#[cfg(not(target_arch = "wasm32"))]
1313
mod azure_cli_credentials;
1414
mod cache;
15+
mod client_assertion_credentials;
1516
#[cfg(feature = "client_certificate")]
1617
mod client_certificate_credentials;
1718
mod default_credentials;
@@ -23,6 +24,7 @@ mod workload_identity_credentials;
2324
pub use app_service_managed_identity_credential::*;
2425
#[cfg(not(target_arch = "wasm32"))]
2526
pub use azure_cli_credentials::*;
27+
pub use client_assertion_credentials::*;
2628
#[cfg(feature = "client_certificate")]
2729
pub use client_certificate_credentials::*;
2830
pub use default_credentials::*;

0 commit comments

Comments
 (0)