graph_oauth/identity/credentials/
client_assertion_credential.rs

1use std::collections::HashMap;
2use std::fmt::{Debug, Formatter};
3
4use async_trait::async_trait;
5use http::{HeaderMap, HeaderName, HeaderValue};
6
7use uuid::Uuid;
8
9use crate::oauth_serializer::{AuthParameter, AuthSerializer};
10use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache};
11use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt};
12use graph_core::identity::ForceTokenRefresh;
13use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF};
14
15use crate::identity::credentials::app_config::AppConfig;
16use crate::identity::{
17    tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance,
18    ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE,
19};
20
21credential_builder!(
22    ClientAssertionCredentialBuilder,
23    ConfidentialClientApplication<ClientAssertionCredential>
24);
25
26/// Client Credentials Using an Assertion.
27///
28/// The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use
29/// its own credentials, instead of impersonating a user, to authenticate when calling another
30/// web service.
31///
32/// Everything in the request is the same as the certificate-based flow, with the crucial exception
33/// of the source of the client_assertion. In this flow, your application does not create the JWT
34/// assertion itself. Instead, your app uses a JWT created by another identity provider.
35/// This is called workload identity federation, where your apps identity in another identity
36/// platform is used to acquire tokens inside the Microsoft identity platform. This is best
37/// suited for cross-cloud scenarios, such as hosting your compute outside Azure but accessing
38/// APIs protected by Microsoft identity platform. For information about the required format
39/// of JWTs created by other identity providers, read about the assertion format.
40/// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential
41#[derive(Clone)]
42pub struct ClientAssertionCredential {
43    pub(crate) app_config: AppConfig,
44    /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
45    /// This is automatically set by the SDK.
46    pub(crate) client_assertion_type: String,
47    /// An assertion (a JWT, or JSON web token) that your application gets from another identity
48    /// provider outside of Microsoft identity platform, like Kubernetes. The specifics of this
49    /// JWT must be registered on your application as a federated identity credential. Read about
50    /// workload identity federation to learn how to setup and use assertions generated from
51    /// other identity providers.
52    pub(crate) client_assertion: String,
53    token_cache: InMemoryCacheStore<Token>,
54}
55
56impl Debug for ClientAssertionCredential {
57    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("ClientAssertionCredential")
59            .field("app_config", &self.app_config)
60            .finish()
61    }
62}
63
64impl ClientAssertionCredential {
65    pub fn new(
66        tenant_id: impl AsRef<str>,
67        client_id: impl AsRef<str>,
68        assertion: impl AsRef<str>,
69    ) -> ClientAssertionCredential {
70        ClientAssertionCredential {
71            app_config: AppConfig::builder(client_id.as_ref())
72                .tenant(tenant_id.as_ref())
73                .scope(vec!["https://graph.microsoft.com/.default"])
74                .build(),
75            client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
76            client_assertion: assertion.as_ref().to_string(),
77            token_cache: Default::default(),
78        }
79    }
80
81    fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> {
82        let response = self.execute()?;
83
84        if !response.status().is_success() {
85            return Err(AuthExecutionError::silent_token_auth(
86                response.into_http_response()?,
87            ));
88        }
89
90        let new_token: Token = response.json()?;
91        self.token_cache.store(cache_id, new_token.clone());
92        Ok(new_token)
93    }
94
95    async fn execute_cached_token_refresh_async(
96        &mut self,
97        cache_id: String,
98    ) -> AuthExecutionResult<Token> {
99        let response = self.execute_async().await?;
100
101        if !response.status().is_success() {
102            return Err(AuthExecutionError::silent_token_auth(
103                response.into_http_response_async().await?,
104            ));
105        }
106
107        let new_token: Token = response.json().await?;
108        self.token_cache.store(cache_id, new_token.clone());
109        Ok(new_token)
110    }
111}
112
113#[async_trait]
114impl TokenCache for ClientAssertionCredential {
115    type Token = Token;
116
117    #[tracing::instrument]
118    fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> {
119        let cache_id = self.app_config.cache_id.to_string();
120        if let Some(token) = self.token_cache.get(cache_id.as_str()) {
121            if token.is_expired_sub(time::Duration::minutes(5)) {
122                tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
123                self.execute_cached_token_refresh(cache_id)
124            } else {
125                tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
126                Ok(token)
127            }
128        } else {
129            tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
130            self.execute_cached_token_refresh(cache_id)
131        }
132    }
133
134    #[tracing::instrument]
135    async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> {
136        let cache_id = self.app_config.cache_id.to_string();
137        if let Some(token) = self.token_cache.get(cache_id.as_str()) {
138            if token.is_expired_sub(time::Duration::minutes(5)) {
139                tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
140                self.execute_cached_token_refresh_async(cache_id).await
141            } else {
142                tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
143                Ok(token.clone())
144            }
145        } else {
146            tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
147            self.execute_cached_token_refresh_async(cache_id).await
148        }
149    }
150
151    fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
152        self.app_config.force_token_refresh = force_token_refresh;
153    }
154}
155
156#[async_trait]
157impl TokenCredentialExecutor for ClientAssertionCredential {
158    fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> {
159        let mut serializer = AuthSerializer::new();
160        let client_id = self.client_id().to_string();
161        if client_id.trim().is_empty() {
162            return AF::result(AuthParameter::ClientId.alias());
163        }
164
165        if self.client_assertion.trim().is_empty() {
166            return AF::result(AuthParameter::ClientAssertion.alias());
167        }
168
169        if self.client_assertion_type.trim().is_empty() {
170            self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned();
171        }
172
173        serializer
174            .client_id(client_id.as_str())
175            .client_assertion(self.client_assertion.as_str())
176            .client_assertion_type(self.client_assertion_type.as_str())
177            .set_scope(self.app_config.scope.clone())
178            .grant_type("client_credentials");
179
180        serializer.as_credential_map(
181            vec![AuthParameter::Scope],
182            vec![
183                AuthParameter::ClientId,
184                AuthParameter::GrantType,
185                AuthParameter::ClientAssertion,
186                AuthParameter::ClientAssertionType,
187            ],
188        )
189    }
190
191    fn client_id(&self) -> &Uuid {
192        &self.app_config.client_id
193    }
194
195    fn authority(&self) -> Authority {
196        self.app_config.authority.clone()
197    }
198
199    fn azure_cloud_instance(&self) -> AzureCloudInstance {
200        self.app_config.azure_cloud_instance
201    }
202
203    fn app_config(&self) -> &AppConfig {
204        &self.app_config
205    }
206}
207
208#[derive(Clone, Debug)]
209pub struct ClientAssertionCredentialBuilder {
210    credential: ClientAssertionCredential,
211}
212
213impl ClientAssertionCredentialBuilder {
214    pub fn new(
215        client_id: impl AsRef<str>,
216        signed_assertion: impl AsRef<str>,
217    ) -> ClientAssertionCredentialBuilder {
218        ClientAssertionCredentialBuilder {
219            credential: ClientAssertionCredential {
220                app_config: AppConfig::builder(client_id.as_ref())
221                    .scope(vec!["https://graph.microsoft.com/.default"])
222                    .build(),
223                client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(),
224                client_assertion: signed_assertion.as_ref().to_owned(),
225                token_cache: Default::default(),
226            },
227        }
228    }
229
230    pub(crate) fn new_with_signed_assertion(
231        signed_assertion: impl AsRef<str>,
232        mut app_config: AppConfig,
233    ) -> ClientAssertionCredentialBuilder {
234        app_config
235            .scope
236            .insert("https://graph.microsoft.com/.default".to_string());
237        ClientAssertionCredentialBuilder {
238            credential: ClientAssertionCredential {
239                app_config,
240                client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(),
241                client_assertion: signed_assertion.as_ref().to_owned(),
242                token_cache: Default::default(),
243            },
244        }
245    }
246
247    pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self {
248        self.credential.client_assertion = client_assertion.as_ref().to_owned();
249        self
250    }
251}