Skip to content

Commit d7a2989

Browse files
authored
implement federated_credential_flow and workload_identity_creds and add them to the environment_credential (#1319)
1 parent 37451bd commit d7a2989

File tree

7 files changed

+319
-6
lines changed

7 files changed

+319
-6
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use azure_identity::{authority_hosts, federated_credentials_flow};
2+
use url::Url;
3+
4+
use std::env;
5+
use std::error::Error;
6+
7+
#[tokio::main]
8+
async fn main() -> Result<(), Box<dyn Error>> {
9+
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable.");
10+
let token = env::var("FEDERATED_TOKEN").expect("Missing FEDERATED_TOKEN environment variable.");
11+
let tenant_id = env::var("TENANT_ID").expect("Missing TENANT_ID environment variable.");
12+
13+
let vault_name = std::env::args()
14+
.nth(1)
15+
.expect("please specify the vault name as first command line parameter");
16+
17+
let http_client = azure_core::new_http_client();
18+
// This will give you the final token to use in authorization.
19+
let token = federated_credentials_flow::perform(
20+
http_client.clone(),
21+
&client_id,
22+
&token,
23+
&["https://vault.azure.net/.default"],
24+
&tenant_id,
25+
authority_hosts::AZURE_PUBLIC_CLOUD.clone(),
26+
)
27+
.await
28+
.expect("federated_credentials_flow failed");
29+
println!("Non interactive authorization == {token:?}");
30+
31+
let url = Url::parse(&format!(
32+
"https://{vault_name}.vault.azure.net/secrets?api-version=7.4"
33+
))?;
34+
35+
let resp = reqwest::Client::new()
36+
.get(url)
37+
.header(
38+
"Authorization",
39+
format!("Bearer {}", token.access_token().secret()),
40+
)
41+
.send()
42+
.await?
43+
.text()
44+
.await?;
45+
46+
println!("\n\nresp {resp:?}");
47+
Ok(())
48+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use azure_core::auth::AccessToken;
2+
use serde::{de, Deserialize, Deserializer};
3+
use time::OffsetDateTime;
4+
5+
#[derive(Debug, Clone, Deserialize)]
6+
struct _LoginResponse {
7+
token_type: String,
8+
expires_in: u64,
9+
ext_expires_in: u64,
10+
expires_on: Option<String>,
11+
not_before: Option<String>,
12+
resource: Option<String>,
13+
access_token: String,
14+
}
15+
16+
#[derive(Debug, Clone)]
17+
pub struct LoginResponse {
18+
pub token_type: String,
19+
pub expires_in: u64,
20+
pub ext_expires_in: u64,
21+
pub expires_on: Option<OffsetDateTime>,
22+
pub not_before: Option<OffsetDateTime>,
23+
pub resource: Option<String>,
24+
pub access_token: AccessToken,
25+
}
26+
27+
impl<'de> Deserialize<'de> for LoginResponse {
28+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
29+
where
30+
D: Deserializer<'de>,
31+
{
32+
let resp = _LoginResponse::deserialize(deserializer)?;
33+
LoginResponse::from_base_response(resp).map_err(de::Error::custom)
34+
}
35+
}
36+
37+
impl LoginResponse {
38+
pub fn access_token(&self) -> &AccessToken {
39+
&self.access_token
40+
}
41+
42+
fn from_base_response(r: _LoginResponse) -> Result<LoginResponse, std::num::ParseIntError> {
43+
let expires_on: Option<OffsetDateTime> = r.expires_on.map(|d| {
44+
OffsetDateTime::from_unix_timestamp(d.parse::<i64>().unwrap_or(0))
45+
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
46+
});
47+
let not_before: Option<OffsetDateTime> = r.not_before.map(|d| {
48+
OffsetDateTime::from_unix_timestamp(d.parse::<i64>().unwrap_or(0))
49+
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
50+
});
51+
52+
Ok(LoginResponse {
53+
token_type: r.token_type,
54+
expires_in: r.expires_in,
55+
ext_expires_in: r.ext_expires_in,
56+
expires_on,
57+
not_before,
58+
resource: r.resource,
59+
access_token: AccessToken::new(r.access_token),
60+
})
61+
}
62+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//! Authorize using the OAuth 2.0 client credentials flow with federated credentials.
2+
//!
3+
//! ```no_run
4+
//! use azure_identity::{authority_hosts, federated_credentials_flow};
5+
//! use url::Url;
6+
//!
7+
//! use std::env;
8+
//! use std::error::Error;
9+
//!
10+
//! #[tokio::main]
11+
//! async fn main() -> Result<(), Box<dyn Error>> {
12+
//! let client_id =
13+
//! env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable.");
14+
//! let token = env::var("FEDERATED_TOKEN").expect("Missing FEDERATED_TOKEN environment variable.");
15+
//! let tenant_id = env::var("TENANT_ID").expect("Missing TENANT_ID environment variable.");
16+
//! let subscription_id =
17+
//! env::var("SUBSCRIPTION_ID").expect("Missing SUBSCRIPTION_ID environment variable.");
18+
//!
19+
//! let http_client = azure_core::new_http_client();
20+
//! // This will give you the final token to use in authorization.
21+
//! let token = federated_credentials_flow::perform(
22+
//! http_client.clone(),
23+
//! &client_id,
24+
//! &token,
25+
//! &["https://management.azure.com/"],
26+
//! &tenant_id,
27+
//! authority_hosts::AZURE_PUBLIC_CLOUD.clone(),
28+
//!
29+
//! )
30+
//! .await?;
31+
//! Ok(())
32+
//! }
33+
//! ```
34+
//!
35+
//! You can learn more about this authorization flow [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential).
36+
37+
mod login_response;
38+
39+
use azure_core::Method;
40+
use azure_core::{
41+
content_type,
42+
error::{ErrorKind, ResultExt},
43+
headers, HttpClient, Request,
44+
};
45+
use log::{debug, error};
46+
use login_response::LoginResponse;
47+
use std::sync::Arc;
48+
use url::{form_urlencoded, Url};
49+
50+
/// Perform the client credentials flow
51+
#[allow(clippy::manual_async_fn)]
52+
#[fix_hidden_lifetime_bug::fix_hidden_lifetime_bug]
53+
pub async fn perform(
54+
http_client: Arc<dyn HttpClient>,
55+
client_id: &str,
56+
client_assertion: &str,
57+
scopes: &[&str],
58+
tenant_id: &str,
59+
host: &str,
60+
) -> azure_core::Result<LoginResponse> {
61+
let encoded: String = form_urlencoded::Serializer::new(String::new())
62+
.append_pair("client_id", client_id)
63+
.append_pair("scope", &scopes.join(" "))
64+
.append_pair(
65+
"client_assertion_type",
66+
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
67+
)
68+
.append_pair("client_assertion", client_assertion)
69+
.append_pair("grant_type", "client_credentials")
70+
.finish();
71+
72+
let url = Url::parse(&format!("{host}/{tenant_id}/oauth2/v2.0/token"))
73+
.with_context(ErrorKind::DataConversion, || {
74+
format!("The supplied tenant id could not be url encoded: {tenant_id}")
75+
})?;
76+
77+
let mut req = Request::new(url, Method::Post);
78+
req.insert_header(
79+
headers::CONTENT_TYPE,
80+
content_type::APPLICATION_X_WWW_FORM_URLENCODED,
81+
);
82+
req.set_body(encoded);
83+
let rsp = http_client.execute_request(&req).await?;
84+
let rsp_status = rsp.status();
85+
debug!("rsp_status == {:?}", rsp_status);
86+
let rsp_body = rsp.into_body().collect().await?;
87+
if !rsp_status.is_success() {
88+
let text = std::str::from_utf8(&rsp_body)?;
89+
error!("rsp_body == {:?}", text);
90+
return Err(ErrorKind::http_response_from_body(rsp_status, &rsp_body).into_error());
91+
}
92+
serde_json::from_slice(&rsp_body).map_kind(ErrorKind::DataConversion)
93+
}

sdk/identity/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub mod client_credentials_flow;
4747
#[cfg(feature = "development")]
4848
pub mod development;
4949
pub mod device_code_flow;
50+
pub mod federated_credentials_flow;
5051
mod oauth2_http_client;
5152
pub mod refresh_token;
5253
mod timeout;

sdk/identity/src/token_credentials/environment_credentials.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{ClientSecretCredential, TokenCredentialOptions};
1+
use super::{ClientSecretCredential, TokenCredentialOptions, WorkloadIdentityCredential};
22
use azure_core::auth::{TokenCredential, TokenResponse};
33
use azure_core::error::{Error, ErrorKind, ResultExt};
44
use azure_core::HttpClient;
@@ -10,8 +10,13 @@ const AZURE_CLIENT_SECRET_ENV_KEY: &str = "AZURE_CLIENT_SECRET";
1010
const AZURE_USERNAME_ENV_KEY: &str = "AZURE_USERNAME";
1111
const AZURE_PASSWORD_ENV_KEY: &str = "AZURE_PASSWORD";
1212
const AZURE_CLIENT_CERTIFICATE_PATH_ENV_KEY: &str = "AZURE_CLIENT_CERTIFICATE_PATH";
13+
const AZURE_FEDERATED_TOKEN_FILE: &str = "AZURE_FEDERATED_TOKEN_FILE";
14+
const AZURE_FEDERATED_TOKEN: &str = "AZURE_FEDERATED_TOKEN";
15+
const AZURE_AUTHORITY_HOST: &str = "AZURE_AUTHORITY_HOST";
1316

14-
/// Enables authentication to Azure Active Directory using client secret, or a username and password.
17+
/// Enables authentication with Workflows Identity if either AZURE_FEDERATED_TOKEN or AZURE_FEDERATED_TOKEN_FILE is set,
18+
/// otherwise enables authentication to Azure Active Directory using client secret, or a username and password.
19+
///
1520
///
1621
/// Details configured in the following environment variables:
1722
///
@@ -20,8 +25,10 @@ const AZURE_CLIENT_CERTIFICATE_PATH_ENV_KEY: &str = "AZURE_CLIENT_CERTIFICATE_PA
2025
/// | `AZURE_TENANT_ID` | The Azure Active Directory tenant(directory) ID. |
2126
/// | `AZURE_CLIENT_ID` | The client(application) ID of an App Registration in the tenant. |
2227
/// | `AZURE_CLIENT_SECRET` | A client secret that was generated for the App Registration. |
28+
/// | `AZURE_FEDERATED_TOKEN_FILE` | Path to an federated token file. Variable is present in pods with aks workload identities. |
29+
/// | `AZURE_AUTHORITY_HOST` | Url for the identity provider to exchange to federated token for an access_token. Variable is present in pods with aks workload identities. |
2330
///
24-
/// This credential ultimately uses a `ClientSecretCredential` to perform the authentication using
31+
/// This credential ultimately uses a or `WorkloadIdentityCredential` a`ClientSecretCredential` to perform the authentication using
2532
/// these details.
2633
/// Please consult the documentation of that class for more details.
2734
#[derive(Clone, Debug)]
@@ -67,22 +74,54 @@ impl TokenCredential for EnvironmentCredential {
6774
let username = std::env::var(AZURE_USERNAME_ENV_KEY);
6875
let password = std::env::var(AZURE_PASSWORD_ENV_KEY);
6976
let client_certificate_path = std::env::var(AZURE_CLIENT_CERTIFICATE_PATH_ENV_KEY);
77+
let federated_token_file = std::env::var(AZURE_FEDERATED_TOKEN_FILE);
78+
let federated_token = std::env::var(AZURE_FEDERATED_TOKEN);
79+
let authority_host = std::env::var(AZURE_AUTHORITY_HOST);
80+
81+
let options: TokenCredentialOptions = if let Ok(authority_host) = authority_host {
82+
TokenCredentialOptions::new(authority_host)
83+
} else {
84+
self.options.clone()
85+
};
86+
87+
if let Ok(token) = federated_token {
88+
let mut credential: WorkloadIdentityCredential = WorkloadIdentityCredential::new(
89+
self.http_client.clone(),
90+
tenant_id,
91+
client_id,
92+
token,
93+
);
94+
credential.set_options(options);
7095

71-
if let Ok(client_secret) = client_secret {
96+
return credential.get_token(resource).await;
97+
} else if let Ok(file) = federated_token_file {
98+
let token = std::fs::read_to_string(file.clone())
99+
.with_context(ErrorKind::Credential, || {
100+
format!("failed to read federated token from file {}", file.as_str())
101+
})?;
102+
let mut credential: WorkloadIdentityCredential = WorkloadIdentityCredential::new(
103+
self.http_client.clone(),
104+
tenant_id,
105+
client_id,
106+
token,
107+
);
108+
credential.set_options(options);
109+
110+
return credential.get_token(resource).await;
111+
} else if let Ok(client_secret) = client_secret {
72112
let credential = ClientSecretCredential::new(
73113
self.http_client.clone(),
74114
tenant_id,
75115
client_id,
76116
client_secret,
77-
self.options.clone(),
117+
options,
78118
);
79119
return credential.get_token(resource).await;
80120
} else if username.is_ok() && password.is_ok() {
81121
// Could use multiple if-let with #![feature(let_chains)] once stabilised - see https://github.com/rust-lang/rust/issues/53667
82122
// TODO: username & password credential
83123
} else if let Ok(_path) = client_certificate_path {
84124
// TODO: client certificate credential
85-
todo!()
86125
}
87126

88127
Err(Error::message(

sdk/identity/src/token_credentials/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod client_secret_credentials;
1313
mod default_credentials;
1414
mod environment_credentials;
1515
mod imds_managed_identity_credentials;
16+
mod workload_identity_credentials;
1617

1718
pub use auto_refreshing_credentials::*;
1819
pub use azure_cli_credentials::*;
@@ -22,3 +23,4 @@ pub use client_secret_credentials::*;
2223
pub use default_credentials::*;
2324
pub use environment_credentials::*;
2425
pub use imds_managed_identity_credentials::*;
26+
pub use workload_identity_credentials::*;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use azure_core::auth::{AccessToken, TokenCredential, TokenResponse};
2+
use azure_core::error::{ErrorKind, ResultExt};
3+
use azure_core::HttpClient;
4+
use std::str;
5+
use std::sync::Arc;
6+
use std::time::Duration;
7+
use time::OffsetDateTime;
8+
9+
use crate::{federated_credentials_flow, TokenCredentialOptions};
10+
11+
/// Enables authentication to Azure Active Directory using a client secret that was generated for an App Registration.
12+
///
13+
/// More information on how to configure a client secret can be found here:
14+
/// <https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
15+
pub struct WorkloadIdentityCredential {
16+
http_client: Arc<dyn HttpClient>,
17+
tenant_id: String,
18+
client_id: String,
19+
token: String,
20+
options: TokenCredentialOptions,
21+
}
22+
23+
impl WorkloadIdentityCredential {
24+
/// Create a new `WorkloadIdentityCredential`
25+
pub fn new(
26+
http_client: Arc<dyn HttpClient>,
27+
tenant_id: String,
28+
client_id: String,
29+
token: String,
30+
) -> Self {
31+
Self {
32+
http_client,
33+
tenant_id,
34+
client_id,
35+
token,
36+
options: TokenCredentialOptions::default(),
37+
}
38+
}
39+
40+
/// set TokenCredentialOptions
41+
pub fn set_options(&mut self, options: TokenCredentialOptions) {
42+
self.options = options;
43+
}
44+
}
45+
46+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
47+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
48+
impl TokenCredential for WorkloadIdentityCredential {
49+
async fn get_token(&self, resource: &str) -> azure_core::Result<TokenResponse> {
50+
let res: TokenResponse = federated_credentials_flow::perform(
51+
self.http_client.clone(),
52+
&self.client_id,
53+
&self.token,
54+
&[&format!("{resource}/.default")],
55+
&self.tenant_id,
56+
self.options.authority_host(),
57+
)
58+
.await
59+
.map(|r| {
60+
TokenResponse::new(
61+
AccessToken::new(r.access_token().secret().to_owned()),
62+
OffsetDateTime::now_utc() + Duration::from_secs(r.expires_in),
63+
)
64+
})
65+
.context(ErrorKind::Credential, "request token error")?;
66+
Ok(res)
67+
}
68+
}

0 commit comments

Comments
 (0)