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