graph_oauth/identity/credentials/
authorization_code_spa_credential.rs1use std::collections::HashMap;
2use std::convert::TryInto;
3use std::fmt::{Debug, Formatter};
4
5use async_trait::async_trait;
6use http::{HeaderMap, HeaderName, HeaderValue};
7
8use url::Url;
9use uuid::Uuid;
10
11use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache};
12use graph_core::crypto::ProofKeyCodeExchange;
13use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt};
14use graph_core::identity::ForceTokenRefresh;
15use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF};
16
17use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder};
18use crate::identity::{
19 tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance,
20 Token, TokenCredentialExecutor,
21};
22use crate::oauth_serializer::{AuthParameter, AuthSerializer};
23use crate::{AuthCodeAuthorizationUrlParameterBuilder, PublicClientApplication};
24
25credential_builder!(
26 AuthorizationCodeSpaCredentialBuilder,
27 PublicClientApplication<AuthorizationCodeSpaCredential>
28);
29
30#[derive(Clone)]
37pub struct AuthorizationCodeSpaCredential {
38 app_config: AppConfig,
39 pub(crate) authorization_code: Option<String>,
43 pub(crate) refresh_token: Option<String>,
47 pub(crate) code_verifier: Option<String>,
51 token_cache: InMemoryCacheStore<Token>,
52}
53
54impl Debug for AuthorizationCodeSpaCredential {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("AuthorizationCodeSpaCredential")
57 .field("app_config", &self.app_config)
58 .finish()
59 }
60}
61
62impl AuthorizationCodeSpaCredential {
63 pub fn new(
64 tenant_id: impl AsRef<str>,
65 client_id: impl AsRef<str>,
66 authorization_code: impl AsRef<str>,
67 ) -> IdentityResult<AuthorizationCodeSpaCredential> {
68 Ok(AuthorizationCodeSpaCredential {
69 app_config: AppConfig::builder(client_id.as_ref())
70 .tenant(tenant_id.as_ref())
71 .build(),
72 authorization_code: Some(authorization_code.as_ref().to_owned()),
73 refresh_token: None,
74 code_verifier: None,
75 token_cache: Default::default(),
76 })
77 }
78
79 pub fn new_with_redirect_uri(
80 tenant_id: impl AsRef<str>,
81 client_id: impl AsRef<str>,
82 authorization_code: impl AsRef<str>,
83 redirect_uri: Url,
84 ) -> IdentityResult<AuthorizationCodeSpaCredential> {
85 Ok(AuthorizationCodeSpaCredential {
86 app_config: AppConfigBuilder::new(client_id.as_ref())
87 .tenant(tenant_id.as_ref())
88 .redirect_uri(redirect_uri)
89 .build(),
90 authorization_code: Some(authorization_code.as_ref().to_owned()),
91 refresh_token: None,
92 code_verifier: None,
93 token_cache: Default::default(),
94 })
95 }
96
97 pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) {
98 self.refresh_token = Some(refresh_token.as_ref().to_owned());
99 }
100
101 pub fn builder(
102 authorization_code: impl AsRef<str>,
103 client_id: impl AsRef<str>,
104 ) -> AuthorizationCodeSpaCredentialBuilder {
105 AuthorizationCodeSpaCredentialBuilder::new(authorization_code, client_id)
106 }
107
108 pub fn authorization_url_builder(
109 client_id: impl TryInto<Uuid>,
110 ) -> AuthCodeAuthorizationUrlParameterBuilder {
111 AuthCodeAuthorizationUrlParameterBuilder::new(client_id)
112 }
113
114 fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> {
115 let response = self.execute()?;
116
117 if !response.status().is_success() {
118 return Err(AuthExecutionError::silent_token_auth(
119 response.into_http_response()?,
120 ));
121 }
122
123 let new_token: Token = response.json()?;
124 self.token_cache.store(cache_id, new_token.clone());
125
126 if new_token.refresh_token.is_some() {
127 self.refresh_token = new_token.refresh_token.clone();
128 }
129
130 Ok(new_token)
131 }
132
133 async fn execute_cached_token_refresh_async(
134 &mut self,
135 cache_id: String,
136 ) -> AuthExecutionResult<Token> {
137 let response = self.execute_async().await?;
138
139 if !response.status().is_success() {
140 return Err(AuthExecutionError::silent_token_auth(
141 response.into_http_response_async().await?,
142 ));
143 }
144
145 let new_token: Token = response.json().await?;
146 self.token_cache.store(cache_id, new_token.clone());
147
148 if new_token.refresh_token.is_some() {
149 self.refresh_token = new_token.refresh_token.clone();
150 }
151 Ok(new_token)
152 }
153}
154
155#[async_trait]
156impl TokenCache for AuthorizationCodeSpaCredential {
157 type Token = Token;
158
159 #[tracing::instrument]
160 fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> {
161 let cache_id = self.app_config.cache_id.to_string();
162
163 match self.app_config.force_token_refresh {
164 ForceTokenRefresh::Never => {
165 if self.refresh_token.is_some() {
168 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
169 if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) {
170 return Ok(token);
171 }
172 }
173
174 if let Some(token) = self.token_cache.get(cache_id.as_str()) {
175 if token.is_expired_sub(time::Duration::minutes(5)) {
176 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
177 if let Some(refresh_token) = token.refresh_token.as_ref() {
178 self.refresh_token = Some(refresh_token.to_owned());
179 }
180
181 self.execute_cached_token_refresh(cache_id)
182 } else {
183 tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
184 Ok(token)
185 }
186 } else {
187 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
188 self.execute_cached_token_refresh(cache_id)
189 }
190 }
191 ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
192 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
193 let token_result = self.execute_cached_token_refresh(cache_id);
194 if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
195 self.app_config.force_token_refresh = ForceTokenRefresh::Never;
196 }
197 token_result
198 }
199 }
200 }
201
202 #[tracing::instrument]
203 async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> {
204 let cache_id = self.app_config.cache_id.to_string();
205
206 match self.app_config.force_token_refresh {
207 ForceTokenRefresh::Never => {
208 if self.refresh_token.is_some() {
211 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
212 if let Ok(token) = self
213 .execute_cached_token_refresh_async(cache_id.clone())
214 .await
215 {
216 return Ok(token);
217 }
218 }
219
220 if let Some(old_token) = self.token_cache.get(cache_id.as_str()) {
221 if old_token.is_expired_sub(time::Duration::minutes(5)) {
222 if let Some(refresh_token) = old_token.refresh_token.as_ref() {
223 self.refresh_token = Some(refresh_token.to_owned());
224 }
225 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some");
226 self.execute_cached_token_refresh_async(cache_id).await
227 } else {
228 tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
229 Ok(old_token.clone())
230 }
231 } else {
232 tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
233 self.execute_cached_token_refresh_async(cache_id).await
234 }
235 }
236 ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
237 let token_result = self.execute_cached_token_refresh_async(cache_id).await;
238 if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
239 self.app_config.force_token_refresh = ForceTokenRefresh::Never;
240 }
241 token_result
242 }
243 }
244 }
245
246 fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
247 self.app_config.force_token_refresh = force_token_refresh;
248 }
249}
250
251#[derive(Clone)]
252pub struct AuthorizationCodeSpaCredentialBuilder {
253 credential: AuthorizationCodeSpaCredential,
254}
255
256impl AuthorizationCodeSpaCredentialBuilder {
257 fn new(
258 authorization_code: impl AsRef<str>,
259 client_id: impl AsRef<str>,
260 ) -> AuthorizationCodeSpaCredentialBuilder {
261 Self {
262 credential: AuthorizationCodeSpaCredential {
263 app_config: AppConfig::new(client_id.as_ref()),
264 authorization_code: Some(authorization_code.as_ref().to_owned()),
265 refresh_token: None,
266 code_verifier: None,
267 token_cache: Default::default(),
268 },
269 }
270 }
271
272 pub(crate) fn new_with_token(
273 app_config: AppConfig,
274 token: Token,
275 ) -> AuthorizationCodeSpaCredentialBuilder {
276 let cache_id = app_config.cache_id.clone();
277 let mut token_cache = InMemoryCacheStore::new();
278 token_cache.store(cache_id, token);
279
280 Self {
281 credential: AuthorizationCodeSpaCredential {
282 app_config,
283 authorization_code: None,
284 refresh_token: None,
285 code_verifier: None,
286 token_cache,
287 },
288 }
289 }
290
291 pub(crate) fn new_with_auth_code(
292 authorization_code: impl AsRef<str>,
293 app_config: AppConfig,
294 ) -> AuthorizationCodeSpaCredentialBuilder {
295 Self {
296 credential: AuthorizationCodeSpaCredential {
297 app_config,
298 authorization_code: Some(authorization_code.as_ref().to_owned()),
299 refresh_token: None,
300 code_verifier: None,
301 token_cache: Default::default(),
302 },
303 }
304 }
305
306 #[allow(dead_code)]
307 pub(crate) fn from_secret(
308 authorization_code: String,
309 app_config: AppConfig,
310 ) -> AuthorizationCodeSpaCredentialBuilder {
311 Self {
312 credential: AuthorizationCodeSpaCredential {
313 app_config,
314 authorization_code: Some(authorization_code),
315 refresh_token: None,
316 code_verifier: None,
317 token_cache: Default::default(),
318 },
319 }
320 }
321
322 pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self {
323 self.credential.authorization_code = Some(authorization_code.as_ref().to_owned());
324 self.credential.refresh_token = None;
325 self
326 }
327
328 pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self {
329 self.credential.refresh_token = Some(refresh_token.as_ref().to_owned());
330 self
331 }
332
333 pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self {
335 self.credential.app_config.redirect_uri = Some(redirect_uri);
336 self
337 }
338
339 fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self {
340 self.credential.code_verifier = Some(code_verifier.as_ref().to_owned());
341 self
342 }
343
344 pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self {
345 self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str());
346 self
347 }
348}
349
350impl From<AuthorizationCodeSpaCredential> for AuthorizationCodeSpaCredentialBuilder {
351 fn from(credential: AuthorizationCodeSpaCredential) -> Self {
352 AuthorizationCodeSpaCredentialBuilder { credential }
353 }
354}
355
356#[async_trait]
357impl TokenCredentialExecutor for AuthorizationCodeSpaCredential {
358 fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> {
359 let mut serializer = AuthSerializer::new();
360 let client_id = self.app_config.client_id.to_string();
361 if client_id.is_empty() || self.app_config.client_id.is_nil() {
362 return AF::result(AuthParameter::ClientId.alias());
363 }
364
365 serializer
366 .client_id(client_id.as_str())
367 .set_scope(self.app_config.scope.clone());
368
369 let cache_id = self.app_config.cache_id.to_string();
370 if let Some(token) = self.token_cache.get(cache_id.as_str()) {
371 if let Some(refresh_token) = token.refresh_token.as_ref() {
372 serializer
373 .grant_type("refresh_token")
374 .refresh_token(refresh_token.as_ref());
375
376 return serializer.as_credential_map(
377 vec![AuthParameter::Scope],
378 vec![
379 AuthParameter::ClientId,
380 AuthParameter::RefreshToken,
381 AuthParameter::GrantType,
382 ],
383 );
384 }
385 }
386
387 let should_attempt_refresh = self.refresh_token.is_some()
388 && self.app_config.force_token_refresh != ForceTokenRefresh::Once
389 && self.app_config.force_token_refresh != ForceTokenRefresh::Always;
390
391 if should_attempt_refresh {
392 let refresh_token = self.refresh_token.clone().unwrap_or_default();
393 if refresh_token.trim().is_empty() {
394 return AF::msg_result(AuthParameter::RefreshToken, "Refresh token is empty");
395 }
396
397 serializer
398 .grant_type("refresh_token")
399 .refresh_token(refresh_token.as_ref());
400
401 return serializer.as_credential_map(
402 vec![AuthParameter::Scope],
403 vec![
404 AuthParameter::ClientId,
405 AuthParameter::RefreshToken,
406 AuthParameter::GrantType,
407 ],
408 );
409 } else if let Some(authorization_code) = self.authorization_code.as_ref() {
410 if authorization_code.trim().is_empty() {
411 return AF::msg_result(
412 AuthParameter::AuthorizationCode.alias(),
413 "Authorization code is empty",
414 );
415 }
416
417 if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() {
418 serializer.redirect_uri(redirect_uri.as_str());
419 }
420
421 serializer
422 .authorization_code(authorization_code.as_ref())
423 .grant_type("authorization_code");
424
425 if let Some(code_verifier) = self.code_verifier.as_ref() {
426 if code_verifier.trim().is_empty() {
427 return AF::msg_result(
428 AuthParameter::CodeVerifier.alias(),
429 "Code verifier is empty - Spa apps require Proof Key Code Exchange",
430 );
431 }
432
433 serializer.code_verifier(code_verifier.as_str());
434 }
435
436 return serializer.as_credential_map(
437 vec![AuthParameter::Scope],
438 vec![
439 AuthParameter::ClientId,
440 AuthParameter::RedirectUri,
441 AuthParameter::AuthorizationCode,
442 AuthParameter::CodeVerifier,
443 AuthParameter::GrantType,
444 ],
445 );
446 }
447
448 AF::msg_result(
449 format!(
450 "{} or {}",
451 AuthParameter::AuthorizationCode.alias(),
452 AuthParameter::RefreshToken.alias()
453 ),
454 "Either authorization code or refresh token is required",
455 )
456 }
457
458 fn client_id(&self) -> &Uuid {
459 &self.app_config.client_id
460 }
461
462 fn authority(&self) -> Authority {
463 self.app_config.authority.clone()
464 }
465
466 fn azure_cloud_instance(&self) -> AzureCloudInstance {
467 self.app_config.azure_cloud_instance
468 }
469
470 fn basic_auth(&self) -> Option<(String, String)> {
472 None
473 }
474
475 fn app_config(&self) -> &AppConfig {
476 &self.app_config
477 }
478}
479
480impl Debug for AuthorizationCodeSpaCredentialBuilder {
481 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
482 self.credential.fmt(f)
483 }
484}
485
486impl From<(AppConfig, AuthorizationResponse)> for AuthorizationCodeSpaCredentialBuilder {
487 fn from(value: (AppConfig, AuthorizationResponse)) -> Self {
488 let (app_config, authorization_response) = value;
489 if let Some(authorization_code) = authorization_response.code.as_ref() {
490 AuthorizationCodeSpaCredentialBuilder::new_with_auth_code(
491 authorization_code,
492 app_config,
493 )
494 } else {
495 AuthorizationCodeSpaCredentialBuilder::new_with_token(
496 app_config,
497 Token::try_from(authorization_response.clone()).unwrap_or_default(),
498 )
499 }
500 }
501}