graph_oauth/identity/credentials/
authorization_code_assertion_credential.rs

1use std::collections::HashMap;
2use std::fmt::{Debug, Formatter};
3
4use async_trait::async_trait;
5use http::{HeaderMap, HeaderName, HeaderValue};
6use reqwest::IntoUrl;
7use url::Url;
8
9use uuid::Uuid;
10
11use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache};
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;
17use crate::identity::{
18    AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance,
19    ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE,
20};
21use crate::oauth_serializer::{AuthParameter, AuthSerializer};
22
23credential_builder!(
24    AuthorizationCodeAssertionCredentialBuilder,
25    ConfidentialClientApplication<AuthorizationCodeAssertionCredential>
26);
27
28/// Authorization Code Using An Assertion
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 AuthorizationCodeAssertionCredential {
38    pub(crate) app_config: AppConfig,
39    /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes.
40    pub(crate) authorization_code: Option<String>,
41    /// The refresh token needed to make an access token request using a refresh token.
42    /// Do not include an authorization code when using a refresh token.
43    pub(crate) refresh_token: Option<String>,
44    /// The same code_verifier that was used to obtain the authorization_code.
45    /// Required if PKCE was used in the authorization code grant request. For more information,
46    /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636.
47    pub(crate) code_verifier: Option<String>,
48    /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
49    pub(crate) client_assertion_type: String,
50    /// An assertion (a JSON web token) that you need to create and sign with the certificate
51    /// you registered as credentials for your application. Read about certificate credentials
52    /// to learn how to register your certificate and the format of the assertion.
53    pub(crate) client_assertion: String,
54    token_cache: InMemoryCacheStore<Token>,
55}
56
57impl Debug for AuthorizationCodeAssertionCredential {
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        f.debug_struct("AuthorizationCodeAssertionCredential")
60            .field("app_config", &self.app_config)
61            .finish()
62    }
63}
64
65impl AuthorizationCodeAssertionCredential {
66    pub fn new(
67        client_id: impl TryInto<Uuid>,
68        authorization_code: impl AsRef<str>,
69        client_assertion: impl AsRef<str>,
70        redirect_uri: Option<impl IntoUrl>,
71    ) -> IdentityResult<AuthorizationCodeAssertionCredential> {
72        let redirect_uri = {
73            if let Some(redirect_uri) = redirect_uri {
74                redirect_uri.into_url().ok()
75            } else {
76                None
77            }
78        };
79
80        Ok(AuthorizationCodeAssertionCredential {
81            app_config: AppConfig::builder(client_id)
82                .redirect_uri_option(redirect_uri)
83                .build(),
84            authorization_code: Some(authorization_code.as_ref().to_owned()),
85            refresh_token: None,
86            code_verifier: None,
87            client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
88            client_assertion: client_assertion.as_ref().to_owned(),
89            token_cache: Default::default(),
90        })
91    }
92
93    pub fn builder(
94        client_id: impl TryInto<Uuid>,
95        authorization_code: impl AsRef<str>,
96    ) -> AuthorizationCodeAssertionCredentialBuilder {
97        AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code(
98            AppConfig::new(client_id),
99            authorization_code,
100        )
101    }
102
103    pub fn authorization_url_builder(
104        client_id: impl TryInto<Uuid>,
105    ) -> AuthCodeAuthorizationUrlParameterBuilder {
106        AuthCodeAuthorizationUrlParameterBuilder::new(client_id)
107    }
108
109    fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> {
110        let response = self.execute()?;
111
112        if !response.status().is_success() {
113            return Err(AuthExecutionError::silent_token_auth(
114                response.into_http_response()?,
115            ));
116        }
117
118        let new_token: Token = response.json()?;
119        self.token_cache.store(cache_id, new_token.clone());
120
121        if new_token.refresh_token.is_some() {
122            self.refresh_token = new_token.refresh_token.clone();
123        }
124
125        Ok(new_token)
126    }
127
128    async fn execute_cached_token_refresh_async(
129        &mut self,
130        cache_id: String,
131    ) -> AuthExecutionResult<Token> {
132        let response = self.execute_async().await?;
133
134        if !response.status().is_success() {
135            return Err(AuthExecutionError::silent_token_auth(
136                response.into_http_response_async().await?,
137            ));
138        }
139
140        let new_token: Token = response.json().await?;
141
142        if new_token.refresh_token.is_some() {
143            self.refresh_token = new_token.refresh_token.clone();
144        }
145
146        self.token_cache.store(cache_id, new_token.clone());
147        Ok(new_token)
148    }
149}
150
151#[async_trait]
152impl TokenCache for AuthorizationCodeAssertionCredential {
153    type Token = Token;
154
155    fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> {
156        let cache_id = self.app_config.cache_id.to_string();
157
158        match self.app_config.force_token_refresh {
159            ForceTokenRefresh::Never => {
160                // Attempt to bypass a read on the token store by using previous
161                // refresh token stored outside of RwLock
162                if self.refresh_token.is_some() {
163                    if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) {
164                        return Ok(token);
165                    }
166                }
167
168                if let Some(token) = self.token_cache.get(cache_id.as_str()) {
169                    if token.is_expired_sub(time::Duration::minutes(5)) {
170                        if let Some(refresh_token) = token.refresh_token.as_ref() {
171                            self.refresh_token = Some(refresh_token.to_owned());
172                        }
173
174                        self.execute_cached_token_refresh(cache_id)
175                    } else {
176                        Ok(token)
177                    }
178                } else {
179                    self.execute_cached_token_refresh(cache_id)
180                }
181            }
182            ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
183                let token_result = self.execute_cached_token_refresh(cache_id);
184                if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
185                    self.app_config.force_token_refresh = ForceTokenRefresh::Never;
186                }
187                token_result
188            }
189        }
190    }
191
192    async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> {
193        let cache_id = self.app_config.cache_id.to_string();
194
195        match self.app_config.force_token_refresh {
196            ForceTokenRefresh::Never => {
197                // Attempt to bypass a read on the token store by using previous
198                // refresh token stored outside of RwLock
199                if self.refresh_token.is_some() {
200                    if let Ok(token) = self
201                        .execute_cached_token_refresh_async(cache_id.clone())
202                        .await
203                    {
204                        return Ok(token);
205                    }
206                }
207
208                if let Some(old_token) = self.token_cache.get(cache_id.as_str()) {
209                    if old_token.is_expired_sub(time::Duration::minutes(5)) {
210                        if let Some(refresh_token) = old_token.refresh_token.as_ref() {
211                            self.refresh_token = Some(refresh_token.to_owned());
212                        }
213
214                        self.execute_cached_token_refresh_async(cache_id).await
215                    } else {
216                        Ok(old_token.clone())
217                    }
218                } else {
219                    self.execute_cached_token_refresh_async(cache_id).await
220                }
221            }
222            ForceTokenRefresh::Once | ForceTokenRefresh::Always => {
223                let token_result = self.execute_cached_token_refresh_async(cache_id).await;
224                if self.app_config.force_token_refresh == ForceTokenRefresh::Once {
225                    self.app_config.force_token_refresh = ForceTokenRefresh::Never;
226                }
227                token_result
228            }
229        }
230    }
231
232    fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
233        self.app_config.force_token_refresh = force_token_refresh;
234    }
235}
236
237#[async_trait]
238impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential {
239    fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> {
240        let mut serializer = AuthSerializer::new();
241        let client_id = self.app_config.client_id.to_string();
242        if client_id.is_empty() || self.app_config.client_id.is_nil() {
243            return AF::result(AuthParameter::ClientId);
244        }
245
246        if self.client_assertion.trim().is_empty() {
247            return AF::result(AuthParameter::ClientAssertion);
248        }
249
250        if self.client_assertion_type.trim().is_empty() {
251            self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned();
252        }
253
254        serializer
255            .client_id(client_id.as_str())
256            .client_assertion(self.client_assertion.as_str())
257            .client_assertion_type(self.client_assertion_type.as_str())
258            .set_scope(self.app_config.scope.clone());
259
260        if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() {
261            serializer.redirect_uri(redirect_uri.as_str());
262        }
263
264        if let Some(code_verifier) = self.code_verifier.as_ref() {
265            serializer.code_verifier(code_verifier.as_ref());
266        }
267
268        if let Some(refresh_token) = self.refresh_token.as_ref() {
269            if refresh_token.trim().is_empty() {
270                return AF::msg_result(
271                    AuthParameter::RefreshToken.alias(),
272                    "refresh_token is empty - cannot be an empty string",
273                );
274            }
275
276            serializer
277                .refresh_token(refresh_token.as_ref())
278                .grant_type("refresh_token");
279
280            return serializer.as_credential_map(
281                vec![AuthParameter::Scope],
282                vec![
283                    AuthParameter::RefreshToken,
284                    AuthParameter::ClientId,
285                    AuthParameter::GrantType,
286                    AuthParameter::ClientAssertion,
287                    AuthParameter::ClientAssertionType,
288                ],
289            );
290        } else if let Some(authorization_code) = self.authorization_code.as_ref() {
291            if authorization_code.trim().is_empty() {
292                return AF::msg_result(
293                    AuthParameter::AuthorizationCode.alias(),
294                    "authorization_code is empty - cannot be an empty string",
295                );
296            }
297
298            serializer
299                .authorization_code(authorization_code.as_str())
300                .grant_type("authorization_code");
301
302            return serializer.as_credential_map(
303                vec![AuthParameter::Scope, AuthParameter::CodeVerifier],
304                vec![
305                    AuthParameter::AuthorizationCode,
306                    AuthParameter::ClientId,
307                    AuthParameter::GrantType,
308                    AuthParameter::RedirectUri,
309                    AuthParameter::ClientAssertion,
310                    AuthParameter::ClientAssertionType,
311                ],
312            );
313        }
314
315        AF::msg_result(
316            format!(
317                "{} or {}",
318                AuthParameter::AuthorizationCode.alias(),
319                AuthParameter::RefreshToken.alias()
320            ),
321            "Either authorization code or refresh token is required",
322        )
323    }
324
325    fn client_id(&self) -> &Uuid {
326        &self.app_config.client_id
327    }
328
329    fn authority(&self) -> Authority {
330        self.app_config.authority.clone()
331    }
332
333    fn azure_cloud_instance(&self) -> AzureCloudInstance {
334        self.app_config.azure_cloud_instance
335    }
336
337    fn app_config(&self) -> &AppConfig {
338        &self.app_config
339    }
340}
341
342#[derive(Clone)]
343pub struct AuthorizationCodeAssertionCredentialBuilder {
344    credential: AuthorizationCodeAssertionCredential,
345}
346
347impl AuthorizationCodeAssertionCredentialBuilder {
348    pub fn new(
349        client_id: impl TryInto<Uuid>,
350        authorization_code: impl AsRef<str>,
351    ) -> AuthorizationCodeAssertionCredentialBuilder {
352        AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code(
353            AppConfig::new(client_id),
354            authorization_code,
355        )
356    }
357
358    pub(crate) fn new_with_auth_code(
359        app_config: AppConfig,
360        authorization_code: impl AsRef<str>,
361    ) -> AuthorizationCodeAssertionCredentialBuilder {
362        Self {
363            credential: AuthorizationCodeAssertionCredential {
364                app_config,
365                authorization_code: Some(authorization_code.as_ref().to_owned()),
366                refresh_token: None,
367                code_verifier: None,
368                client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
369                client_assertion: String::new(),
370                token_cache: Default::default(),
371            },
372        }
373    }
374
375    #[cfg(feature = "interactive-auth")]
376    pub(crate) fn new_with_token(
377        app_config: AppConfig,
378        token: Token,
379    ) -> AuthorizationCodeAssertionCredentialBuilder {
380        let cache_id = app_config.cache_id.clone();
381        let mut token_cache = InMemoryCacheStore::new();
382        token_cache.store(cache_id, token);
383
384        Self {
385            credential: AuthorizationCodeAssertionCredential {
386                app_config,
387                authorization_code: None,
388                refresh_token: None,
389                code_verifier: None,
390                client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
391                client_assertion: String::new(),
392                token_cache,
393            },
394        }
395    }
396
397    pub(crate) fn from_assertion(
398        authorization_code: impl AsRef<str>,
399        assertion: impl AsRef<str>,
400        app_config: AppConfig,
401    ) -> AuthorizationCodeAssertionCredentialBuilder {
402        Self {
403            credential: AuthorizationCodeAssertionCredential {
404                app_config,
405                authorization_code: Some(authorization_code.as_ref().to_owned()),
406                refresh_token: None,
407                code_verifier: None,
408                client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
409                client_assertion: assertion.as_ref().to_owned(),
410                token_cache: Default::default(),
411            },
412        }
413    }
414
415    pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self {
416        self.credential.authorization_code = Some(authorization_code.as_ref().to_owned());
417        self
418    }
419
420    pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self {
421        self.credential.authorization_code = None;
422        self.credential.refresh_token = Some(refresh_token.as_ref().to_owned());
423        self
424    }
425
426    pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self {
427        self.credential.app_config.redirect_uri = Some(redirect_uri);
428        self
429    }
430
431    pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self {
432        self.credential.code_verifier = Some(code_verifier.as_ref().to_owned());
433        self
434    }
435
436    pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self {
437        self.credential.client_assertion = client_assertion.as_ref().to_owned();
438        self
439    }
440
441    pub fn with_client_assertion_type<T: AsRef<str>>(
442        &mut self,
443        client_assertion_type: T,
444    ) -> &mut Self {
445        self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned();
446        self
447    }
448
449    pub fn credential(self) -> AuthorizationCodeAssertionCredential {
450        self.credential
451    }
452}
453
454impl Debug for AuthorizationCodeAssertionCredentialBuilder {
455    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
456        self.credential.fmt(f)
457    }
458}