graph_oauth/identity/credentials/
auth_code_authorization_url.rs

1use std::collections::{BTreeSet, HashMap};
2use std::fmt::{Debug, Formatter};
3
4use http::{HeaderMap, HeaderName, HeaderValue};
5use reqwest::IntoUrl;
6
7use url::Url;
8use uuid::Uuid;
9
10use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange};
11use graph_error::{IdentityResult, AF};
12
13use crate::identity::{
14    AppConfig, AsQuery, AuthorizationCodeAssertionCredentialBuilder,
15    AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode,
16    ResponseType,
17};
18use crate::oauth_serializer::{AuthParameter, AuthSerializer};
19
20#[cfg(feature = "openssl")]
21use crate::identity::{AuthorizationCodeCertificateCredentialBuilder, X509Certificate};
22
23#[cfg(feature = "interactive-auth")]
24use {
25    crate::identity::{
26        tracing_targets::INTERACTIVE_AUTH, AuthorizationCodeSpaCredentialBuilder,
27        AuthorizationResponse, Token,
28    },
29    crate::interactive::{
30        HostOptions, InteractiveAuthEvent, UserEvents, WebViewAuth, WebViewAuthorizationEvent,
31        WebViewHostValidator, WebViewOptions, WithInteractiveAuth,
32    },
33    crate::{Assertion, Secret},
34    graph_error::{AuthExecutionError, WebViewError, WebViewResult},
35    tao::{event_loop::EventLoopProxy, window::Window},
36    wry::{WebView, WebViewBuilder},
37};
38
39credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder);
40
41/// Get the authorization url required to perform the initial authorization and redirect in the
42/// authorization code flow.
43///
44/// The authorization code flow begins with the client directing the user to the /authorize
45/// endpoint.
46///
47/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application
48/// to obtain authorized access to protected resources like web APIs. The auth code flow requires
49/// a user-agent that supports redirection from the authorization server (the Microsoft identity platform)
50/// back to your application. For example, a web browser, desktop, or mobile application operated
51/// by a user to sign in to your app and access their data.
52///
53/// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code
54///
55/// # Build a confidential client for the authorization code grant.
56/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_auth_code) to set the authorization code received from
57/// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code)
58/// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder)
59/// to build the url that the user will be directed to authorize at.
60///
61/// ```rust
62/// use uuid::Uuid;
63/// use graph_oauth::{AzureCloudInstance, ConfidentialClientApplication, Prompt};
64/// use url::Url;
65///
66/// let auth_url_builder = ConfidentialClientApplication::builder(Uuid::new_v4())
67///     .auth_code_url_builder()
68///     .with_tenant("tenant-id")
69///     .with_prompt(Prompt::Login)
70///     .with_state("1234")
71///     .with_scope(vec!["User.Read"])
72///     .with_redirect_uri(Url::parse("http://localhost:8000").unwrap())
73///     .build();
74///
75/// let url = auth_url_builder.url();
76/// // or
77/// let url = auth_url_builder.url_with_host(&AzureCloudInstance::AzurePublic);
78/// ```
79#[derive(Clone)]
80pub struct AuthCodeAuthorizationUrlParameters {
81    pub(crate) app_config: AppConfig,
82    pub(crate) response_type: BTreeSet<ResponseType>,
83    /// Optional (recommended)
84    ///
85    /// Specifies how the identity platform should return the requested token to your app.
86    ///
87    /// Supported values:
88    ///
89    /// - query: Default when requesting an access token. Provides the code as a query string
90    /// parameter on your redirect URI. The query parameter isn't supported when requesting an
91    /// ID token by using the implicit flow.
92    /// - fragment: Default when requesting an ID token by using the implicit flow.
93    /// Also supported if requesting only a code.
94    /// - form_post: Executes a POST containing the code to your redirect URI.
95    /// Supported when requesting a code.
96    pub(crate) response_mode: Option<ResponseMode>,
97    /// A value included in the request, generated by the app, that is included in the
98    /// resulting id_token as a claim. The app can then verify this value to mitigate token
99    /// replay attacks. The value is typically a randomized, unique string that can be used
100    /// to identify the origin of the request.
101    /// The nonce is automatically generated unless set by the caller.
102    pub(crate) nonce: Option<String>,
103    pub(crate) state: Option<String>,
104    /// Optional
105    /// Indicates the type of user interaction that is required. The only valid values at
106    /// this time are login, none, consent, and select_account.
107    ///
108    /// The [Prompt::Login] claim forces the user to enter their credentials on that request,
109    /// which negates single sign-on.
110    ///
111    /// The [Prompt::None] parameter is the opposite, and should be paired with a login_hint to
112    /// indicate which user must be signed in. These parameters ensure that the user isn't
113    /// presented with any interactive prompt at all. If the request can't be completed silently
114    /// via single sign-on, the Microsoft identity platform returns an error. Causes include no
115    /// signed-in user, the hinted user isn't signed in, or multiple users are signed in but no
116    /// hint was provided.
117    ///
118    /// The [Prompt::Consent] claim triggers the OAuth consent dialog after the
119    /// user signs in. The dialog asks the user to grant permissions to the app.
120    ///
121    /// Finally, [Prompt::SelectAccount] shows the user an account selector, negating silent SSO but
122    /// allowing the user to pick which account they intend to sign in with, without requiring
123    /// credential entry. You can't use both login_hint and select_account.
124    pub(crate) prompt: BTreeSet<Prompt>,
125    /// Optional
126    /// The realm of the user in a federated directory. This skips the email-based discovery
127    /// process that the user goes through on the sign-in page, for a slightly more streamlined
128    /// user experience. For tenants that are federated through an on-premises directory
129    /// like AD FS, this often results in a seamless sign-in because of the existing login session.
130    pub(crate) domain_hint: Option<String>,
131    /// Optional
132    /// You can use this parameter to pre-fill the username and email address field of the
133    /// sign-in page for the user, if you know the username ahead of time. Often, apps use
134    /// this parameter during re-authentication, after already extracting the login_hint
135    /// optional claim from an earlier sign-in.
136    pub(crate) login_hint: Option<String>,
137    pub(crate) code_challenge: Option<String>,
138    pub(crate) code_challenge_method: Option<String>,
139}
140
141impl Debug for AuthCodeAuthorizationUrlParameters {
142    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("AuthCodeAuthorizationUrlParameters")
144            .field("app_config", &self.app_config)
145            .field("response_type", &self.response_type)
146            .field("response_mode", &self.response_mode)
147            .field("prompt", &self.prompt)
148            .finish()
149    }
150}
151
152impl AuthCodeAuthorizationUrlParameters {
153    pub fn new(
154        client_id: impl AsRef<str>,
155        redirect_uri: impl IntoUrl,
156    ) -> IdentityResult<AuthCodeAuthorizationUrlParameters> {
157        let mut response_type = BTreeSet::new();
158        response_type.insert(ResponseType::Code);
159        let redirect_uri_result = Url::parse(redirect_uri.as_str());
160
161        Ok(AuthCodeAuthorizationUrlParameters {
162            app_config: AppConfig::builder(client_id.as_ref())
163                .redirect_uri(redirect_uri.into_url().or(redirect_uri_result)?)
164                .build(),
165            response_type,
166            response_mode: None,
167            nonce: None,
168            state: None,
169            prompt: Default::default(),
170            domain_hint: None,
171            login_hint: None,
172            code_challenge: None,
173            code_challenge_method: None,
174        })
175    }
176
177    pub fn builder(client_id: impl TryInto<Uuid>) -> AuthCodeAuthorizationUrlParameterBuilder {
178        AuthCodeAuthorizationUrlParameterBuilder::new(client_id)
179    }
180
181    pub fn url(&self) -> IdentityResult<Url> {
182        self.url_with_host(&AzureCloudInstance::default())
183    }
184
185    pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> {
186        self.authorization_url_with_host(azure_cloud_instance)
187    }
188
189    pub fn into_credential(
190        self,
191        authorization_code: impl AsRef<str>,
192    ) -> AuthorizationCodeCredentialBuilder {
193        AuthorizationCodeCredentialBuilder::new_with_auth_code(authorization_code, self.app_config)
194    }
195
196    pub fn into_assertion_credential(
197        self,
198        authorization_code: impl AsRef<str>,
199    ) -> AuthorizationCodeAssertionCredentialBuilder {
200        AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code(
201            self.app_config,
202            authorization_code,
203        )
204    }
205
206    #[cfg(feature = "openssl")]
207    pub fn into_certificate_credential(
208        self,
209        authorization_code: impl AsRef<str>,
210        x509: &X509Certificate,
211    ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> {
212        AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509(
213            authorization_code,
214            x509,
215            self.app_config,
216        )
217    }
218
219    /// Get the nonce.
220    ///
221    /// This value may be generated automatically by the client and may be useful for users
222    /// who want to manually verify that the nonce stored in the client is the same as the
223    /// nonce returned in the response from the authorization server.
224    /// Verifying the nonce helps mitigate token replay attacks.
225    pub fn nonce(&mut self) -> Option<&String> {
226        self.nonce.as_ref()
227    }
228
229    #[cfg(feature = "interactive-auth")]
230    pub(crate) fn interactive_webview_authentication(
231        &self,
232        options: WebViewOptions,
233    ) -> WebViewResult<AuthorizationResponse> {
234        let uri = self
235            .url()
236            .map_err(|err| Box::new(AuthExecutionError::from(err)))?;
237        let redirect_uri = self.redirect_uri().cloned().unwrap();
238        let (sender, receiver) = std::sync::mpsc::channel();
239
240        std::thread::spawn(move || {
241            AuthCodeAuthorizationUrlParameters::run(uri, vec![redirect_uri], options, sender)
242                .unwrap();
243        });
244        let mut iter = receiver.try_iter();
245        let mut next = iter.next();
246
247        while next.is_none() {
248            next = iter.next();
249        }
250
251        match next {
252            None => unreachable!(),
253            Some(auth_event) => match auth_event {
254                InteractiveAuthEvent::InvalidRedirectUri(reason) => {
255                    Err(WebViewError::InvalidUri(reason))
256                }
257                InteractiveAuthEvent::ReachedRedirectUri(uri) => {
258                    let query = uri
259                        .query()
260                        .or(uri.fragment())
261                        .ok_or(WebViewError::InvalidUri(format!(
262                            "uri missing query or fragment: {}",
263                            uri
264                        )))?;
265
266                    let response_query: AuthorizationResponse =
267                        serde_urlencoded::from_str(query)
268                            .map_err(|err| WebViewError::InvalidUri(err.to_string()))?;
269
270                    if response_query.is_err() {
271                        tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri");
272                        return Err(WebViewError::Authorization {
273                            error: response_query
274                                .error
275                                .map(|query_error| query_error.to_string())
276                                .unwrap_or_default(),
277                            error_description: response_query.error_description.unwrap_or_default(),
278                            error_uri: response_query.error_uri.map(|uri| uri.to_string()),
279                        });
280                    }
281
282                    tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri");
283
284                    Ok(response_query)
285                }
286                InteractiveAuthEvent::WindowClosed(window_close_reason) => {
287                    Err(WebViewError::WindowClosed(window_close_reason.to_string()))
288                }
289            },
290        }
291    }
292
293    #[allow(dead_code)]
294    #[cfg(feature = "interactive-auth")]
295    pub(crate) fn interactive_authentication_builder(
296        &self,
297        options: WebViewOptions,
298    ) -> WebViewResult<AuthorizationResponse> {
299        let uri = self
300            .url()
301            .map_err(|err| Box::new(AuthExecutionError::from(err)))?;
302        let redirect_uri = self.redirect_uri().cloned().unwrap();
303        let (sender, receiver) = std::sync::mpsc::channel();
304
305        std::thread::spawn(move || {
306            AuthCodeAuthorizationUrlParameters::run(uri, vec![redirect_uri], options, sender)
307                .unwrap();
308        });
309        let mut iter = receiver.try_iter();
310        let mut next = iter.next();
311
312        while next.is_none() {
313            next = iter.next();
314        }
315
316        match next {
317            None => unreachable!(),
318            Some(auth_event) => match auth_event {
319                InteractiveAuthEvent::InvalidRedirectUri(reason) => {
320                    Err(WebViewError::InvalidUri(reason))
321                }
322                InteractiveAuthEvent::ReachedRedirectUri(uri) => {
323                    let query = uri
324                        .query()
325                        .or(uri.fragment())
326                        .ok_or(WebViewError::InvalidUri(format!(
327                            "uri missing query or fragment: {}",
328                            uri
329                        )))?;
330
331                    let response_query: AuthorizationResponse =
332                        serde_urlencoded::from_str(query)
333                            .map_err(|err| WebViewError::InvalidUri(err.to_string()))?;
334
335                    Ok(response_query)
336                }
337                InteractiveAuthEvent::WindowClosed(window_close_reason) => {
338                    Err(WebViewError::WindowClosed(window_close_reason.to_string()))
339                }
340            },
341        }
342    }
343}
344
345#[cfg(feature = "interactive-auth")]
346mod internal {
347    use super::*;
348
349    impl WebViewAuth for AuthCodeAuthorizationUrlParameters {
350        fn webview(
351            host_options: HostOptions,
352            window: &Window,
353            proxy: EventLoopProxy<UserEvents>,
354        ) -> anyhow::Result<WebView> {
355            let start_uri = host_options.start_uri.clone();
356            let validator = WebViewHostValidator::try_from(host_options)?;
357            Ok(WebViewBuilder::new(window)
358                .with_url(start_uri.as_ref())
359                // Disables file drop
360                .with_file_drop_handler(|_| true)
361                .with_navigation_handler(move |uri| {
362                    if let Ok(url) = Url::parse(uri.as_str()) {
363                        let is_valid_host = validator.is_valid_uri(&url);
364                        let is_redirect = validator.is_redirect_host(&url);
365
366                        if is_redirect {
367                            proxy.send_event(UserEvents::ReachedRedirectUri(url))
368                                .unwrap();
369                            proxy.send_event(UserEvents::InternalCloseWindow)
370                                .unwrap();
371                            return true;
372                        }
373
374                        is_valid_host
375                    } else {
376                        tracing::debug!(target: INTERACTIVE_AUTH, "unable to navigate webview - url is none");
377                        proxy.send_event(UserEvents::CloseWindow).unwrap();
378                        false
379                    }
380                })
381                .build()?)
382        }
383    }
384}
385
386impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters {
387    fn redirect_uri(&self) -> Option<&Url> {
388        self.app_config.redirect_uri.as_ref()
389    }
390
391    fn authorization_url(&self) -> IdentityResult<Url> {
392        self.authorization_url_with_host(&AzureCloudInstance::default())
393    }
394
395    fn authorization_url_with_host(
396        &self,
397        azure_cloud_instance: &AzureCloudInstance,
398    ) -> IdentityResult<Url> {
399        let mut serializer = AuthSerializer::new();
400
401        if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() {
402            if redirect_uri.as_str().trim().is_empty() {
403                return AF::result("redirect_uri");
404            } else {
405                serializer.redirect_uri(redirect_uri.as_str());
406            }
407        }
408
409        let client_id = self.app_config.client_id.to_string();
410        if client_id.is_empty() || self.app_config.client_id.is_nil() {
411            return AF::result("client_id");
412        }
413
414        if self.app_config.scope.is_empty() {
415            return AF::result("scope");
416        }
417
418        serializer
419            .client_id(client_id.as_str())
420            .set_scope(self.app_config.scope.clone());
421
422        let response_types: Vec<String> =
423            self.response_type.iter().map(|s| s.to_string()).collect();
424
425        if response_types.is_empty() {
426            serializer.response_type("code");
427            if let Some(response_mode) = self.response_mode.as_ref() {
428                serializer.response_mode(response_mode.as_ref());
429            }
430        } else {
431            let response_type = response_types.join(" ").trim().to_owned();
432            if response_type.is_empty() {
433                serializer.response_type("code");
434            } else {
435                serializer.response_type(response_type);
436            }
437
438            // Set response_mode
439            if self.response_type.contains(&ResponseType::IdToken) {
440                if self.response_mode.eq(&Some(ResponseMode::Query)) {
441                    return Err(AF::msg_err(
442                        "response_mode",
443                        "ResponseType::IdToken requires ResponseMode::Fragment or ResponseMode::FormPost")
444                    );
445                } else if let Some(response_mode) = self.response_mode.as_ref() {
446                    serializer.response_mode(response_mode.as_ref());
447                }
448            } else if let Some(response_mode) = self.response_mode.as_ref() {
449                serializer.response_mode(response_mode.as_ref());
450            }
451        }
452
453        if let Some(state) = self.state.as_ref() {
454            serializer.state(state.as_str());
455        }
456
457        if !self.prompt.is_empty() {
458            serializer.prompt(&self.prompt.as_query());
459        }
460
461        if let Some(domain_hint) = self.domain_hint.as_ref() {
462            serializer.domain_hint(domain_hint.as_str());
463        }
464
465        if let Some(login_hint) = self.login_hint.as_ref() {
466            serializer.login_hint(login_hint.as_str());
467        }
468
469        if let Some(nonce) = self.nonce.as_ref() {
470            serializer.nonce(nonce);
471        }
472
473        if let Some(code_challenge) = self.code_challenge.as_ref() {
474            serializer.code_challenge(code_challenge.as_str());
475        }
476
477        if let Some(code_challenge_method) = self.code_challenge_method.as_ref() {
478            serializer.code_challenge_method(code_challenge_method.as_str());
479        }
480
481        let query = serializer.encode_query(
482            vec![
483                AuthParameter::ResponseMode,
484                AuthParameter::State,
485                AuthParameter::Prompt,
486                AuthParameter::LoginHint,
487                AuthParameter::DomainHint,
488                AuthParameter::Nonce,
489                AuthParameter::CodeChallenge,
490                AuthParameter::CodeChallengeMethod,
491            ],
492            vec![
493                AuthParameter::ClientId,
494                AuthParameter::ResponseType,
495                AuthParameter::RedirectUri,
496                AuthParameter::Scope,
497            ],
498        )?;
499
500        let mut uri = azure_cloud_instance.auth_uri(&self.app_config.authority)?;
501        uri.set_query(Some(query.as_str()));
502        Ok(uri)
503    }
504}
505
506#[derive(Clone)]
507pub struct AuthCodeAuthorizationUrlParameterBuilder {
508    credential: AuthCodeAuthorizationUrlParameters,
509}
510
511impl AuthCodeAuthorizationUrlParameterBuilder {
512    pub fn new(client_id: impl TryInto<Uuid>) -> AuthCodeAuthorizationUrlParameterBuilder {
513        let mut response_type = BTreeSet::new();
514        response_type.insert(ResponseType::Code);
515        AuthCodeAuthorizationUrlParameterBuilder {
516            credential: AuthCodeAuthorizationUrlParameters {
517                app_config: AppConfig::new(client_id),
518                response_mode: None,
519                response_type,
520                nonce: None,
521                state: None,
522                prompt: Default::default(),
523                domain_hint: None,
524                login_hint: None,
525                code_challenge: None,
526                code_challenge_method: None,
527            },
528        }
529    }
530
531    pub(crate) fn new_with_app_config(
532        app_config: AppConfig,
533    ) -> AuthCodeAuthorizationUrlParameterBuilder {
534        let mut response_type = BTreeSet::new();
535        response_type.insert(ResponseType::Code);
536        AuthCodeAuthorizationUrlParameterBuilder {
537            credential: AuthCodeAuthorizationUrlParameters {
538                app_config,
539                response_mode: None,
540                response_type,
541                nonce: None,
542                state: None,
543                prompt: Default::default(),
544                domain_hint: None,
545                login_hint: None,
546                code_challenge: None,
547                code_challenge_method: None,
548            },
549        }
550    }
551
552    pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self {
553        self.credential.app_config.redirect_uri = Some(redirect_uri);
554        self
555    }
556
557    /// Default is code. Must include code for the authorization code flow.
558    /// Can also include id_token or token if using the hybrid flow.
559    pub fn with_response_type<I: IntoIterator<Item = ResponseType>>(
560        &mut self,
561        response_type: I,
562    ) -> &mut Self {
563        self.credential.response_type = response_type.into_iter().collect();
564        self
565    }
566
567    /// Specifies how the identity platform should return the requested token to your app.
568    ///
569    /// Supported values:
570    ///
571    /// - **query**: Default when requesting an access token. Provides the code as a query string
572    ///     parameter on your redirect URI. The query parameter is not supported when requesting an
573    ///     ID token by using the implicit flow.
574    /// - **fragment**: Default when requesting an ID token by using the implicit flow.
575    ///     Also supported if requesting only a code.
576    /// - **form_post**: Executes a POST containing the code to your redirect URI.
577    ///     Supported when requesting a code.
578    pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self {
579        self.credential.response_mode = Some(response_mode);
580        self
581    }
582
583    /// A value included in the request, generated by the app, that is included in the
584    /// resulting id_token as a claim. The app can then verify this value to mitigate token
585    /// replay attacks. The value is typically a randomized, unique string that can be used
586    /// to identify the origin of the request.
587    pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self {
588        self.credential.nonce = Some(nonce.as_ref().to_owned());
589        self
590    }
591
592    /// Generates a secure random nonce.
593    /// Nonce is a value included in the request, generated by the app, that is included in the
594    /// resulting id_token as a claim. The app can then verify this value to mitigate token
595    /// replay attacks. The value is typically a randomized, unique string that can be used
596    /// to identify the origin of the request.
597    pub fn with_generated_nonce(&mut self) -> &mut Self {
598        self.credential.nonce = Some(secure_random_32());
599        self
600    }
601
602    pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self {
603        self.credential.state = Some(state.as_ref().to_owned());
604        self
605    }
606
607    /// Indicates the type of user interaction that is required. Valid values are login, none,
608    /// consent, and select_account.
609    ///
610    /// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on.
611    /// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt.
612    ///     If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error.
613    /// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to
614    ///     grant permissions to the app.
615    /// - **prompt=select_account** interrupts single sign-on providing account selection experience
616    ///     listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether.
617    pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self {
618        self.credential.prompt.extend(prompt.into_iter());
619        self
620    }
621
622    pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self {
623        self.credential.domain_hint = Some(domain_hint.as_ref().to_owned());
624        self
625    }
626
627    pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self {
628        self.credential.login_hint = Some(login_hint.as_ref().to_owned());
629        self
630    }
631
632    /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE).
633    /// Required if code_challenge_method is included.
634    pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self {
635        self.credential.code_challenge = Some(code_challenge.as_ref().to_owned());
636        self
637    }
638
639    /// The method used to encode the code_verifier for the code_challenge parameter.
640    /// This SHOULD be S256, but the spec allows the use of plain if the client can't support SHA256.
641    ///
642    /// If excluded, code_challenge is assumed to be plaintext if code_challenge is included.
643    /// The Microsoft identity platform supports both plain and S256.
644    pub fn with_code_challenge_method<T: AsRef<str>>(
645        &mut self,
646        code_challenge_method: T,
647    ) -> &mut Self {
648        self.credential.code_challenge_method = Some(code_challenge_method.as_ref().to_owned());
649        self
650    }
651
652    /// Sets the code_challenge and code_challenge_method using the [ProofKeyCodeExchange]
653    /// Callers should keep the [ProofKeyCodeExchange] and provide it to the credential
654    /// builder in order to set the client verifier and request an access token.
655    pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self {
656        self.with_code_challenge(proof_key_for_code_exchange.code_challenge.as_str());
657        self.with_code_challenge_method(proof_key_for_code_exchange.code_challenge_method.as_str());
658        self
659    }
660
661    pub fn build(&self) -> AuthCodeAuthorizationUrlParameters {
662        self.credential.clone()
663    }
664
665    pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> {
666        self.credential.url_with_host(azure_cloud_instance)
667    }
668
669    pub fn url(&self) -> IdentityResult<Url> {
670        self.credential.url()
671    }
672
673    pub fn with_auth_code(
674        self,
675        authorization_code: impl AsRef<str>,
676    ) -> AuthorizationCodeCredentialBuilder {
677        AuthorizationCodeCredentialBuilder::new_with_auth_code(
678            authorization_code,
679            self.credential.app_config,
680        )
681    }
682
683    pub fn with_auth_code_assertion(
684        self,
685        authorization_code: impl AsRef<str>,
686    ) -> AuthorizationCodeAssertionCredentialBuilder {
687        AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code(
688            self.credential.app_config,
689            authorization_code,
690        )
691    }
692
693    #[cfg(feature = "openssl")]
694    pub fn with_auth_code_x509_certificate(
695        self,
696        authorization_code: impl AsRef<str>,
697        x509: &X509Certificate,
698    ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> {
699        AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509(
700            authorization_code,
701            x509,
702            self.credential.app_config,
703        )
704    }
705}
706
707#[cfg(feature = "interactive-auth")]
708impl WithInteractiveAuth<Secret> for AuthCodeAuthorizationUrlParameterBuilder {
709    type CredentialBuilder = AuthorizationCodeCredentialBuilder;
710
711    fn with_interactive_auth(
712        &self,
713        auth_type: Secret,
714        options: WebViewOptions,
715    ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> {
716        let authorization_response = self
717            .credential
718            .interactive_webview_authentication(options)?;
719
720        if authorization_response.is_err() {
721            tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri");
722            return Ok(WebViewAuthorizationEvent::Unauthorized(
723                authorization_response,
724            ));
725        }
726
727        tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri");
728
729        let mut credential_builder = {
730            if let Some(authorization_code) = authorization_response.code.as_ref() {
731                AuthorizationCodeCredentialBuilder::new_with_auth_code(
732                    authorization_code,
733                    self.credential.app_config.clone(),
734                )
735            } else {
736                AuthorizationCodeCredentialBuilder::new_with_token(
737                    self.credential.app_config.clone(),
738                    Token::try_from(authorization_response.clone())?,
739                )
740            }
741        };
742
743        credential_builder.with_client_secret(auth_type.0);
744        Ok(WebViewAuthorizationEvent::Authorized {
745            authorization_response,
746            credential_builder,
747        })
748    }
749}
750
751#[cfg(feature = "interactive-auth")]
752impl WithInteractiveAuth<ProofKeyCodeExchange> for AuthCodeAuthorizationUrlParameterBuilder {
753    type CredentialBuilder = AuthorizationCodeSpaCredentialBuilder;
754
755    fn with_interactive_auth(
756        &self,
757        auth_type: ProofKeyCodeExchange,
758        options: WebViewOptions,
759    ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> {
760        let authorization_response = self
761            .credential
762            .interactive_webview_authentication(options)?;
763
764        if authorization_response.is_err() {
765            tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri");
766            return Ok(WebViewAuthorizationEvent::Unauthorized(
767                authorization_response,
768            ));
769        }
770
771        tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri");
772
773        let mut credential_builder = {
774            if let Some(authorization_code) = authorization_response.code.as_ref() {
775                AuthorizationCodeSpaCredentialBuilder::new_with_auth_code(
776                    authorization_code,
777                    self.credential.app_config.clone(),
778                )
779            } else {
780                AuthorizationCodeSpaCredentialBuilder::new_with_token(
781                    self.credential.app_config.clone(),
782                    Token::try_from(authorization_response.clone())?,
783                )
784            }
785        };
786
787        credential_builder.with_pkce(&auth_type);
788        Ok(WebViewAuthorizationEvent::Authorized {
789            authorization_response,
790            credential_builder,
791        })
792    }
793}
794
795#[cfg(feature = "interactive-auth")]
796impl WithInteractiveAuth<Assertion> for AuthCodeAuthorizationUrlParameterBuilder {
797    type CredentialBuilder = AuthorizationCodeAssertionCredentialBuilder;
798
799    fn with_interactive_auth(
800        &self,
801        auth_type: Assertion,
802        options: WebViewOptions,
803    ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> {
804        let authorization_response = self
805            .credential
806            .interactive_webview_authentication(options)?;
807
808        if authorization_response.is_err() {
809            tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri");
810            return Ok(WebViewAuthorizationEvent::Unauthorized(
811                authorization_response,
812            ));
813        }
814
815        tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri");
816        let mut credential_builder = {
817            if let Some(authorization_code) = authorization_response.code.as_ref() {
818                AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code(
819                    self.credential.app_config.clone(),
820                    authorization_code,
821                )
822            } else {
823                AuthorizationCodeAssertionCredentialBuilder::new_with_token(
824                    self.credential.app_config.clone(),
825                    Token::try_from(authorization_response.clone())?,
826                )
827            }
828        };
829
830        credential_builder.with_client_assertion(auth_type.0);
831        Ok(WebViewAuthorizationEvent::Authorized {
832            authorization_response,
833            credential_builder,
834        })
835    }
836}
837
838#[cfg(all(feature = "openssl", feature = "interactive-auth"))]
839impl WithInteractiveAuth<&X509Certificate> for AuthCodeAuthorizationUrlParameterBuilder {
840    type CredentialBuilder = AuthorizationCodeCertificateCredentialBuilder;
841
842    fn with_interactive_auth(
843        &self,
844        auth_type: &X509Certificate,
845        options: WebViewOptions,
846    ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> {
847        let authorization_response = self
848            .credential
849            .interactive_webview_authentication(options)?;
850
851        if authorization_response.is_err() {
852            tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri");
853            return Ok(WebViewAuthorizationEvent::Unauthorized(
854                authorization_response,
855            ));
856        }
857
858        tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri");
859        let mut credential_builder = {
860            if let Some(authorization_code) = authorization_response.code.as_ref() {
861                AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509(
862                    authorization_code,
863                    auth_type,
864                    self.credential.app_config.clone(),
865                )?
866            } else {
867                AuthorizationCodeCertificateCredentialBuilder::new_with_token(
868                    Token::try_from(authorization_response.clone())?,
869                    auth_type,
870                    self.credential.app_config.clone(),
871                )?
872            }
873        };
874
875        credential_builder.with_x509(auth_type)?;
876        Ok(WebViewAuthorizationEvent::Authorized {
877            authorization_response,
878            credential_builder,
879        })
880    }
881}
882
883#[cfg(test)]
884mod test {
885    use super::*;
886
887    #[test]
888    fn serialize_uri() {
889        let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
890            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
891            .with_scope(["read", "write"])
892            .build();
893
894        let url_result = authorizer.url();
895        assert!(url_result.is_ok());
896    }
897
898    #[test]
899    fn url_with_host() {
900        let url_result = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
901            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
902            .with_scope(["read", "write"])
903            .url_with_host(&AzureCloudInstance::AzureGermany);
904
905        assert!(url_result.is_ok());
906    }
907
908    #[test]
909    #[should_panic]
910    fn response_type_id_token_panics_when_response_mode_query() {
911        let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
912            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
913            .with_scope(["read", "write"])
914            .with_response_mode(ResponseMode::Query)
915            .with_response_type(vec![ResponseType::IdToken])
916            .url()
917            .unwrap();
918
919        let _query = url.query().unwrap();
920    }
921
922    #[test]
923    fn response_mode_not_set() {
924        let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
925            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
926            .with_scope(["read", "write"])
927            .url()
928            .unwrap();
929
930        let query = url.query().unwrap();
931        assert!(!query.contains("response_mode"));
932        assert!(query.contains("response_type=code"));
933    }
934
935    #[test]
936    fn multi_response_type_set() {
937        let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
938            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
939            .with_scope(["read", "write"])
940            .with_response_mode(ResponseMode::FormPost)
941            .with_response_type(vec![ResponseType::IdToken, ResponseType::Code])
942            .url()
943            .unwrap();
944
945        let query = url.query().unwrap();
946        assert!(query.contains("response_mode=form_post"));
947        assert!(query.contains("response_type=code+id_token"));
948    }
949
950    #[test]
951    fn generate_nonce() {
952        let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4())
953            .with_redirect_uri(Url::parse("https://localhost:8080").unwrap())
954            .with_scope(["read", "write"])
955            .with_generated_nonce()
956            .url()
957            .unwrap();
958
959        let query = url.query().unwrap();
960        assert!(query.contains("nonce"));
961    }
962}