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#[derive(Clone)]
80pub struct AuthCodeAuthorizationUrlParameters {
81 pub(crate) app_config: AppConfig,
82 pub(crate) response_type: BTreeSet<ResponseType>,
83 pub(crate) response_mode: Option<ResponseMode>,
97 pub(crate) nonce: Option<String>,
103 pub(crate) state: Option<String>,
104 pub(crate) prompt: BTreeSet<Prompt>,
125 pub(crate) domain_hint: Option<String>,
131 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 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 .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 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 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 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 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 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 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 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 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 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}