1use std::collections::HashMap;
2use std::fmt::{Debug, Formatter};
3
4use async_trait::async_trait;
5use http::{HeaderMap, HeaderName, HeaderValue};
6
7use url::Url;
8use uuid::Uuid;
9
10use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache};
11use graph_core::crypto::ProofKeyCodeExchange;
12use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt};
13use graph_core::identity::ForceTokenRefresh;
14use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF};
15
16use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder};
17use crate::identity::{
18 tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance,
19 ConfidentialClientApplication, Token, TokenCredentialExecutor,
20};
21use crate::oauth_serializer::{AuthParameter, AuthSerializer};
22use crate::AuthCodeAuthorizationUrlParameterBuilder;
23
24credential_builder!(
25 AuthorizationCodeCredentialBuilder,
26 ConfidentialClientApplication<AuthorizationCodeCredential>
27);
28
29#[derive(Clone)]
36pub struct AuthorizationCodeCredential {
37 app_config: AppConfig,
38 pub(crate) authorization_code: Option<String>,
42 pub(crate) refresh_token: Option<String>,
46 pub(crate) client_secret: String,
56 pub(crate) code_verifier: Option<String>,
60 token_cache: InMemoryCacheStore<Token>,
61}
62
63impl Debug for AuthorizationCodeCredential {
64 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("AuthorizationCodeCredential")
66 .field("app_config", &self.app_config)
67 .finish()
68 }
69}
70
71impl AuthorizationCodeCredential {
72 pub fn new(
73 tenant_id: impl AsRef<str>,
74 client_id: impl AsRef<str>,
75 client_secret: impl AsRef<str>,
76 authorization_code: impl AsRef<str>,
77 ) -> IdentityResult<AuthorizationCodeCredential> {
78 Ok(AuthorizationCodeCredential {
79 app_config: AppConfig::builder(client_id.as_ref())
80 .tenant(tenant_id.as_ref())
81 .build(),
82 authorization_code: Some(authorization_code.as_ref().to_owned()),
83 refresh_token: None,
84 client_secret: client_secret.as_ref().to_owned(),
85 code_verifier: None,
86 token_cache: Default::default(),
87 })
88 }
89
90 pub fn new_with_redirect_uri(
91 tenant_id: impl AsRef<str>,
92 client_id: impl AsRef<str>,
93 client_secret: impl AsRef<str>,
94 authorization_code: impl AsRef<str>,
95 redirect_uri: Url,
96 ) -> IdentityResult<AuthorizationCodeCredential> {
97 Ok(AuthorizationCodeCredential {
98 app_config: AppConfigBuilder::new(client_id.as_ref())
99 .tenant(tenant_id.as_ref())
100 .redirect_uri(redirect_uri)
101 .build(),
102 authorization_code: Some(authorization_code.as_ref().to_owned()),
103 refresh_token: None,
104 client_secret: client_secret.as_ref().to_owned(),
105 code_verifier: None,
106 token_cache: Default::default(),
107 })
108 }
109
110 pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) {
111 self.refresh_token = Some(refresh_token.as_ref().to_owned());
112 }
113
114 pub fn builder(
115 authorization_code: impl AsRef<str>,
116 client_id: impl AsRef<str>,
117 client_secret: impl AsRef<str>,
118 ) -> AuthorizationCodeCredentialBuilder {
119 AuthorizationCodeCredentialBuilder::new(authorization_code, client_id, client_secret)
120 }
121
122 pub fn authorization_url_builder(
123 client_id: impl TryInto<Uuid>,
124 ) -> AuthCodeAuthorizationUrlParameterBuilder {
125 AuthCodeAuthorizationUrlParameterBuilder::new(client_id)
126 }
127
128 fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> {
129 let response = self.execute()?;
130
131 if !response.status().is_success() {
132 return Err(AuthExecutionError::silent_token_auth(
133 response.into_http_response()?,
134 ));
135 }
136
137 let new_token: Token = response.json()?;
138 self.token_cache.store(cache_id, new_token.clone());
139
140 if new_token.refresh_token.is_some() {
141 self.refresh_token = new_token.refresh_token.clone();
142 }
143
144 Ok(new_token)
145 }
146
147 async fn execute_cached_token_refresh_async(
148 &mut self,
149 cache_id: String,
150 ) -> AuthExecutionResult<Token> {
151 let response = self.execute_async().await?;
152
153 if !response.status().is_success() {
154 return Err(AuthExecutionError::silent_token_auth(
155 response.into_http_response_async().await?,
156 ));
157 }
158
159 let new_token: Token = response.json().await?;
160 self.token_cache.store(cache_id, new_token.clone());
161
162 if new_token.refresh_token.is_some() {
163 self.refresh_token = new_token.refresh_token.clone();
164 }
165 Ok(new_token)
166 }
167}
168
169#[async_trait]
170impl TokenCache for AuthorizationCodeCredential {
171 type Token = Token;
172
173 #[tracing::instrument]
174 fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> {
175 let cache_id = self.app_config.cache_id.to_string();
176
177 match self.app_config.force_token_refresh {
178 ForceTokenRefresh::Never => {
179 if self.refresh_token.is_some() {
182 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
183 if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) {
184 return Ok(token);
185 }
186 }
187
188 if let Some(token) = self.token_cache.get(cache_id.as_str()) {
189 if token.is_expired_sub(time::Duration::minutes(5)) {
190 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
191 if let Some(refresh_token) = token.refresh_token.as_ref() {
192 self.refresh_token = Some(refresh_token.to_owned());
193 }
194
195 self.execute_cached_token_refresh(cache_id)
196 } else {
197 tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
198 Ok(token)
199 }
200 } else {
201 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
202 self.execute_cached_token_refresh(cache_id)
203 }
204 }
205 ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
206 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
207 let token_result = self.execute_cached_token_refresh(cache_id);
208 if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
209 self.app_config.force_token_refresh = ForceTokenRefresh::Never;
210 }
211 token_result
212 }
213 }
214 }
215
216 #[tracing::instrument]
217 async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> {
218 let cache_id = self.app_config.cache_id.to_string();
219
220 match self.app_config.force_token_refresh {
221 ForceTokenRefresh::Never => {
222 if self.refresh_token.is_some() {
225 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
226 if let Ok(token) = self
227 .execute_cached_token_refresh_async(cache_id.clone())
228 .await
229 {
230 return Ok(token);
231 }
232 }
233
234 if let Some(old_token) = self.token_cache.get(cache_id.as_str()) {
235 if old_token.is_expired_sub(time::Duration::minutes(5)) {
236 if let Some(refresh_token) = old_token.refresh_token.as_ref() {
237 self.refresh_token = Some(refresh_token.to_owned());
238 }
239 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
240 self.execute_cached_token_refresh_async(cache_id).await
241 } else {
242 tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
243 Ok(old_token.clone())
244 }
245 } else {
246 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
247 self.execute_cached_token_refresh_async(cache_id).await
248 }
249 }
250 ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
251 let token_result = self.execute_cached_token_refresh_async(cache_id).await;
252 if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
253 self.app_config.force_token_refresh = ForceTokenRefresh::Never;
254 }
255 token_result
256 }
257 }
258 }
259
260 fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
261 self.app_config.force_token_refresh = force_token_refresh;
262 }
263}
264
265#[derive(Clone)]
266pub struct AuthorizationCodeCredentialBuilder {
267 credential: AuthorizationCodeCredential,
268}
269
270impl AuthorizationCodeCredentialBuilder {
271 fn new(
272 authorization_code: impl AsRef<str>,
273 client_id: impl AsRef<str>,
274 client_secret: impl AsRef<str>,
275 ) -> AuthorizationCodeCredentialBuilder {
276 Self {
277 credential: AuthorizationCodeCredential {
278 app_config: AppConfig::new(client_id.as_ref()),
279 authorization_code: Some(authorization_code.as_ref().to_owned()),
280 refresh_token: None,
281 client_secret: client_secret.as_ref().to_owned(),
282 code_verifier: None,
283 token_cache: Default::default(),
284 },
285 }
286 }
287
288 pub(crate) fn new_with_token(
289 app_config: AppConfig,
290 token: Token,
291 ) -> AuthorizationCodeCredentialBuilder {
292 let cache_id = app_config.cache_id.clone();
293 let mut token_cache = InMemoryCacheStore::new();
294 token_cache.store(cache_id, token);
295
296 Self {
297 credential: AuthorizationCodeCredential {
298 app_config,
299 authorization_code: None,
300 refresh_token: None,
301 client_secret: String::new(),
302 code_verifier: None,
303 token_cache,
304 },
305 }
306 }
307
308 pub(crate) fn new_with_auth_code(
309 authorization_code: impl AsRef<str>,
310 app_config: AppConfig,
311 ) -> AuthorizationCodeCredentialBuilder {
312 Self {
313 credential: AuthorizationCodeCredential {
314 app_config,
315 authorization_code: Some(authorization_code.as_ref().to_owned()),
316 refresh_token: None,
317 client_secret: String::new(),
318 code_verifier: None,
319 token_cache: Default::default(),
320 },
321 }
322 }
323
324 #[allow(dead_code)]
325 pub(crate) fn from_secret(
326 authorization_code: String,
327 secret: String,
328 app_config: AppConfig,
329 ) -> AuthorizationCodeCredentialBuilder {
330 Self {
331 credential: AuthorizationCodeCredential {
332 app_config,
333 authorization_code: Some(authorization_code),
334 refresh_token: None,
335 client_secret: secret,
336 code_verifier: None,
337 token_cache: Default::default(),
338 },
339 }
340 }
341
342 pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self {
343 self.credential.authorization_code = Some(authorization_code.as_ref().to_owned());
344 self.credential.refresh_token = None;
345 self
346 }
347
348 pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self {
349 self.credential.refresh_token = Some(refresh_token.as_ref().to_owned());
350 self
351 }
352
353 pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self {
355 self.credential.app_config.redirect_uri = Some(redirect_uri);
356 self
357 }
358
359 pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self {
360 self.credential.client_secret = client_secret.as_ref().to_owned();
361 self
362 }
363
364 fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self {
365 self.credential.code_verifier = Some(code_verifier.as_ref().to_owned());
366 self
367 }
368
369 pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self {
370 self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str());
371 self
372 }
373}
374
375impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder {
376 fn from(credential: AuthorizationCodeCredential) -> Self {
377 AuthorizationCodeCredentialBuilder { credential }
378 }
379}
380
381#[async_trait]
382impl TokenCredentialExecutor for AuthorizationCodeCredential {
383 fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> {
384 let mut serializer = AuthSerializer::new();
385 let client_id = self.app_config.client_id.to_string();
386 if client_id.is_empty() || self.app_config.client_id.is_nil() {
387 return AF::result(AuthParameter::ClientId.alias());
388 }
389
390 if self.client_secret.trim().is_empty() {
391 return AF::result(AuthParameter::ClientSecret.alias());
392 }
393
394 serializer
395 .client_id(client_id.as_str())
396 .client_secret(self.client_secret.as_str())
397 .set_scope(self.app_config.scope.clone());
398
399 let cache_id = self.app_config.cache_id.to_string();
400 if let Some(token) = self.token_cache.get(cache_id.as_str()) {
401 if let Some(refresh_token) = token.refresh_token.as_ref() {
402 serializer
403 .grant_type("refresh_token")
404 .refresh_token(refresh_token.as_ref());
405
406 return serializer.as_credential_map(
407 vec![AuthParameter::Scope],
408 vec![
409 AuthParameter::ClientId,
410 AuthParameter::ClientSecret,
411 AuthParameter::RefreshToken,
412 AuthParameter::GrantType,
413 ],
414 );
415 }
416 }
417
418 let should_attempt_refresh = self.refresh_token.is_some()
419 && self.app_config.force_token_refresh != ForceTokenRefresh::Once
420 && self.app_config.force_token_refresh != ForceTokenRefresh::Always;
421
422 if should_attempt_refresh {
423 let refresh_token = self.refresh_token.clone().unwrap_or_default();
424 if refresh_token.trim().is_empty() {
425 return AF::msg_result(AuthParameter::RefreshToken, "Refresh token is empty");
426 }
427
428 serializer
429 .grant_type("refresh_token")
430 .refresh_token(refresh_token.as_ref());
431
432 return serializer.as_credential_map(
433 vec![AuthParameter::Scope],
434 vec![
435 AuthParameter::ClientId,
436 AuthParameter::ClientSecret,
437 AuthParameter::RefreshToken,
438 AuthParameter::GrantType,
439 ],
440 );
441 } else if let Some(authorization_code) = self.authorization_code.as_ref() {
442 if authorization_code.trim().is_empty() {
443 return AF::msg_result(
444 AuthParameter::AuthorizationCode.alias(),
445 "Authorization code is empty",
446 );
447 }
448
449 if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() {
450 serializer.redirect_uri(redirect_uri.as_str());
451 }
452
453 serializer
454 .authorization_code(authorization_code.as_ref())
455 .grant_type("authorization_code");
456
457 if let Some(code_verifier) = self.code_verifier.as_ref() {
458 serializer.code_verifier(code_verifier.as_str());
459 }
460
461 return serializer.as_credential_map(
462 vec![AuthParameter::Scope, AuthParameter::CodeVerifier],
463 vec![
464 AuthParameter::ClientId,
465 AuthParameter::ClientSecret,
466 AuthParameter::RedirectUri,
467 AuthParameter::AuthorizationCode,
468 AuthParameter::GrantType,
469 ],
470 );
471 }
472
473 AF::msg_result(
474 format!(
475 "{} or {}",
476 AuthParameter::AuthorizationCode.alias(),
477 AuthParameter::RefreshToken.alias()
478 ),
479 "Either authorization code or refresh token is required",
480 )
481 }
482
483 fn client_id(&self) -> &Uuid {
484 &self.app_config.client_id
485 }
486
487 fn authority(&self) -> Authority {
488 self.app_config.authority.clone()
489 }
490
491 fn azure_cloud_instance(&self) -> AzureCloudInstance {
492 self.app_config.azure_cloud_instance
493 }
494
495 fn basic_auth(&self) -> Option<(String, String)> {
496 Some((
497 self.app_config.client_id.to_string(),
498 self.client_secret.clone(),
499 ))
500 }
501
502 fn app_config(&self) -> &AppConfig {
503 &self.app_config
504 }
505}
506
507impl Debug for AuthorizationCodeCredentialBuilder {
508 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
509 self.credential.fmt(f)
510 }
511}
512
513impl From<(AppConfig, AuthorizationResponse)> for AuthorizationCodeCredentialBuilder {
514 fn from(value: (AppConfig, AuthorizationResponse)) -> Self {
515 let (app_config, authorization_response) = value;
516 if let Some(authorization_code) = authorization_response.code.as_ref() {
517 AuthorizationCodeCredentialBuilder::new_with_auth_code(authorization_code, app_config)
518 } else {
519 AuthorizationCodeCredentialBuilder::new_with_token(
520 app_config,
521 Token::try_from(authorization_response.clone()).unwrap_or_default(),
522 )
523 }
524 }
525}
526
527#[cfg(test)]
528mod test {
529 use super::*;
530
531 #[test]
532 fn with_tenant_id_common() {
533 let credential = AuthorizationCodeCredential::builder(
534 "auth_code",
535 Uuid::new_v4().to_string(),
536 "client_secret",
537 )
538 .with_authority(Authority::TenantId("common".into()))
539 .build();
540
541 assert_eq!(credential.authority(), Authority::TenantId("common".into()))
542 }
543
544 #[test]
545 fn with_tenant_id_adfs() {
546 let credential = AuthorizationCodeCredential::builder(
547 "auth_code",
548 Uuid::new_v4().to_string(),
549 "client_secret",
550 )
551 .with_authority(Authority::AzureDirectoryFederatedServices)
552 .build();
553
554 assert_eq!(credential.authority().as_ref(), "adfs");
555 }
556
557 #[test]
558 #[should_panic]
559 fn required_value_missing_client_id() {
560 let mut credential_builder = AuthorizationCodeCredential::builder(
561 "auth_code",
562 Uuid::default().to_string(),
563 "secret",
564 );
565 credential_builder
566 .with_authorization_code("code")
567 .with_refresh_token("token");
568 let mut credential = credential_builder.build();
569 let _ = credential.form_urlencode().unwrap();
570 }
571
572 #[test]
573 fn serialization() {
574 let uuid_value = Uuid::new_v4().to_string();
575 let mut credential_builder =
576 AuthorizationCodeCredential::builder("auth_code", uuid_value.clone(), "secret");
577 let mut credential = credential_builder
578 .with_redirect_uri(Url::parse("http://localhost").unwrap())
579 .with_client_secret("client_secret")
580 .with_scope(vec!["scope"])
581 .with_tenant("tenant_id")
582 .build();
583
584 let map = credential.form_urlencode().unwrap();
585 assert_eq!(map.get("client_id"), Some(&uuid_value))
586 }
587
588 #[test]
589 fn should_force_refresh_test() {
590 let uuid_value = Uuid::new_v4().to_string();
591 let mut credential_builder =
592 AuthorizationCodeCredential::builder("auth_code", uuid_value, "client_secret");
593 let _credential = credential_builder
594 .with_redirect_uri(Url::parse("http://localhost").unwrap())
595 .with_client_secret("client_secret")
596 .with_scope(vec!["scope"])
597 .with_tenant("tenant_id")
598 .build();
599 }
600}