graph_oauth/identity/credentials/
authorization_code_spa_credential.rs

1use 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/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application
31/// to obtain authorized access to protected resources like web APIs. The auth code flow requires
32/// a user-agent that supports redirection from the authorization server (the Microsoft
33/// identity platform) back to your application. For example, a web browser, desktop, or mobile
34/// application operated by a user to sign in to your app and access their data.
35/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
36#[derive(Clone)]
37pub struct AuthorizationCodeSpaCredential {
38    app_config: AppConfig,
39    /// Required unless requesting a refresh token
40    /// The authorization code obtained from a call to authorize.
41    /// The code should be obtained with all required scopes.
42    pub(crate) authorization_code: Option<String>,
43    /// Required when requesting a new access token using a refresh token
44    /// The refresh token needed to make an access token request using a refresh token.
45    /// Do not include an authorization code when using a refresh token.
46    pub(crate) refresh_token: Option<String>,
47    /// The same code_verifier that was used to obtain the authorization_code.
48    /// Required if PKCE was used in the authorization code grant request. For more information,
49    /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636.
50    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                // Attempt to bypass a read on the token store by using previous
166                // refresh token stored outside of RwLock
167                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                // Attempt to bypass a read on the token store by using previous
209                // refresh token stored outside of RwLock
210                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    /// Defaults to http://localhost
334    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    /// Basic auth is not used in Spas because they do not use a client secret.
471    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}