graph_oauth/identity/credentials/
authorization_code_credential.rs

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/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application
30/// to obtain authorized access to protected resources like web APIs. The auth code flow requires
31/// a user-agent that supports redirection from the authorization server (the Microsoft
32/// identity platform) back to your application. For example, a web browser, desktop, or mobile
33/// application operated by a user to sign in to your app and access their data.
34/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
35#[derive(Clone)]
36pub struct AuthorizationCodeCredential {
37    app_config: AppConfig,
38    /// Required unless requesting a refresh token
39    /// The authorization code obtained from a call to authorize.
40    /// The code should be obtained with all required scopes.
41    pub(crate) authorization_code: Option<String>,
42    /// Required when requesting a new access token using a refresh token
43    /// The refresh token needed to make an access token request using a refresh token.
44    /// Do not include an authorization code when using a refresh token.
45    pub(crate) refresh_token: Option<String>,
46    /// Required
47    /// The application secret that you created in the app registration portal for your app.
48    /// Don't use the application secret in a native app or single page app because a
49    /// client_secret can't be reliably stored on devices or web pages. It's required for web
50    /// apps and web APIs, which can store the client_secret securely on the server side. Like
51    /// all parameters here, the client secret must be URL-encoded before being sent. This step
52    /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax
53    /// specification. The Basic auth pattern of instead providing credentials in the Authorization
54    /// header, per RFC 6749 is also supported.
55    pub(crate) client_secret: String,
56    /// The same code_verifier that was used to obtain the authorization_code.
57    /// Required if PKCE was used in the authorization code grant request. For more information,
58    /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636.
59    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                // Attempt to bypass a read on the token store by using previous
180                // refresh token stored outside of RwLock
181                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                // Attempt to bypass a read on the token store by using previous
223                // refresh token stored outside of RwLock
224                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    /// Defaults to http://localhost
354    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}