graph_oauth/identity/credentials/
client_assertion_credential.rs1use 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#[derive(Clone)]
42pub struct ClientAssertionCredential {
43 pub(crate) app_config: AppConfig,
44 pub(crate) client_assertion_type: String,
47 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}