You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by tu...@apache.org on 2023/01/25 14:51:04 UTC

[arrow-rs] branch master updated: [object_store] support azure managed and workload identities (#3581)

This is an automated email from the ASF dual-hosted git repository.

tustvold pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new 73ce0760d [object_store] support azure managed and workload identities (#3581)
73ce0760d is described below

commit 73ce0760d849722e71a8a1a44b6b3230ae68f8b3
Author: Robert Pack <42...@users.noreply.github.com>
AuthorDate: Wed Jan 25 15:50:58 2023 +0100

    [object_store] support azure managed and workload identities (#3581)
    
    * feat: add azure managed identity credential
    
    * test: azure managed identity credential
    
    * feat: add azure federated token credential
    
    * test: add workload identity test
    
    * refactor: PR feedback
    
    * Update object_store/src/azure/mod.rs
    
    Co-authored-by: Raphael Taylor-Davies <17...@users.noreply.github.com>
    
    * refactor: id priorities
    
    * refactor: use managed identity as default credential
    
    * chore: remove usused parameter
    
    Co-authored-by: Raphael Taylor-Davies <17...@users.noreply.github.com>
---
 object_store/src/azure/client.rs     |   8 +-
 object_store/src/azure/credential.rs | 337 +++++++++++++++++++++++++++++++++--
 object_store/src/azure/mod.rs        | 147 +++++++++++++--
 3 files changed, 459 insertions(+), 33 deletions(-)

diff --git a/object_store/src/azure/client.rs b/object_store/src/azure/client.rs
index 426b3b164..e42950b90 100644
--- a/object_store/src/azure/client.rs
+++ b/object_store/src/azure/client.rs
@@ -169,9 +169,11 @@ impl AzureClient {
             CredentialProvider::AccessKey(key) => {
                 Ok(AzureCredential::AccessKey(key.to_owned()))
             }
-            CredentialProvider::ClientSecret(cred) => {
-                let token = cred
-                    .fetch_token(&self.client, &self.config.retry_config)
+            CredentialProvider::TokenCredential(cache, cred) => {
+                let token = cache
+                    .get_or_insert_with(|| {
+                        cred.fetch_token(&self.client, &self.config.retry_config)
+                    })
                     .await
                     .context(AuthorizationSnafu)?;
                 Ok(AzureCredential::AuthorizationToken(
diff --git a/object_store/src/azure/credential.rs b/object_store/src/azure/credential.rs
index 96ff8ce15..280d84300 100644
--- a/object_store/src/azure/credential.rs
+++ b/object_store/src/azure/credential.rs
@@ -31,6 +31,7 @@ use reqwest::{
     },
     Client, Method, RequestBuilder,
 };
+use serde::Deserialize;
 use snafu::{ResultExt, Snafu};
 use std::borrow::Cow;
 use std::str;
@@ -44,8 +45,11 @@ pub(crate) static DELETE_SNAPSHOTS: HeaderName =
     HeaderName::from_static("x-ms-delete-snapshots");
 pub(crate) static COPY_SOURCE: HeaderName = HeaderName::from_static("x-ms-copy-source");
 static CONTENT_MD5: HeaderName = HeaderName::from_static("content-md5");
-pub(crate) static RFC1123_FMT: &str = "%a, %d %h %Y %T GMT";
+pub(crate) const RFC1123_FMT: &str = "%a, %d %h %Y %T GMT";
 const CONTENT_TYPE_JSON: &str = "application/json";
+const MSI_SECRET_ENV_KEY: &str = "IDENTITY_HEADER";
+const MSI_API_VERSION: &str = "2019-08-01";
+const AZURE_STORAGE_SCOPE: &str = "https://storage.azure.com/.default";
 
 #[derive(Debug, Snafu)]
 pub enum Error {
@@ -54,6 +58,9 @@ pub enum Error {
 
     #[snafu(display("Error getting token response body: {}", source))]
     TokenResponseBody { source: reqwest::Error },
+
+    #[snafu(display("Error reading federated token file "))]
+    FederatedTokenFile,
 }
 
 pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -63,7 +70,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 pub enum CredentialProvider {
     AccessKey(String),
     SASToken(Vec<(String, String)>),
-    ClientSecret(ClientSecretOAuthProvider),
+    TokenCredential(TokenCache<String>, Box<dyn TokenCredential>),
 }
 
 pub(crate) enum AzureCredential {
@@ -273,7 +280,16 @@ fn lexy_sort<'a>(
     values
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[async_trait::async_trait]
+pub trait TokenCredential: std::fmt::Debug + Send + Sync + 'static {
+    async fn fetch_token(
+        &self,
+        client: &Client,
+        retry: &RetryConfig,
+    ) -> Result<TemporaryToken<String>>;
+}
+
+#[derive(Deserialize, Debug)]
 struct TokenResponse {
     access_token: String,
     expires_in: u64,
@@ -282,11 +298,9 @@ struct TokenResponse {
 /// Encapsulates the logic to perform an OAuth token challenge
 #[derive(Debug)]
 pub struct ClientSecretOAuthProvider {
-    scope: String,
     token_url: String,
     client_id: String,
     client_secret: String,
-    cache: TokenCache<String>,
 }
 
 impl ClientSecretOAuthProvider {
@@ -294,45 +308,220 @@ impl ClientSecretOAuthProvider {
     pub fn new(
         client_id: String,
         client_secret: String,
-        tenant_id: String,
+        tenant_id: impl AsRef<str>,
         authority_host: Option<String>,
     ) -> Self {
         let authority_host = authority_host
             .unwrap_or_else(|| authority_hosts::AZURE_PUBLIC_CLOUD.to_owned());
 
         Self {
-            scope: "https://storage.azure.com/.default".to_owned(),
-            token_url: format!("{}/{}/oauth2/v2.0/token", authority_host, tenant_id),
+            token_url: format!(
+                "{}/{}/oauth2/v2.0/token",
+                authority_host,
+                tenant_id.as_ref()
+            ),
             client_id,
             client_secret,
-            cache: TokenCache::default(),
         }
     }
+}
 
+#[async_trait::async_trait]
+impl TokenCredential for ClientSecretOAuthProvider {
     /// Fetch a token
-    pub async fn fetch_token(
+    async fn fetch_token(
         &self,
         client: &Client,
         retry: &RetryConfig,
-    ) -> Result<String> {
-        self.cache
-            .get_or_insert_with(|| self.fetch_token_inner(client, retry))
+    ) -> Result<TemporaryToken<String>> {
+        let response: TokenResponse = client
+            .request(Method::POST, &self.token_url)
+            .header(ACCEPT, HeaderValue::from_static(CONTENT_TYPE_JSON))
+            .form(&[
+                ("client_id", self.client_id.as_str()),
+                ("client_secret", self.client_secret.as_str()),
+                ("scope", AZURE_STORAGE_SCOPE),
+                ("grant_type", "client_credentials"),
+            ])
+            .send_retry(retry)
+            .await
+            .context(TokenRequestSnafu)?
+            .json()
             .await
+            .context(TokenResponseBodySnafu)?;
+
+        let token = TemporaryToken {
+            token: response.access_token,
+            expiry: Instant::now() + Duration::from_secs(response.expires_in),
+        };
+
+        Ok(token)
     }
+}
+
+fn expires_in_string<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
+where
+    D: serde::de::Deserializer<'de>,
+{
+    let v = String::deserialize(deserializer)?;
+    v.parse::<u64>().map_err(serde::de::Error::custom)
+}
+
+// NOTE: expires_on is a String version of unix epoch time, not an integer.
+// <https://learn.microsoft.com/en-gb/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http>
+#[derive(Debug, Clone, Deserialize)]
+struct MsiTokenResponse {
+    pub access_token: String,
+    #[serde(deserialize_with = "expires_in_string")]
+    pub expires_in: u64,
+}
+
+/// Attempts authentication using a managed identity that has been assigned to the deployment environment.
+///
+/// This authentication type works in Azure VMs, App Service and Azure Functions applications, as well as the Azure Cloud Shell
+/// <https://learn.microsoft.com/en-gb/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http>
+#[derive(Debug)]
+pub struct ImdsManagedIdentityOAuthProvider {
+    msi_endpoint: String,
+    client_id: Option<String>,
+    object_id: Option<String>,
+    msi_res_id: Option<String>,
+    client: Client,
+}
+
+impl ImdsManagedIdentityOAuthProvider {
+    /// Create a new [`ImdsManagedIdentityOAuthProvider`] for an azure backed store
+    pub fn new(
+        client_id: Option<String>,
+        object_id: Option<String>,
+        msi_res_id: Option<String>,
+        msi_endpoint: Option<String>,
+        client: Client,
+    ) -> Self {
+        let msi_endpoint = msi_endpoint.unwrap_or_else(|| {
+            "http://169.254.169.254/metadata/identity/oauth2/token".to_owned()
+        });
 
-    /// Fetch a fresh token
-    async fn fetch_token_inner(
+        Self {
+            msi_endpoint,
+            client_id,
+            object_id,
+            msi_res_id,
+            client,
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl TokenCredential for ImdsManagedIdentityOAuthProvider {
+    /// Fetch a token
+    async fn fetch_token(
+        &self,
+        _client: &Client,
+        retry: &RetryConfig,
+    ) -> Result<TemporaryToken<String>> {
+        let mut query_items = vec![
+            ("api-version", MSI_API_VERSION),
+            ("resource", AZURE_STORAGE_SCOPE),
+        ];
+
+        let mut identity = None;
+        if let Some(client_id) = &self.client_id {
+            identity = Some(("client_id", client_id));
+        }
+        if let Some(object_id) = &self.object_id {
+            identity = Some(("object_id", object_id));
+        }
+        if let Some(msi_res_id) = &self.msi_res_id {
+            identity = Some(("msi_res_id", msi_res_id));
+        }
+        if let Some((key, value)) = identity {
+            query_items.push((key, value));
+        }
+
+        let mut builder = self
+            .client
+            .request(Method::GET, &self.msi_endpoint)
+            .header("metadata", "true")
+            .query(&query_items);
+
+        if let Ok(val) = std::env::var(MSI_SECRET_ENV_KEY) {
+            builder = builder.header("x-identity-header", val);
+        };
+
+        let response: MsiTokenResponse = builder
+            .send_retry(retry)
+            .await
+            .context(TokenRequestSnafu)?
+            .json()
+            .await
+            .context(TokenResponseBodySnafu)?;
+
+        let token = TemporaryToken {
+            token: response.access_token,
+            expiry: Instant::now() + Duration::from_secs(response.expires_in),
+        };
+
+        Ok(token)
+    }
+}
+
+/// Credential for using workload identity dfederation
+///
+/// <https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation>
+#[derive(Debug)]
+pub struct WorkloadIdentityOAuthProvider {
+    token_url: String,
+    client_id: String,
+    federated_token_file: String,
+}
+
+impl WorkloadIdentityOAuthProvider {
+    /// Create a new [`WorkloadIdentityOAuthProvider`] for an azure backed store
+    pub fn new(
+        client_id: impl Into<String>,
+        federated_token_file: impl Into<String>,
+        tenant_id: impl AsRef<str>,
+        authority_host: Option<String>,
+    ) -> Self {
+        let authority_host = authority_host
+            .unwrap_or_else(|| authority_hosts::AZURE_PUBLIC_CLOUD.to_owned());
+
+        Self {
+            token_url: format!(
+                "{}/{}/oauth2/v2.0/token",
+                authority_host,
+                tenant_id.as_ref()
+            ),
+            client_id: client_id.into(),
+            federated_token_file: federated_token_file.into(),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl TokenCredential for WorkloadIdentityOAuthProvider {
+    /// Fetch a token
+    async fn fetch_token(
         &self,
         client: &Client,
         retry: &RetryConfig,
     ) -> Result<TemporaryToken<String>> {
+        let token_str = std::fs::read_to_string(&self.federated_token_file)
+            .map_err(|_| Error::FederatedTokenFile)?;
+
+        // 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
         let response: TokenResponse = client
             .request(Method::POST, &self.token_url)
             .header(ACCEPT, HeaderValue::from_static(CONTENT_TYPE_JSON))
             .form(&[
                 ("client_id", self.client_id.as_str()),
-                ("client_secret", self.client_secret.as_str()),
-                ("scope", self.scope.as_str()),
+                (
+                    "client_assertion_type",
+                    "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+                ),
+                ("client_assertion", token_str.as_str()),
+                ("scope", AZURE_STORAGE_SCOPE),
                 ("grant_type", "client_credentials"),
             ])
             .send_retry(retry)
@@ -350,3 +539,117 @@ impl ClientSecretOAuthProvider {
         Ok(token)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::client::mock_server::MockServer;
+    use futures::executor::block_on;
+    use hyper::body::to_bytes;
+    use hyper::{Body, Response};
+    use reqwest::{Client, Method};
+    use tempfile::NamedTempFile;
+
+    #[tokio::test]
+    async fn test_managed_identity() {
+        let server = MockServer::new();
+
+        std::env::set_var(MSI_SECRET_ENV_KEY, "env-secret");
+
+        let endpoint = server.url();
+        let client = Client::new();
+        let retry_config = RetryConfig::default();
+
+        // Test IMDS
+        server.push_fn(|req| {
+            assert_eq!(req.uri().path(), "/metadata/identity/oauth2/token");
+            assert!(req.uri().query().unwrap().contains("client_id=client_id"));
+            assert_eq!(req.method(), &Method::GET);
+            let t = req
+                .headers()
+                .get("x-identity-header")
+                .unwrap()
+                .to_str()
+                .unwrap();
+            assert_eq!(t, "env-secret");
+            let t = req.headers().get("metadata").unwrap().to_str().unwrap();
+            assert_eq!(t, "true");
+            Response::new(Body::from(
+                r#"
+            {
+                "access_token": "TOKEN",
+                "refresh_token": "",
+                "expires_in": "3599",
+                "expires_on": "1506484173",
+                "not_before": "1506480273",
+                "resource": "https://management.azure.com/",
+                "token_type": "Bearer"
+              }
+            "#,
+            ))
+        });
+
+        let credential = ImdsManagedIdentityOAuthProvider::new(
+            Some("client_id".into()),
+            None,
+            None,
+            Some(format!("{}/metadata/identity/oauth2/token", endpoint)),
+            client.clone(),
+        );
+
+        let token = credential
+            .fetch_token(&client, &retry_config)
+            .await
+            .unwrap();
+
+        assert_eq!(&token.token, "TOKEN");
+    }
+
+    #[tokio::test]
+    async fn test_workload_identity() {
+        let server = MockServer::new();
+        let tokenfile = NamedTempFile::new().unwrap();
+        let tenant = "tenant";
+        std::fs::write(tokenfile.path(), "federated-token").unwrap();
+
+        let endpoint = server.url();
+        let client = Client::new();
+        let retry_config = RetryConfig::default();
+
+        // Test IMDS
+        server.push_fn(move |req| {
+            assert_eq!(req.uri().path(), format!("/{}/oauth2/v2.0/token", tenant));
+            assert_eq!(req.method(), &Method::POST);
+            let body = block_on(to_bytes(req.into_body())).unwrap();
+            let body = String::from_utf8(body.to_vec()).unwrap();
+            assert!(body.contains("federated-token"));
+            Response::new(Body::from(
+                r#"
+            {
+                "access_token": "TOKEN",
+                "refresh_token": "",
+                "expires_in": 3599,
+                "expires_on": "1506484173",
+                "not_before": "1506480273",
+                "resource": "https://management.azure.com/",
+                "token_type": "Bearer"
+              }
+            "#,
+            ))
+        });
+
+        let credential = WorkloadIdentityOAuthProvider::new(
+            "client_id",
+            tokenfile.path().to_str().unwrap(),
+            tenant,
+            Some(endpoint.to_string()),
+        );
+
+        let token = credential
+            .fetch_token(&client, &retry_config)
+            .await
+            .unwrap();
+
+        assert_eq!(&token.token, "TOKEN");
+    }
+}
diff --git a/object_store/src/azure/mod.rs b/object_store/src/azure/mod.rs
index 3bce8e598..1eea27801 100644
--- a/object_store/src/azure/mod.rs
+++ b/object_store/src/azure/mod.rs
@@ -27,6 +27,7 @@
 //! a way to drop old blocks. Instead unused blocks are automatically cleaned up
 //! after 7 days.
 use self::client::{BlockId, BlockList};
+use crate::client::token::TokenCache;
 use crate::{
     multipart::{CloudMultiPartUpload, CloudMultiPartUploadImpl, UploadPart},
     path::Path,
@@ -65,6 +66,8 @@ const EMULATOR_ACCOUNT: &str = "devstoreaccount1";
 const EMULATOR_ACCOUNT_KEY: &str =
     "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
 
+const MSI_ENDPOINT_ENV_KEY: &str = "IDENTITY_ENDPOINT";
+
 /// A specialized `Error` for Azure object store-related errors
 #[derive(Debug, Snafu)]
 #[allow(missing_docs)]
@@ -393,6 +396,10 @@ pub struct MicrosoftAzureBuilder {
     authority_host: Option<String>,
     url: Option<String>,
     use_emulator: bool,
+    msi_endpoint: Option<String>,
+    object_id: Option<String>,
+    msi_resource_id: Option<String>,
+    federated_token_file: Option<String>,
     retry_config: RetryConfig,
     client_options: ClientOptions,
 }
@@ -496,6 +503,36 @@ pub enum AzureConfigKey {
     /// - `object_store_use_emulator`
     /// - `use_emulator`
     UseEmulator,
+
+    /// Endpoint to request a imds managed identity token
+    ///
+    /// Supported keys:
+    /// - `azure_msi_endpoint`
+    /// - `azure_identity_endpoint`
+    /// - `identity_endpoint`
+    /// - `msi_endpoint`
+    MsiEndpoint,
+
+    /// Object id for use with managed identity authentication
+    ///
+    /// Supported keys:
+    /// - `azure_object_id`
+    /// - `object_id`
+    ObjectId,
+
+    /// Msi resource id for use with managed identity authentication
+    ///
+    /// Supported keys:
+    /// - `azure_msi_resource_id`
+    /// - `msi_resource_id`
+    MsiResourceId,
+
+    /// File containing token for Azure AD workload identity federation
+    ///
+    /// Supported keys:
+    /// - `azure_federated_token_file`
+    /// - `federated_token_file`
+    FederatedTokenFile,
 }
 
 impl AsRef<str> for AzureConfigKey {
@@ -509,6 +546,10 @@ impl AsRef<str> for AzureConfigKey {
             Self::SasKey => "azure_storage_sas_key",
             Self::Token => "azure_storage_token",
             Self::UseEmulator => "azure_storage_use_emulator",
+            Self::MsiEndpoint => "azure_msi_endpoint",
+            Self::ObjectId => "azure_object_id",
+            Self::MsiResourceId => "azure_msi_resource_id",
+            Self::FederatedTokenFile => "azure_federated_token_file",
         }
     }
 }
@@ -543,6 +584,15 @@ impl FromStr for AzureConfigKey {
             | "sas_token" => Ok(Self::SasKey),
             "azure_storage_token" | "bearer_token" | "token" => Ok(Self::Token),
             "azure_storage_use_emulator" | "use_emulator" => Ok(Self::UseEmulator),
+            "azure_msi_endpoint"
+            | "azure_identity_endpoint"
+            | "identity_endpoint"
+            | "msi_endpoint" => Ok(Self::MsiEndpoint),
+            "azure_object_id" | "object_id" => Ok(Self::ObjectId),
+            "azure_msi_resource_id" | "msi_resource_id" => Ok(Self::MsiResourceId),
+            "azure_federated_token_file" | "federated_token_file" => {
+                Ok(Self::FederatedTokenFile)
+            }
             _ => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
         }
     }
@@ -600,6 +650,10 @@ impl MicrosoftAzureBuilder {
                 builder.client_options.with_allow_http(str_is_truthy(&text));
         }
 
+        if let Ok(text) = std::env::var(MSI_ENDPOINT_ENV_KEY) {
+            builder = builder.with_msi_endpoint(text);
+        }
+
         builder
     }
 
@@ -644,6 +698,12 @@ impl MicrosoftAzureBuilder {
             AzureConfigKey::AuthorityId => self.tenant_id = Some(value.into()),
             AzureConfigKey::SasKey => self.sas_key = Some(value.into()),
             AzureConfigKey::Token => self.bearer_token = Some(value.into()),
+            AzureConfigKey::MsiEndpoint => self.msi_endpoint = Some(value.into()),
+            AzureConfigKey::ObjectId => self.object_id = Some(value.into()),
+            AzureConfigKey::MsiResourceId => self.msi_resource_id = Some(value.into()),
+            AzureConfigKey::FederatedTokenFile => {
+                self.federated_token_file = Some(value.into())
+            }
             AzureConfigKey::UseEmulator => {
                 self.use_emulator = str_is_truthy(&value.into())
             }
@@ -743,6 +803,24 @@ impl MicrosoftAzureBuilder {
         self
     }
 
+    /// Sets the client id for use in client secret or k8s federated credential flow
+    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
+        self.client_id = Some(client_id.into());
+        self
+    }
+
+    /// Sets the client secret for use in client secret flow
+    pub fn with_client_secret(mut self, client_secret: impl Into<String>) -> Self {
+        self.client_secret = Some(client_secret.into());
+        self
+    }
+
+    /// Sets the tenant id for use in client secret or k8s federated credential flow
+    pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
+        self.tenant_id = Some(tenant_id.into());
+        self
+    }
+
     /// Set query pairs appended to the url for shared access signature authorization
     pub fn with_sas_authorization(
         mut self,
@@ -769,8 +847,8 @@ impl MicrosoftAzureBuilder {
     /// Sets an alternative authority host for OAuth based authorization
     /// common hosts for azure clouds are defined in [authority_hosts].
     /// Defaults to <https://login.microsoftonline.com>
-    pub fn with_authority_host(mut self, authority_host: String) -> Self {
-        self.authority_host = Some(authority_host);
+    pub fn with_authority_host(mut self, authority_host: impl Into<String>) -> Self {
+        self.authority_host = Some(authority_host.into());
         self
     }
 
@@ -792,6 +870,23 @@ impl MicrosoftAzureBuilder {
         self
     }
 
+    /// Sets the endpoint for acquiring managed identity token
+    pub fn with_msi_endpoint(mut self, msi_endpoint: impl Into<String>) -> Self {
+        self.msi_endpoint = Some(msi_endpoint.into());
+        self
+    }
+
+    /// Sets a file path for acquiring azure federated identity token in k8s
+    ///
+    /// requires `client_id` and `tenant_id` to be set
+    pub fn with_federated_token_file(
+        mut self,
+        federated_token_file: impl Into<String>,
+    ) -> Self {
+        self.federated_token_file = Some(federated_token_file.into());
+        self
+    }
+
     /// Configure a connection to container with given name on Microsoft Azure
     /// Blob store.
     pub fn build(mut self) -> Result<MicrosoftAzure> {
@@ -821,28 +916,54 @@ impl MicrosoftAzureBuilder {
             let url = Url::parse(&account_url)
                 .context(UnableToParseUrlSnafu { url: account_url })?;
             let credential = if let Some(bearer_token) = self.bearer_token {
-                Ok(credential::CredentialProvider::AccessKey(bearer_token))
+                credential::CredentialProvider::AccessKey(bearer_token)
             } else if let Some(access_key) = self.access_key {
-                Ok(credential::CredentialProvider::AccessKey(access_key))
+                credential::CredentialProvider::AccessKey(access_key)
+            } else if let (Some(client_id), Some(tenant_id), Some(federated_token_file)) =
+                (&self.client_id, &self.tenant_id, self.federated_token_file)
+            {
+                let client_credential = credential::WorkloadIdentityOAuthProvider::new(
+                    client_id,
+                    federated_token_file,
+                    tenant_id,
+                    self.authority_host,
+                );
+                credential::CredentialProvider::TokenCredential(
+                    TokenCache::default(),
+                    Box::new(client_credential),
+                )
             } else if let (Some(client_id), Some(client_secret), Some(tenant_id)) =
-                (self.client_id, self.client_secret, self.tenant_id)
+                (&self.client_id, self.client_secret, &self.tenant_id)
             {
                 let client_credential = credential::ClientSecretOAuthProvider::new(
-                    client_id,
+                    client_id.clone(),
                     client_secret,
                     tenant_id,
                     self.authority_host,
                 );
-                Ok(credential::CredentialProvider::ClientSecret(
-                    client_credential,
-                ))
+                credential::CredentialProvider::TokenCredential(
+                    TokenCache::default(),
+                    Box::new(client_credential),
+                )
             } else if let Some(query_pairs) = self.sas_query_pairs {
-                Ok(credential::CredentialProvider::SASToken(query_pairs))
+                credential::CredentialProvider::SASToken(query_pairs)
             } else if let Some(sas) = self.sas_key {
-                Ok(credential::CredentialProvider::SASToken(split_sas(&sas)?))
+                credential::CredentialProvider::SASToken(split_sas(&sas)?)
             } else {
-                Err(Error::MissingCredentials {})
-            }?;
+                let client =
+                    self.client_options.clone().with_allow_http(true).client()?;
+                let msi_credential = credential::ImdsManagedIdentityOAuthProvider::new(
+                    self.client_id,
+                    self.object_id,
+                    self.msi_resource_id,
+                    self.msi_endpoint,
+                    client,
+                );
+                credential::CredentialProvider::TokenCredential(
+                    TokenCache::default(),
+                    Box::new(msi_credential),
+                )
+            };
             (false, url, credential, account_name)
         };