1use std::sync::{Arc, LazyLock};
2
3use anyhow::{anyhow, format_err};
4use http::{Extensions, StatusCode};
5use reqwest::{Request, Response};
6use reqwest_middleware::{ClientWithMiddleware, Error, Middleware, Next};
7use tokio::sync::Mutex;
8use tracing::{debug, trace, warn};
9
10use uv_netrc::Netrc;
11use uv_preview::{Preview, PreviewFeature};
12use uv_redacted::DisplaySafeUrl;
13use uv_static::EnvVars;
14use uv_warnings::owo_colors::OwoColorize;
15
16use crate::providers::{
17 AzureEndpointProvider, GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider,
18};
19use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore};
20use crate::{
21 AccessToken, CredentialsCache, KeyringProvider,
22 cache::FetchUrl,
23 credentials::{Authentication, AuthenticationError, Credentials, Username},
24 index::{AuthPolicy, Indexes},
25 realm::Realm,
26};
27use crate::{Index, TextCredentialStore};
28
29static IS_DEPENDABOT: LazyLock<bool> =
31 LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true"));
32
33impl From<AuthenticationError> for Error {
34 fn from(err: AuthenticationError) -> Self {
35 Self::middleware(err)
36 }
37}
38
39enum NetrcMode {
41 Automatic(LazyLock<Option<Netrc>>),
42 Enabled(Netrc),
43 Disabled,
44}
45
46impl Default for NetrcMode {
47 fn default() -> Self {
48 Self::Automatic(LazyLock::new(|| match Netrc::new() {
49 Ok(netrc) => Some(netrc),
50 Err(uv_netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
51 debug!("No netrc file found");
52 None
53 }
54 Err(err) => {
55 warn!("Error reading netrc file: {err}");
56 None
57 }
58 }))
59 }
60}
61
62impl NetrcMode {
63 fn get(&self) -> Option<&Netrc> {
65 match self {
66 Self::Automatic(lock) => lock.as_ref(),
67 Self::Enabled(netrc) => Some(netrc),
68 Self::Disabled => None,
69 }
70 }
71}
72
73enum TextStoreMode {
75 Automatic(tokio::sync::OnceCell<Option<TextCredentialStore>>),
76 Enabled(TextCredentialStore),
77 Disabled,
78}
79
80impl Default for TextStoreMode {
81 fn default() -> Self {
82 Self::Automatic(tokio::sync::OnceCell::new())
83 }
84}
85
86impl TextStoreMode {
87 async fn load_default_store() -> Option<TextCredentialStore> {
88 let path = TextCredentialStore::default_file()
89 .inspect_err(|err| {
90 warn!("Failed to determine credentials file path: {}", err);
91 })
92 .ok()?;
93
94 match TextCredentialStore::read(&path).await {
95 Ok((store, _lock)) => {
96 debug!("Loaded credential file {}", path.display());
97 Some(store)
98 }
99 Err(err)
100 if err
101 .as_io_error()
102 .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
103 {
104 debug!("No credentials file found at {}", path.display());
105 None
106 }
107 Err(err) => {
108 warn!(
109 "Failed to load credentials from {}: {}",
110 path.display(),
111 err
112 );
113 None
114 }
115 }
116 }
117
118 async fn get(&self) -> Option<&TextCredentialStore> {
120 match self {
121 Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(),
124 Self::Enabled(store) => Some(store),
125 Self::Disabled => None,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
131enum TokenState {
132 Uninitialized,
134 Initialized(Option<AccessToken>),
137}
138
139#[derive(Clone)]
140enum S3CredentialState {
141 Uninitialized,
143 Initialized(Option<Arc<Authentication>>),
146}
147
148#[derive(Clone)]
149enum GcsCredentialState {
150 Uninitialized,
152 Initialized(Option<Arc<Authentication>>),
155}
156
157#[derive(Clone)]
158enum AzureCredentialState {
159 Uninitialized,
161 Initialized(Option<Arc<Authentication>>),
164}
165
166pub struct AuthMiddleware {
171 netrc: NetrcMode,
172 text_store: TextStoreMode,
173 keyring: Option<KeyringProvider>,
174 cache: Arc<CredentialsCache>,
176 indexes: Indexes,
178 only_authenticated: bool,
181 base_client: Option<ClientWithMiddleware>,
183 pyx_token_store: Option<PyxTokenStore>,
185 pyx_token_state: Mutex<TokenState>,
187 s3_credential_state: Mutex<S3CredentialState>,
189 gcs_credential_state: Mutex<GcsCredentialState>,
191 azure_credential_state: Mutex<AzureCredentialState>,
193 preview: Preview,
194}
195
196impl Default for AuthMiddleware {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl AuthMiddleware {
203 pub fn new() -> Self {
204 Self {
205 netrc: NetrcMode::default(),
206 text_store: TextStoreMode::default(),
207 keyring: None,
208 cache: Arc::new(CredentialsCache::default()),
210 indexes: Indexes::new(),
211 only_authenticated: false,
212 base_client: None,
213 pyx_token_store: None,
214 pyx_token_state: Mutex::new(TokenState::Uninitialized),
215 s3_credential_state: Mutex::new(S3CredentialState::Uninitialized),
216 gcs_credential_state: Mutex::new(GcsCredentialState::Uninitialized),
217 azure_credential_state: Mutex::new(AzureCredentialState::Uninitialized),
218 preview: Preview::default(),
219 }
220 }
221
222 #[must_use]
226 pub fn with_netrc(mut self, netrc: Option<Netrc>) -> Self {
227 self.netrc = if let Some(netrc) = netrc {
228 NetrcMode::Enabled(netrc)
229 } else {
230 NetrcMode::Disabled
231 };
232 self
233 }
234
235 #[must_use]
239 pub fn with_text_store(mut self, store: Option<TextCredentialStore>) -> Self {
240 self.text_store = if let Some(store) = store {
241 TextStoreMode::Enabled(store)
242 } else {
243 TextStoreMode::Disabled
244 };
245 self
246 }
247
248 #[must_use]
250 pub fn with_keyring(mut self, keyring: Option<KeyringProvider>) -> Self {
251 self.keyring = keyring;
252 self
253 }
254
255 #[must_use]
257 pub fn with_preview(mut self, preview: Preview) -> Self {
258 self.preview = preview;
259 self
260 }
261
262 #[must_use]
264 pub fn with_cache(mut self, cache: CredentialsCache) -> Self {
265 self.cache = Arc::new(cache);
266 self
267 }
268
269 #[must_use]
271 pub fn with_cache_arc(mut self, cache: Arc<CredentialsCache>) -> Self {
272 self.cache = cache;
273 self
274 }
275
276 #[must_use]
278 pub fn with_indexes(mut self, indexes: Indexes) -> Self {
279 self.indexes = indexes;
280 self
281 }
282
283 #[must_use]
286 pub fn with_only_authenticated(mut self, only_authenticated: bool) -> Self {
287 self.only_authenticated = only_authenticated;
288 self
289 }
290
291 #[must_use]
293 pub fn with_base_client(mut self, client: ClientWithMiddleware) -> Self {
294 self.base_client = Some(client);
295 self
296 }
297
298 #[must_use]
300 pub fn with_pyx_token_store(mut self, token_store: PyxTokenStore) -> Self {
301 self.pyx_token_store = Some(token_store);
302 self
303 }
304
305 fn cache(&self) -> &CredentialsCache {
307 &self.cache
308 }
309}
310
311#[async_trait::async_trait]
312impl Middleware for AuthMiddleware {
313 async fn handle(
350 &self,
351 mut request: Request,
352 extensions: &mut Extensions,
353 next: Next<'_>,
354 ) -> reqwest_middleware::Result<Response> {
355 let request_credentials = Credentials::from_request(&request).map(Authentication::from);
357
358 let url = tracing_url(&request, request_credentials.as_ref());
361 let index = self.indexes.index_for(request.url());
362 let auth_policy = self.indexes.auth_policy_for(request.url());
363 trace!("Handling request for {url} with authentication policy {auth_policy}");
364
365 let credentials: Option<Arc<Authentication>> = if matches!(auth_policy, AuthPolicy::Never) {
366 None
367 } else {
368 if let Some(request_credentials) = request_credentials {
369 return self
370 .complete_request_with_request_credentials(
371 request_credentials,
372 request,
373 extensions,
374 next,
375 &url,
376 index,
377 auth_policy,
378 )
379 .await;
380 }
381
382 trace!("Request for {url} is unauthenticated, checking cache");
384
385 let credentials = self
388 .cache()
389 .get_url(DisplaySafeUrl::ref_cast(request.url()), &Username::none());
390 if let Some(credentials) = credentials.as_ref() {
391 request = credentials.authenticate(request).await?;
392
393 if credentials.is_authenticated() {
395 trace!("Request for {url} is fully authenticated");
396 return self
397 .complete_request(None, request, extensions, next, auth_policy)
398 .await;
399 }
400
401 trace!("Found username for {url} in cache, attempting request");
404 }
405 credentials
406 };
407 let attempt_has_username = credentials
408 .as_ref()
409 .is_some_and(|credentials| credentials.username().is_some());
410
411 let is_known_url = self
413 .pyx_token_store
414 .as_ref()
415 .is_some_and(|token_store| token_store.is_known_url(request.url()));
416
417 let must_authenticate = self.only_authenticated
418 || (match auth_policy {
419 AuthPolicy::Auto => is_known_url,
420 AuthPolicy::Always => true,
421 AuthPolicy::Never => false,
422 }
423 && !*IS_DEPENDABOT);
427
428 let (mut retry_request, response) = if !must_authenticate {
429 let url = tracing_url(&request, credentials.as_deref());
430 if credentials.is_none() {
431 trace!("Attempting unauthenticated request for {url}");
432 } else {
433 trace!("Attempting partially authenticated request for {url}");
434 }
435
436 let retry_request = request.try_clone().ok_or_else(|| {
439 Error::Middleware(anyhow!(
440 "Request object is not cloneable. Are you passing a streaming body?"
441 .to_string()
442 ))
443 })?;
444
445 let response = next.clone().run(request, extensions).await?;
446
447 if !matches!(
450 response.status(),
451 StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED
452 ) || matches!(auth_policy, AuthPolicy::Never)
453 {
454 return Ok(response);
455 }
456
457 trace!(
459 "Request for {url} failed with {}, checking for credentials",
460 response.status()
461 );
462
463 (retry_request, Some(response))
464 } else {
465 trace!("Checking for credentials for {url}");
468 (request, None)
469 };
470 let retry_request_url = DisplaySafeUrl::ref_cast(retry_request.url());
471
472 let username = credentials
473 .as_ref()
474 .map(|credentials| credentials.to_username())
475 .unwrap_or(Username::none());
476 let credentials = if let Some(index) = index {
477 self.cache().get_url(&index.url, &username).or_else(|| {
478 self.cache()
479 .get_realm(Realm::from(&**retry_request_url), username)
480 })
481 } else {
482 self.cache()
485 .get_realm(Realm::from(&**retry_request_url), username)
486 }
487 .or(credentials);
488
489 if let Some(credentials) = credentials.as_ref() {
490 if credentials.is_authenticated() {
491 trace!("Retrying request for {url} with credentials from cache {credentials:?}");
492 retry_request = credentials.authenticate(retry_request).await?;
493 return self
494 .complete_request(None, retry_request, extensions, next, auth_policy)
495 .await;
496 }
497 }
498
499 if let Some(credentials) = self
502 .fetch_credentials(
503 credentials.as_deref(),
504 retry_request_url,
505 index,
506 auth_policy,
507 )
508 .await
509 {
510 retry_request = credentials.authenticate(retry_request).await?;
511 trace!("Retrying request for {url} with {credentials:?}");
512 return self
513 .complete_request(
514 Some(credentials),
515 retry_request,
516 extensions,
517 next,
518 auth_policy,
519 )
520 .await;
521 }
522
523 if let Some(credentials) = credentials.as_ref() {
524 if !attempt_has_username {
525 trace!("Retrying request for {url} with username from cache {credentials:?}");
526 retry_request = credentials.authenticate(retry_request).await?;
527 return self
528 .complete_request(None, retry_request, extensions, next, auth_policy)
529 .await;
530 }
531 }
532
533 if let Some(response) = response {
534 Ok(response)
535 } else if let Some(store) = is_known_url
536 .then_some(self.pyx_token_store.as_ref())
537 .flatten()
538 {
539 let domain = store
540 .api()
541 .domain()
542 .unwrap_or("pyx.dev")
543 .trim_start_matches("api.");
544 Err(Error::Middleware(format_err!(
545 "Run `{}` to authenticate uv with pyx",
546 format!("uv auth login {domain}").green()
547 )))
548 } else {
549 Err(Error::Middleware(format_err!(
550 "Missing credentials for {url}"
551 )))
552 }
553 }
554}
555
556impl AuthMiddleware {
557 async fn complete_request(
561 &self,
562 credentials: Option<Arc<Authentication>>,
563 request: Request,
564 extensions: &mut Extensions,
565 next: Next<'_>,
566 auth_policy: AuthPolicy,
567 ) -> reqwest_middleware::Result<Response> {
568 let Some(credentials) = credentials else {
569 return next.run(request, extensions).await;
571 };
572 let url = DisplaySafeUrl::from_url(request.url().clone());
573 if matches!(auth_policy, AuthPolicy::Always) && !credentials.is_authenticated() {
574 return Err(Error::Middleware(format_err!(
575 "Incomplete credentials for {url}"
576 )));
577 }
578 let result = next.run(request, extensions).await;
579
580 if result
582 .as_ref()
583 .is_ok_and(|response| response.error_for_status_ref().is_ok())
584 {
585 trace!("Updating cached credentials for {url} to {credentials:?}");
587 self.cache().insert(&url, credentials);
588 }
589
590 result
591 }
592
593 async fn complete_request_with_request_credentials(
595 &self,
596 credentials: Authentication,
597 mut request: Request,
598 extensions: &mut Extensions,
599 next: Next<'_>,
600 url: &DisplaySafeUrl,
601 index: Option<&Index>,
602 auth_policy: AuthPolicy,
603 ) -> reqwest_middleware::Result<Response> {
604 let credentials = Arc::new(credentials);
605
606 if credentials.is_authenticated() {
608 trace!("Request for {url} already contains complete authentication");
609 return self
610 .complete_request(Some(credentials), request, extensions, next, auth_policy)
611 .await;
612 }
613
614 trace!("Request for {url} is missing a password, looking for credentials");
615
616 let maybe_cached_credentials = if let Some(index) = index {
620 self.cache()
621 .get_url(&index.url, credentials.as_username().as_ref())
622 .or_else(|| {
623 self.cache()
624 .get_url(&index.root_url, credentials.as_username().as_ref())
625 })
626 } else {
627 self.cache()
628 .get_realm(Realm::from(request.url()), credentials.to_username())
629 };
630 if let Some(credentials) = maybe_cached_credentials {
631 request = credentials.authenticate(request).await?;
632 let credentials = None;
634 return self
635 .complete_request(credentials, request, extensions, next, auth_policy)
636 .await;
637 }
638
639 let credentials = if let Some(credentials) = self.cache().get_url(
640 DisplaySafeUrl::ref_cast(request.url()),
641 credentials.as_username().as_ref(),
642 ) {
643 request = credentials.authenticate(request).await?;
644 None
646 } else if let Some(credentials) = self
647 .fetch_credentials(
648 Some(&credentials),
649 DisplaySafeUrl::ref_cast(request.url()),
650 index,
651 auth_policy,
652 )
653 .await
654 {
655 request = credentials.authenticate(request).await?;
656 Some(credentials)
657 } else if index.is_some() {
658 if let Some(credentials) = self
660 .cache()
661 .get_realm(Realm::from(request.url()), credentials.to_username())
662 {
663 request = credentials.authenticate(request).await?;
664 Some(credentials)
665 } else {
666 Some(credentials)
667 }
668 } else {
669 Some(credentials)
671 };
672
673 self.complete_request(credentials, request, extensions, next, auth_policy)
674 .await
675 }
676
677 async fn fetch_credentials(
681 &self,
682 credentials: Option<&Authentication>,
683 url: &DisplaySafeUrl,
684 index: Option<&Index>,
685 auth_policy: AuthPolicy,
686 ) -> Option<Arc<Authentication>> {
687 let username = Username::from(
688 credentials.map(|credentials| credentials.username().unwrap_or_default().to_string()),
689 );
690
691 let key = if let Some(index) = index {
694 (FetchUrl::Index(index.url.clone()), username)
695 } else {
696 (FetchUrl::Realm(Realm::from(&**url)), username)
697 };
698 if !self.cache().fetches.register(key.clone()) {
699 let credentials = self
700 .cache()
701 .fetches
702 .wait(&key)
703 .await
704 .expect("The key must exist after register is called");
705
706 if credentials.is_some() {
707 trace!("Using credentials from previous fetch for {}", key.0);
708 } else {
709 trace!(
710 "Skipping fetch of credentials for {}, previous attempt failed",
711 key.0
712 );
713 }
714
715 return credentials;
716 }
717
718 if let Some(credentials) = HuggingFaceProvider::credentials_for(url)
720 .map(Authentication::from)
721 .map(Arc::new)
722 {
723 debug!("Found Hugging Face credentials for {url}");
724 self.cache().fetches.done(key, Some(credentials.clone()));
725 return Some(credentials);
726 }
727
728 if S3EndpointProvider::is_s3_endpoint(url, self.preview) {
729 let mut s3_state = self.s3_credential_state.lock().await;
730
731 let credentials = match &*s3_state {
733 S3CredentialState::Uninitialized => {
734 trace!("Initializing S3 credentials for {url}");
735 let signer = S3EndpointProvider::create_signer();
736 let credentials = Arc::new(Authentication::from(signer));
737 *s3_state = S3CredentialState::Initialized(Some(credentials.clone()));
738 Some(credentials)
739 }
740 S3CredentialState::Initialized(credentials) => credentials.clone(),
741 };
742
743 if let Some(credentials) = credentials {
744 debug!("Found S3 credentials for {url}");
745 self.cache().fetches.done(key, Some(credentials.clone()));
746 return Some(credentials);
747 }
748 }
749
750 if GcsEndpointProvider::is_gcs_endpoint(url, self.preview) {
751 let mut gcs_state = self.gcs_credential_state.lock().await;
752
753 let credentials = match &*gcs_state {
755 GcsCredentialState::Uninitialized => {
756 trace!("Initializing GCS credentials for {url}");
757 let signer = GcsEndpointProvider::create_signer();
758 let credentials = Arc::new(Authentication::from(signer));
759 *gcs_state = GcsCredentialState::Initialized(Some(credentials.clone()));
760 Some(credentials)
761 }
762 GcsCredentialState::Initialized(credentials) => credentials.clone(),
763 };
764
765 if let Some(credentials) = credentials {
766 debug!("Found GCS credentials for {url}");
767 self.cache().fetches.done(key, Some(credentials.clone()));
768 return Some(credentials);
769 }
770 }
771
772 if AzureEndpointProvider::is_azure_endpoint(url, self.preview) {
773 let mut azure_state = self.azure_credential_state.lock().await;
774
775 let credentials = match &*azure_state {
777 AzureCredentialState::Uninitialized => {
778 trace!("Initializing Azure credentials for {url}");
779 let signer = AzureEndpointProvider::create_signer();
780 let credentials = Arc::new(Authentication::from(signer));
781 *azure_state = AzureCredentialState::Initialized(Some(credentials.clone()));
782 Some(credentials)
783 }
784 AzureCredentialState::Initialized(credentials) => credentials.clone(),
785 };
786
787 if let Some(credentials) = credentials {
788 debug!("Found Azure credentials for {url}");
789 self.cache().fetches.done(key, Some(credentials.clone()));
790 return Some(credentials);
791 }
792 }
793
794 let credentials = if let Some(credentials) = async {
796 let base_client = self.base_client.as_ref()?;
797 let token_store = self.pyx_token_store.as_ref()?;
798 if !token_store.is_known_url(url) {
799 return None;
800 }
801
802 let mut token_state = self.pyx_token_state.lock().await;
803
804 let token = match *token_state {
806 TokenState::Uninitialized => {
807 trace!("Initializing token store for {url}");
808 let generated = match token_store
809 .access_token(base_client, DEFAULT_TOLERANCE_SECS)
810 .await
811 {
812 Ok(Some(token)) => Some(token),
813 Ok(None) => None,
814 Err(err) => {
815 warn!("Failed to generate access tokens: {err}");
816 None
817 }
818 };
819 *token_state = TokenState::Initialized(generated.clone());
820 generated
821 }
822 TokenState::Initialized(ref tokens) => tokens.clone(),
823 };
824
825 token.map(Credentials::from)
826 }
827 .await
828 {
829 debug!("Found credentials from token store for {url}");
830 Some(credentials)
831 } else if let Some(credentials) = self.netrc.get().and_then(|netrc| {
833 debug!("Checking netrc for credentials for {url}");
834 Credentials::from_netrc(
835 netrc,
836 url,
837 credentials
838 .as_ref()
839 .and_then(|credentials| credentials.username()),
840 )
841 }) {
842 debug!("Found credentials in netrc file for {url}");
843 Some(credentials)
844
845 } else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| {
847 debug!("Checking text store for credentials for {url}");
848 match text_store.get_credentials(
849 url,
850 credentials
851 .as_ref()
852 .and_then(|credentials| credentials.username()),
853 ) {
854 Ok(credentials) => credentials.cloned(),
855 Err(err) => {
856 debug!("Failed to get credentials from text store: {err}");
857 None
858 }
859 }
860 }) {
861 debug!("Found credentials in plaintext store for {url}");
862 Some(credentials)
863 } else if let Some(credentials) = {
864 if self.preview.is_enabled(PreviewFeature::NativeAuth) {
865 let native_store = KeyringProvider::native();
866 let username = credentials.and_then(|credentials| credentials.username());
867 let display_username = if let Some(username) = username {
868 format!("{username}@")
869 } else {
870 String::new()
871 };
872 if let Some(index) = index {
873 debug!(
876 "Checking native store for credentials for index URL {}{}",
877 display_username, index.root_url
878 );
879 native_store.fetch(&index.root_url, username).await
880 } else {
881 debug!(
882 "Checking native store for credentials for URL {}{}",
883 display_username, url
884 );
885 native_store.fetch(url, username).await
886 }
887 } else {
889 None
890 }
891 } {
892 debug!("Found credentials in native store for {url}");
893 Some(credentials)
894 } else if let Some(credentials) = match self.keyring {
899 Some(ref keyring) => {
900 if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
904 if let Some(index) = index {
905 debug!(
906 "Checking keyring for credentials for index URL {}@{}",
907 username, index.url
908 );
909 keyring
910 .fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
911 .await
912 } else {
913 debug!(
914 "Checking keyring for credentials for full URL {}@{}",
915 username, url
916 );
917 keyring.fetch(url, Some(username)).await
918 }
919 } else if matches!(auth_policy, AuthPolicy::Always) {
920 if let Some(index) = index {
921 debug!(
922 "Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
923 index.url
924 );
925 keyring
926 .fetch(DisplaySafeUrl::ref_cast(&index.url), None)
927 .await
928 } else {
929 None
930 }
931 } else {
932 debug!(
933 "Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
934 );
935 None
936 }
937 }
938 None => None,
939 } {
940 debug!("Found credentials in keyring for {url}");
941 Some(credentials)
942 } else {
943 None
944 };
945
946 let credentials = credentials.map(Authentication::from).map(Arc::new);
947
948 self.cache().fetches.done(key, credentials.clone());
950
951 credentials
952 }
953}
954
955fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl {
956 let mut url = DisplaySafeUrl::from_url(request.url().clone());
957 if let Some(Authentication::Credentials(creds)) = credentials {
958 if let Some(username) = creds.username() {
959 let _ = url.set_username(username);
960 }
961 if let Some(password) = creds.password() {
962 let _ = url.set_password(Some(password));
963 }
964 }
965 url
966}
967
968#[cfg(test)]
969mod tests {
970 use std::io::Write;
971
972 use http::Method;
973 use reqwest::Client;
974 use tempfile::NamedTempFile;
975 use test_log::test;
976
977 use url::Url;
978 use wiremock::matchers::{basic_auth, method, path_regex};
979 use wiremock::{Mock, MockServer, ResponseTemplate};
980
981 use crate::Index;
982 use crate::credentials::Password;
983
984 use super::*;
985
986 type Error = Box<dyn std::error::Error>;
987
988 async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
989 let server = MockServer::start().await;
990
991 Mock::given(method("GET"))
992 .and(basic_auth(username, password))
993 .respond_with(ResponseTemplate::new(200))
994 .mount(&server)
995 .await;
996
997 Mock::given(method("GET"))
998 .respond_with(ResponseTemplate::new(401))
999 .mount(&server)
1000 .await;
1001
1002 server
1003 }
1004
1005 fn test_client_builder() -> reqwest_middleware::ClientBuilder {
1006 reqwest_middleware::ClientBuilder::new(
1007 Client::builder()
1008 .build()
1009 .expect("Reqwest client should build"),
1010 )
1011 }
1012
1013 #[test(tokio::test)]
1014 async fn test_no_credentials() -> Result<(), Error> {
1015 let server = start_test_server("user", "password").await;
1016 let client = test_client_builder()
1017 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1018 .build();
1019
1020 assert_eq!(
1021 client
1022 .get(format!("{}/foo", server.uri()))
1023 .send()
1024 .await?
1025 .status(),
1026 401
1027 );
1028
1029 assert_eq!(
1030 client
1031 .get(format!("{}/bar", server.uri()))
1032 .send()
1033 .await?
1034 .status(),
1035 401
1036 );
1037
1038 Ok(())
1039 }
1040
1041 #[test(tokio::test)]
1043 async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
1044 let username = "user";
1045 let password = "password";
1046
1047 let server = start_test_server(username, password).await;
1048 let client = test_client_builder()
1049 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1050 .build();
1051
1052 let base_url = Url::parse(&server.uri())?;
1053
1054 let mut url = base_url.clone();
1055 url.set_username(username).unwrap();
1056 url.set_password(Some(password)).unwrap();
1057 assert_eq!(client.get(url).send().await?.status(), 200);
1058
1059 assert_eq!(
1061 client.get(server.uri()).send().await?.status(),
1062 200,
1063 "Subsequent requests should not require credentials"
1064 );
1065
1066 assert_eq!(
1067 client
1068 .get(format!("{}/foo", server.uri()))
1069 .send()
1070 .await?
1071 .status(),
1072 200,
1073 "Requests can be to different paths in the same realm"
1074 );
1075
1076 let mut url = base_url.clone();
1077 url.set_username(username).unwrap();
1078 url.set_password(Some("invalid")).unwrap();
1079 assert_eq!(
1080 client.get(url).send().await?.status(),
1081 401,
1082 "Credentials in the URL should take precedence and fail"
1083 );
1084
1085 Ok(())
1086 }
1087
1088 #[test(tokio::test)]
1089 async fn test_credentials_in_url_seed() -> Result<(), Error> {
1090 let username = "user";
1091 let password = "password";
1092
1093 let server = start_test_server(username, password).await;
1094 let base_url = Url::parse(&server.uri())?;
1095 let cache = CredentialsCache::new();
1096 cache.insert(
1097 DisplaySafeUrl::ref_cast(&base_url),
1098 Arc::new(Authentication::from(Credentials::basic(
1099 Some(username.to_string()),
1100 Some(password.to_string()),
1101 ))),
1102 );
1103
1104 let client = test_client_builder()
1105 .with(AuthMiddleware::new().with_cache(cache))
1106 .build();
1107
1108 let mut url = base_url.clone();
1109 url.set_username(username).unwrap();
1110 url.set_password(Some(password)).unwrap();
1111 assert_eq!(client.get(url).send().await?.status(), 200);
1112
1113 assert_eq!(
1115 client.get(server.uri()).send().await?.status(),
1116 200,
1117 "Requests should not require credentials"
1118 );
1119
1120 assert_eq!(
1121 client
1122 .get(format!("{}/foo", server.uri()))
1123 .send()
1124 .await?
1125 .status(),
1126 200,
1127 "Requests can be to different paths in the same realm"
1128 );
1129
1130 let mut url = base_url.clone();
1131 url.set_username(username).unwrap();
1132 url.set_password(Some("invalid")).unwrap();
1133 assert_eq!(
1134 client.get(url).send().await?.status(),
1135 401,
1136 "Credentials in the URL should take precedence and fail"
1137 );
1138
1139 Ok(())
1140 }
1141
1142 #[test(tokio::test)]
1143 async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1144 let username = "user";
1145 let password = "";
1146
1147 let server = start_test_server(username, password).await;
1148 let base_url = Url::parse(&server.uri())?;
1149 let cache = CredentialsCache::new();
1150 cache.insert(
1151 DisplaySafeUrl::ref_cast(&base_url),
1152 Arc::new(Authentication::from(Credentials::basic(
1153 Some(username.to_string()),
1154 None,
1155 ))),
1156 );
1157
1158 let client = test_client_builder()
1159 .with(AuthMiddleware::new().with_cache(cache))
1160 .build();
1161
1162 let mut url = base_url.clone();
1163 url.set_username(username).unwrap();
1164 url.set_password(None).unwrap();
1165 assert_eq!(client.get(url).send().await?.status(), 200);
1166
1167 assert_eq!(
1169 client.get(server.uri()).send().await?.status(),
1170 200,
1171 "Requests should not require credentials"
1172 );
1173
1174 assert_eq!(
1175 client
1176 .get(format!("{}/foo", server.uri()))
1177 .send()
1178 .await?
1179 .status(),
1180 200,
1181 "Requests can be to different paths in the same realm"
1182 );
1183
1184 let mut url = base_url.clone();
1185 url.set_username(username).unwrap();
1186 url.set_password(Some("invalid")).unwrap();
1187 assert_eq!(
1188 client.get(url).send().await?.status(),
1189 401,
1190 "Credentials in the URL should take precedence and fail"
1191 );
1192
1193 assert_eq!(
1194 client.get(server.uri()).send().await?.status(),
1195 200,
1196 "Subsequent requests should not use the invalid credentials"
1197 );
1198
1199 Ok(())
1200 }
1201
1202 #[test(tokio::test)]
1203 async fn test_netrc_file_default_host() -> Result<(), Error> {
1204 let username = "user";
1205 let password = "password";
1206
1207 let mut netrc_file = NamedTempFile::new()?;
1208 writeln!(netrc_file, "default login {username} password {password}")?;
1209
1210 let server = start_test_server(username, password).await;
1211 let client = test_client_builder()
1212 .with(
1213 AuthMiddleware::new()
1214 .with_cache(CredentialsCache::new())
1215 .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1216 )
1217 .build();
1218
1219 assert_eq!(
1220 client.get(server.uri()).send().await?.status(),
1221 200,
1222 "Credentials should be pulled from the netrc file"
1223 );
1224
1225 let mut url = Url::parse(&server.uri())?;
1226 url.set_username(username).unwrap();
1227 url.set_password(Some("invalid")).unwrap();
1228 assert_eq!(
1229 client.get(url).send().await?.status(),
1230 401,
1231 "Credentials in the URL should take precedence and fail"
1232 );
1233
1234 assert_eq!(
1235 client.get(server.uri()).send().await?.status(),
1236 200,
1237 "Subsequent requests should not use the invalid credentials"
1238 );
1239
1240 Ok(())
1241 }
1242
1243 #[test(tokio::test)]
1244 async fn test_netrc_file_matching_host() -> Result<(), Error> {
1245 let username = "user";
1246 let password = "password";
1247 let server = start_test_server(username, password).await;
1248 let base_url = Url::parse(&server.uri())?;
1249
1250 let mut netrc_file = NamedTempFile::new()?;
1251 writeln!(
1252 netrc_file,
1253 r"machine {} login {username} password {password}",
1254 base_url.host_str().unwrap()
1255 )?;
1256
1257 let client = test_client_builder()
1258 .with(
1259 AuthMiddleware::new()
1260 .with_cache(CredentialsCache::new())
1261 .with_netrc(Some(
1262 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1263 )),
1264 )
1265 .build();
1266
1267 assert_eq!(
1268 client.get(server.uri()).send().await?.status(),
1269 200,
1270 "Credentials should be pulled from the netrc file"
1271 );
1272
1273 let mut url = base_url.clone();
1274 url.set_username(username).unwrap();
1275 url.set_password(Some("invalid")).unwrap();
1276 assert_eq!(
1277 client.get(url).send().await?.status(),
1278 401,
1279 "Credentials in the URL should take precedence and fail"
1280 );
1281
1282 assert_eq!(
1283 client.get(server.uri()).send().await?.status(),
1284 200,
1285 "Subsequent requests should not use the invalid credentials"
1286 );
1287
1288 Ok(())
1289 }
1290
1291 #[test(tokio::test)]
1292 async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1293 let username = "user";
1294 let password = "password";
1295 let server = start_test_server(username, password).await;
1296
1297 let mut netrc_file = NamedTempFile::new()?;
1298 writeln!(
1299 netrc_file,
1300 r"machine example.com login {username} password {password}",
1301 )?;
1302
1303 let client = test_client_builder()
1304 .with(
1305 AuthMiddleware::new()
1306 .with_cache(CredentialsCache::new())
1307 .with_netrc(Some(
1308 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1309 )),
1310 )
1311 .build();
1312
1313 assert_eq!(
1314 client.get(server.uri()).send().await?.status(),
1315 401,
1316 "Credentials should not be pulled from the netrc file due to host mismatch"
1317 );
1318
1319 let mut url = Url::parse(&server.uri())?;
1320 url.set_username(username).unwrap();
1321 url.set_password(Some(password)).unwrap();
1322 assert_eq!(
1323 client.get(url).send().await?.status(),
1324 200,
1325 "Credentials in the URL should still work"
1326 );
1327
1328 Ok(())
1329 }
1330
1331 #[test(tokio::test)]
1332 async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1333 let username = "user";
1334 let password = "password";
1335 let server = start_test_server(username, password).await;
1336 let base_url = Url::parse(&server.uri())?;
1337
1338 let mut netrc_file = NamedTempFile::new()?;
1339 writeln!(
1340 netrc_file,
1341 r"machine {} login {username} password {password}",
1342 base_url.host_str().unwrap()
1343 )?;
1344
1345 let client = test_client_builder()
1346 .with(
1347 AuthMiddleware::new()
1348 .with_cache(CredentialsCache::new())
1349 .with_netrc(Some(
1350 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1351 )),
1352 )
1353 .build();
1354
1355 let mut url = base_url.clone();
1356 url.set_username("other-user").unwrap();
1357 assert_eq!(
1358 client.get(url).send().await?.status(),
1359 401,
1360 "The netrc password should not be used due to a username mismatch"
1361 );
1362
1363 let mut url = base_url.clone();
1364 url.set_username("user").unwrap();
1365 assert_eq!(
1366 client.get(url).send().await?.status(),
1367 200,
1368 "The netrc password should be used for a matching user"
1369 );
1370
1371 Ok(())
1372 }
1373
1374 #[test(tokio::test)]
1375 async fn test_keyring() -> Result<(), Error> {
1376 let username = "user";
1377 let password = "password";
1378 let server = start_test_server(username, password).await;
1379 let base_url = Url::parse(&server.uri())?;
1380
1381 let client = test_client_builder()
1382 .with(
1383 AuthMiddleware::new()
1384 .with_cache(CredentialsCache::new())
1385 .with_keyring(Some(KeyringProvider::dummy([(
1386 format!(
1387 "{}:{}",
1388 base_url.host_str().unwrap(),
1389 base_url.port().unwrap()
1390 ),
1391 username,
1392 password,
1393 )]))),
1394 )
1395 .build();
1396
1397 assert_eq!(
1398 client.get(server.uri()).send().await?.status(),
1399 401,
1400 "Credentials are not pulled from the keyring without a username"
1401 );
1402
1403 let mut url = base_url.clone();
1404 url.set_username(username).unwrap();
1405 assert_eq!(
1406 client.get(url).send().await?.status(),
1407 200,
1408 "Credentials for the username should be pulled from the keyring"
1409 );
1410
1411 let mut url = base_url.clone();
1412 url.set_username(username).unwrap();
1413 url.set_password(Some("invalid")).unwrap();
1414 assert_eq!(
1415 client.get(url).send().await?.status(),
1416 401,
1417 "Password in the URL should take precedence and fail"
1418 );
1419
1420 let mut url = base_url.clone();
1421 url.set_username(username).unwrap();
1422 assert_eq!(
1423 client.get(url.clone()).send().await?.status(),
1424 200,
1425 "Subsequent requests should not use the invalid password"
1426 );
1427
1428 let mut url = base_url.clone();
1429 url.set_username("other_user").unwrap();
1430 assert_eq!(
1431 client.get(url).send().await?.status(),
1432 401,
1433 "Credentials are not pulled from the keyring when given another username"
1434 );
1435
1436 Ok(())
1437 }
1438
1439 #[test(tokio::test)]
1440 async fn test_keyring_always_authenticate() -> Result<(), Error> {
1441 let username = "user";
1442 let password = "password";
1443 let server = start_test_server(username, password).await;
1444 let base_url = Url::parse(&server.uri())?;
1445
1446 let indexes = indexes_for(&base_url, AuthPolicy::Always);
1447 let client = test_client_builder()
1448 .with(
1449 AuthMiddleware::new()
1450 .with_cache(CredentialsCache::new())
1451 .with_keyring(Some(KeyringProvider::dummy([(
1452 format!(
1453 "{}:{}",
1454 base_url.host_str().unwrap(),
1455 base_url.port().unwrap()
1456 ),
1457 username,
1458 password,
1459 )])))
1460 .with_indexes(indexes),
1461 )
1462 .build();
1463
1464 assert_eq!(
1465 client.get(server.uri()).send().await?.status(),
1466 200,
1467 "Credentials (including a username) should be pulled from the keyring"
1468 );
1469
1470 let mut url = base_url.clone();
1471 url.set_username(username).unwrap();
1472 assert_eq!(
1473 client.get(url).send().await?.status(),
1474 200,
1475 "The password for the username should be pulled from the keyring"
1476 );
1477
1478 let mut url = base_url.clone();
1479 url.set_username(username).unwrap();
1480 url.set_password(Some("invalid")).unwrap();
1481 assert_eq!(
1482 client.get(url).send().await?.status(),
1483 401,
1484 "Password in the URL should take precedence and fail"
1485 );
1486
1487 let mut url = base_url.clone();
1488 url.set_username("other_user").unwrap();
1489 assert!(
1490 matches!(
1491 client.get(url).send().await,
1492 Err(reqwest_middleware::Error::Middleware(_))
1493 ),
1494 "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1495 );
1496
1497 Ok(())
1498 }
1499
1500 #[test(tokio::test)]
1505 async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1506 let username = "user";
1507 let password = "password";
1508 let server = start_test_server(username, password).await;
1509 let base_url = Url::parse(&server.uri())?;
1510
1511 let client = test_client_builder()
1512 .with(
1513 AuthMiddleware::new()
1514 .with_cache(CredentialsCache::new())
1515 .with_keyring(Some(KeyringProvider::dummy([(
1516 base_url.host_str().unwrap(),
1518 username,
1519 password,
1520 )]))),
1521 )
1522 .build();
1523
1524 let mut url = base_url.clone();
1525 url.set_username(username).unwrap();
1526 assert_eq!(
1527 client.get(url).send().await?.status(),
1528 401,
1529 "We should fail because the port is not present in the keyring entry"
1530 );
1531
1532 Ok(())
1533 }
1534
1535 #[test(tokio::test)]
1536 async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1537 let username = "user";
1538 let password = "password";
1539
1540 let server = start_test_server(username, password).await;
1541 let base_url = Url::parse(&server.uri())?;
1542 let cache = CredentialsCache::new();
1543
1544 cache.insert(
1547 DisplaySafeUrl::ref_cast(&base_url),
1548 Arc::new(Authentication::from(Credentials::basic(
1549 Some(username.to_string()),
1550 None,
1551 ))),
1552 );
1553 let client = test_client_builder()
1554 .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1555 KeyringProvider::dummy([(
1556 format!(
1557 "{}:{}",
1558 base_url.host_str().unwrap(),
1559 base_url.port().unwrap()
1560 ),
1561 username,
1562 password,
1563 )]),
1564 )))
1565 .build();
1566
1567 assert_eq!(
1568 client.get(server.uri()).send().await?.status(),
1569 200,
1570 "The username is pulled from the cache, and the password from the keyring"
1571 );
1572
1573 let mut url = base_url.clone();
1574 url.set_username(username).unwrap();
1575 assert_eq!(
1576 client.get(url).send().await?.status(),
1577 200,
1578 "Credentials for the username should be pulled from the keyring"
1579 );
1580
1581 Ok(())
1582 }
1583
1584 #[test(tokio::test)]
1585 async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1586 let username_1 = "user1";
1587 let password_1 = "password1";
1588 let server_1 = start_test_server(username_1, password_1).await;
1589 let base_url_1 = Url::parse(&server_1.uri())?;
1590
1591 let username_2 = "user2";
1592 let password_2 = "password2";
1593 let server_2 = start_test_server(username_2, password_2).await;
1594 let base_url_2 = Url::parse(&server_2.uri())?;
1595
1596 let cache = CredentialsCache::new();
1597 cache.insert(
1599 DisplaySafeUrl::ref_cast(&base_url_1),
1600 Arc::new(Authentication::from(Credentials::basic(
1601 Some(username_1.to_string()),
1602 Some(password_1.to_string()),
1603 ))),
1604 );
1605 cache.insert(
1606 DisplaySafeUrl::ref_cast(&base_url_2),
1607 Arc::new(Authentication::from(Credentials::basic(
1608 Some(username_2.to_string()),
1609 Some(password_2.to_string()),
1610 ))),
1611 );
1612
1613 let client = test_client_builder()
1614 .with(AuthMiddleware::new().with_cache(cache))
1615 .build();
1616
1617 assert_eq!(
1619 client.get(server_1.uri()).send().await?.status(),
1620 200,
1621 "Requests should not require credentials"
1622 );
1623 assert_eq!(
1624 client.get(server_2.uri()).send().await?.status(),
1625 200,
1626 "Requests should not require credentials"
1627 );
1628
1629 assert_eq!(
1630 client
1631 .get(format!("{}/foo", server_1.uri()))
1632 .send()
1633 .await?
1634 .status(),
1635 200,
1636 "Requests can be to different paths in the same realm"
1637 );
1638 assert_eq!(
1639 client
1640 .get(format!("{}/foo", server_2.uri()))
1641 .send()
1642 .await?
1643 .status(),
1644 200,
1645 "Requests can be to different paths in the same realm"
1646 );
1647
1648 Ok(())
1649 }
1650
1651 #[test(tokio::test)]
1652 async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1653 let username_1 = "user1";
1654 let password_1 = "password1";
1655 let server_1 = start_test_server(username_1, password_1).await;
1656 let base_url_1 = Url::parse(&server_1.uri())?;
1657
1658 let username_2 = "user2";
1659 let password_2 = "password2";
1660 let server_2 = start_test_server(username_2, password_2).await;
1661 let base_url_2 = Url::parse(&server_2.uri())?;
1662
1663 let client = test_client_builder()
1664 .with(
1665 AuthMiddleware::new()
1666 .with_cache(CredentialsCache::new())
1667 .with_keyring(Some(KeyringProvider::dummy([
1668 (
1669 format!(
1670 "{}:{}",
1671 base_url_1.host_str().unwrap(),
1672 base_url_1.port().unwrap()
1673 ),
1674 username_1,
1675 password_1,
1676 ),
1677 (
1678 format!(
1679 "{}:{}",
1680 base_url_2.host_str().unwrap(),
1681 base_url_2.port().unwrap()
1682 ),
1683 username_2,
1684 password_2,
1685 ),
1686 ]))),
1687 )
1688 .build();
1689
1690 assert_eq!(
1692 client.get(server_1.uri()).send().await?.status(),
1693 401,
1694 "Requests should require a username"
1695 );
1696 assert_eq!(
1697 client.get(server_2.uri()).send().await?.status(),
1698 401,
1699 "Requests should require a username"
1700 );
1701
1702 let mut url_1 = base_url_1.clone();
1703 url_1.set_username(username_1).unwrap();
1704 assert_eq!(
1705 client.get(url_1.clone()).send().await?.status(),
1706 200,
1707 "Requests with a username should succeed"
1708 );
1709 assert_eq!(
1710 client.get(server_2.uri()).send().await?.status(),
1711 401,
1712 "Credentials should not be re-used for the second server"
1713 );
1714
1715 let mut url_2 = base_url_2.clone();
1716 url_2.set_username(username_2).unwrap();
1717 assert_eq!(
1718 client.get(url_2.clone()).send().await?.status(),
1719 200,
1720 "Requests with a username should succeed"
1721 );
1722
1723 assert_eq!(
1724 client.get(format!("{url_1}/foo")).send().await?.status(),
1725 200,
1726 "Requests can be to different paths in the same realm"
1727 );
1728 assert_eq!(
1729 client.get(format!("{url_2}/foo")).send().await?.status(),
1730 200,
1731 "Requests can be to different paths in the same realm"
1732 );
1733
1734 Ok(())
1735 }
1736
1737 #[test(tokio::test)]
1738 async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1739 let username_1 = "user1";
1740 let password_1 = "password1";
1741 let username_2 = "user2";
1742 let password_2 = "password2";
1743
1744 let server = MockServer::start().await;
1745
1746 Mock::given(method("GET"))
1747 .and(path_regex("/prefix_1.*"))
1748 .and(basic_auth(username_1, password_1))
1749 .respond_with(ResponseTemplate::new(200))
1750 .mount(&server)
1751 .await;
1752
1753 Mock::given(method("GET"))
1754 .and(path_regex("/prefix_2.*"))
1755 .and(basic_auth(username_2, password_2))
1756 .respond_with(ResponseTemplate::new(200))
1757 .mount(&server)
1758 .await;
1759
1760 Mock::given(method("GET"))
1763 .and(path_regex("/prefix_3.*"))
1764 .and(basic_auth(username_1, password_1))
1765 .respond_with(ResponseTemplate::new(401))
1766 .mount(&server)
1767 .await;
1768 Mock::given(method("GET"))
1769 .and(path_regex("/prefix_3.*"))
1770 .and(basic_auth(username_2, password_2))
1771 .respond_with(ResponseTemplate::new(401))
1772 .mount(&server)
1773 .await;
1774 Mock::given(method("GET"))
1775 .and(path_regex("/prefix_3.*"))
1776 .respond_with(ResponseTemplate::new(200))
1777 .mount(&server)
1778 .await;
1779
1780 Mock::given(method("GET"))
1781 .respond_with(ResponseTemplate::new(401))
1782 .mount(&server)
1783 .await;
1784
1785 let base_url = Url::parse(&server.uri())?;
1786 let base_url_1 = base_url.join("prefix_1")?;
1787 let base_url_2 = base_url.join("prefix_2")?;
1788 let base_url_3 = base_url.join("prefix_3")?;
1789
1790 let cache = CredentialsCache::new();
1791
1792 cache.insert(
1794 DisplaySafeUrl::ref_cast(&base_url_1),
1795 Arc::new(Authentication::from(Credentials::basic(
1796 Some(username_1.to_string()),
1797 Some(password_1.to_string()),
1798 ))),
1799 );
1800 cache.insert(
1801 DisplaySafeUrl::ref_cast(&base_url_2),
1802 Arc::new(Authentication::from(Credentials::basic(
1803 Some(username_2.to_string()),
1804 Some(password_2.to_string()),
1805 ))),
1806 );
1807
1808 let client = test_client_builder()
1809 .with(AuthMiddleware::new().with_cache(cache))
1810 .build();
1811
1812 assert_eq!(
1814 client.get(base_url_1.clone()).send().await?.status(),
1815 200,
1816 "Requests should not require credentials"
1817 );
1818 assert_eq!(
1819 client.get(base_url_2.clone()).send().await?.status(),
1820 200,
1821 "Requests should not require credentials"
1822 );
1823 assert_eq!(
1824 client
1825 .get(base_url.join("prefix_1/foo")?)
1826 .send()
1827 .await?
1828 .status(),
1829 200,
1830 "Requests can be to different paths in the same realm"
1831 );
1832 assert_eq!(
1833 client
1834 .get(base_url.join("prefix_2/foo")?)
1835 .send()
1836 .await?
1837 .status(),
1838 200,
1839 "Requests can be to different paths in the same realm"
1840 );
1841 assert_eq!(
1842 client
1843 .get(base_url.join("prefix_1_foo")?)
1844 .send()
1845 .await?
1846 .status(),
1847 401,
1848 "Requests to paths with a matching prefix but different resource segments should fail"
1849 );
1850
1851 assert_eq!(
1852 client.get(base_url_3.clone()).send().await?.status(),
1853 200,
1854 "Requests to the 'public' prefix should not use credentials"
1855 );
1856
1857 Ok(())
1858 }
1859
1860 #[test(tokio::test)]
1861 async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1862 let username_1 = "user1";
1863 let password_1 = "password1";
1864 let username_2 = "user2";
1865 let password_2 = "password2";
1866
1867 let server = MockServer::start().await;
1868
1869 Mock::given(method("GET"))
1870 .and(path_regex("/prefix_1.*"))
1871 .and(basic_auth(username_1, password_1))
1872 .respond_with(ResponseTemplate::new(200))
1873 .mount(&server)
1874 .await;
1875
1876 Mock::given(method("GET"))
1877 .and(path_regex("/prefix_2.*"))
1878 .and(basic_auth(username_2, password_2))
1879 .respond_with(ResponseTemplate::new(200))
1880 .mount(&server)
1881 .await;
1882
1883 Mock::given(method("GET"))
1886 .and(path_regex("/prefix_3.*"))
1887 .and(basic_auth(username_1, password_1))
1888 .respond_with(ResponseTemplate::new(401))
1889 .mount(&server)
1890 .await;
1891 Mock::given(method("GET"))
1892 .and(path_regex("/prefix_3.*"))
1893 .and(basic_auth(username_2, password_2))
1894 .respond_with(ResponseTemplate::new(401))
1895 .mount(&server)
1896 .await;
1897 Mock::given(method("GET"))
1898 .and(path_regex("/prefix_3.*"))
1899 .respond_with(ResponseTemplate::new(200))
1900 .mount(&server)
1901 .await;
1902
1903 Mock::given(method("GET"))
1904 .respond_with(ResponseTemplate::new(401))
1905 .mount(&server)
1906 .await;
1907
1908 let base_url = Url::parse(&server.uri())?;
1909 let base_url_1 = base_url.join("prefix_1")?;
1910 let base_url_2 = base_url.join("prefix_2")?;
1911 let base_url_3 = base_url.join("prefix_3")?;
1912
1913 let client = test_client_builder()
1914 .with(
1915 AuthMiddleware::new()
1916 .with_cache(CredentialsCache::new())
1917 .with_keyring(Some(KeyringProvider::dummy([
1918 (
1919 format!(
1920 "{}:{}",
1921 base_url_1.host_str().unwrap(),
1922 base_url_1.port().unwrap()
1923 ),
1924 username_1,
1925 password_1,
1926 ),
1927 (
1928 format!(
1929 "{}:{}",
1930 base_url_2.host_str().unwrap(),
1931 base_url_2.port().unwrap()
1932 ),
1933 username_2,
1934 password_2,
1935 ),
1936 ]))),
1937 )
1938 .build();
1939
1940 assert_eq!(
1942 client.get(base_url_1.clone()).send().await?.status(),
1943 401,
1944 "Requests should require a username"
1945 );
1946 assert_eq!(
1947 client.get(base_url_2.clone()).send().await?.status(),
1948 401,
1949 "Requests should require a username"
1950 );
1951
1952 let mut url_1 = base_url_1.clone();
1953 url_1.set_username(username_1).unwrap();
1954 assert_eq!(
1955 client.get(url_1.clone()).send().await?.status(),
1956 200,
1957 "Requests with a username should succeed"
1958 );
1959 assert_eq!(
1960 client.get(base_url_2.clone()).send().await?.status(),
1961 401,
1962 "Credentials should not be re-used for the second prefix"
1963 );
1964
1965 let mut url_2 = base_url_2.clone();
1966 url_2.set_username(username_2).unwrap();
1967 assert_eq!(
1968 client.get(url_2.clone()).send().await?.status(),
1969 200,
1970 "Requests with a username should succeed"
1971 );
1972
1973 assert_eq!(
1974 client
1975 .get(base_url.join("prefix_1/foo")?)
1976 .send()
1977 .await?
1978 .status(),
1979 200,
1980 "Requests can be to different paths in the same prefix"
1981 );
1982 assert_eq!(
1983 client
1984 .get(base_url.join("prefix_2/foo")?)
1985 .send()
1986 .await?
1987 .status(),
1988 200,
1989 "Requests can be to different paths in the same prefix"
1990 );
1991 assert_eq!(
1992 client
1993 .get(base_url.join("prefix_1_foo")?)
1994 .send()
1995 .await?
1996 .status(),
1997 401,
1998 "Requests to paths with a matching prefix but different resource segments should fail"
1999 );
2000 assert_eq!(
2001 client.get(base_url_3.clone()).send().await?.status(),
2002 200,
2003 "Requests to the 'public' prefix should not use credentials"
2004 );
2005
2006 Ok(())
2007 }
2008
2009 #[test(tokio::test)]
2013 async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
2014 -> Result<(), Error> {
2015 let username = "user";
2016 let password_1 = "password1";
2017 let password_2 = "password2";
2018
2019 let server = MockServer::start().await;
2020
2021 Mock::given(method("GET"))
2022 .and(path_regex("/prefix_1.*"))
2023 .and(basic_auth(username, password_1))
2024 .respond_with(ResponseTemplate::new(200))
2025 .mount(&server)
2026 .await;
2027
2028 Mock::given(method("GET"))
2029 .and(path_regex("/prefix_2.*"))
2030 .and(basic_auth(username, password_2))
2031 .respond_with(ResponseTemplate::new(200))
2032 .mount(&server)
2033 .await;
2034
2035 Mock::given(method("GET"))
2036 .respond_with(ResponseTemplate::new(401))
2037 .mount(&server)
2038 .await;
2039
2040 let base_url = Url::parse(&server.uri())?;
2041 let base_url_1 = base_url.join("prefix_1")?;
2042 let base_url_2 = base_url.join("prefix_2")?;
2043
2044 let client = test_client_builder()
2045 .with(
2046 AuthMiddleware::new()
2047 .with_cache(CredentialsCache::new())
2048 .with_keyring(Some(KeyringProvider::dummy([
2049 (base_url_1.clone(), username, password_1),
2050 (base_url_2.clone(), username, password_2),
2051 ]))),
2052 )
2053 .build();
2054
2055 assert_eq!(
2057 client.get(base_url_1.clone()).send().await?.status(),
2058 401,
2059 "Requests should require a username"
2060 );
2061 assert_eq!(
2062 client.get(base_url_2.clone()).send().await?.status(),
2063 401,
2064 "Requests should require a username"
2065 );
2066
2067 let mut url_1 = base_url_1.clone();
2068 url_1.set_username(username).unwrap();
2069 assert_eq!(
2070 client.get(url_1.clone()).send().await?.status(),
2071 200,
2072 "The first request with a username will succeed"
2073 );
2074 assert_eq!(
2075 client.get(base_url_2.clone()).send().await?.status(),
2076 401,
2077 "Credentials should not be re-used for the second prefix"
2078 );
2079 assert_eq!(
2080 client
2081 .get(base_url.join("prefix_1/foo")?)
2082 .send()
2083 .await?
2084 .status(),
2085 200,
2086 "Subsequent requests can be to different paths in the same prefix"
2087 );
2088
2089 let mut url_2 = base_url_2.clone();
2090 url_2.set_username(username).unwrap();
2091 assert_eq!(
2092 client.get(url_2.clone()).send().await?.status(),
2093 401, "A request with the same username and realm for a URL that needs a different password will fail"
2095 );
2096 assert_eq!(
2097 client
2098 .get(base_url.join("prefix_2/foo")?)
2099 .send()
2100 .await?
2101 .status(),
2102 401, "Requests to other paths in the failing prefix will also fail"
2104 );
2105
2106 Ok(())
2107 }
2108
2109 #[test(tokio::test)]
2113 async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2114 -> Result<(), Error> {
2115 let username = "user";
2116 let password_1 = "password1";
2117 let password_2 = "password2";
2118
2119 let server = MockServer::start().await;
2120
2121 Mock::given(method("GET"))
2122 .and(path_regex("/prefix_1.*"))
2123 .and(basic_auth(username, password_1))
2124 .respond_with(ResponseTemplate::new(200))
2125 .mount(&server)
2126 .await;
2127
2128 Mock::given(method("GET"))
2129 .and(path_regex("/prefix_2.*"))
2130 .and(basic_auth(username, password_2))
2131 .respond_with(ResponseTemplate::new(200))
2132 .mount(&server)
2133 .await;
2134
2135 Mock::given(method("GET"))
2136 .respond_with(ResponseTemplate::new(401))
2137 .mount(&server)
2138 .await;
2139
2140 let base_url = Url::parse(&server.uri())?;
2141 let base_url_1 = base_url.join("prefix_1")?;
2142 let base_url_2 = base_url.join("prefix_2")?;
2143 let indexes = Indexes::from_indexes(vec![
2144 Index {
2145 url: DisplaySafeUrl::from_url(base_url_1.clone()),
2146 root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2147 auth_policy: AuthPolicy::Auto,
2148 },
2149 Index {
2150 url: DisplaySafeUrl::from_url(base_url_2.clone()),
2151 root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2152 auth_policy: AuthPolicy::Auto,
2153 },
2154 ]);
2155
2156 let client = test_client_builder()
2157 .with(
2158 AuthMiddleware::new()
2159 .with_cache(CredentialsCache::new())
2160 .with_keyring(Some(KeyringProvider::dummy([
2161 (base_url_1.clone(), username, password_1),
2162 (base_url_2.clone(), username, password_2),
2163 ])))
2164 .with_indexes(indexes),
2165 )
2166 .build();
2167
2168 assert_eq!(
2170 client.get(base_url_1.clone()).send().await?.status(),
2171 401,
2172 "Requests should require a username"
2173 );
2174 assert_eq!(
2175 client.get(base_url_2.clone()).send().await?.status(),
2176 401,
2177 "Requests should require a username"
2178 );
2179
2180 let mut url_1 = base_url_1.clone();
2181 url_1.set_username(username).unwrap();
2182 assert_eq!(
2183 client.get(url_1.clone()).send().await?.status(),
2184 200,
2185 "The first request with a username will succeed"
2186 );
2187 assert_eq!(
2188 client.get(base_url_2.clone()).send().await?.status(),
2189 401,
2190 "Credentials should not be re-used for the second prefix"
2191 );
2192 assert_eq!(
2193 client
2194 .get(base_url.join("prefix_1/foo")?)
2195 .send()
2196 .await?
2197 .status(),
2198 200,
2199 "Subsequent requests can be to different paths in the same prefix"
2200 );
2201
2202 let mut url_2 = base_url_2.clone();
2203 url_2.set_username(username).unwrap();
2204 assert_eq!(
2205 client.get(url_2.clone()).send().await?.status(),
2206 200,
2207 "A request with the same username and realm for a URL will use index-specific password"
2208 );
2209 assert_eq!(
2210 client
2211 .get(base_url.join("prefix_2/foo")?)
2212 .send()
2213 .await?
2214 .status(),
2215 200,
2216 "Requests to other paths with that prefix will also succeed"
2217 );
2218
2219 Ok(())
2220 }
2221
2222 #[test(tokio::test)]
2225 async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2226 -> Result<(), Error> {
2227 let username = "user";
2228 let password = "password";
2229
2230 let server = MockServer::start().await;
2231
2232 Mock::given(method("GET"))
2233 .and(basic_auth(username, password))
2234 .respond_with(ResponseTemplate::new(200))
2235 .mount(&server)
2236 .await;
2237
2238 Mock::given(method("GET"))
2239 .and(path_regex("/prefix_1.*"))
2240 .and(basic_auth(username, password))
2241 .respond_with(ResponseTemplate::new(200))
2242 .mount(&server)
2243 .await;
2244
2245 Mock::given(method("GET"))
2246 .respond_with(ResponseTemplate::new(401))
2247 .mount(&server)
2248 .await;
2249
2250 let base_url = Url::parse(&server.uri())?;
2251 let index_url = base_url.join("prefix_1")?;
2252 let indexes = Indexes::from_indexes(vec![Index {
2253 url: DisplaySafeUrl::from_url(index_url.clone()),
2254 root_url: DisplaySafeUrl::from_url(index_url.clone()),
2255 auth_policy: AuthPolicy::Auto,
2256 }]);
2257
2258 let client = test_client_builder()
2259 .with(
2260 AuthMiddleware::new()
2261 .with_cache(CredentialsCache::new())
2262 .with_keyring(Some(KeyringProvider::dummy([(
2263 base_url.clone(),
2264 username,
2265 password,
2266 )])))
2267 .with_indexes(indexes),
2268 )
2269 .build();
2270
2271 assert_eq!(
2273 client.get(index_url.clone()).send().await?.status(),
2274 401,
2275 "Requests should require a username"
2276 );
2277
2278 let mut realm_url = base_url.clone();
2280 realm_url.set_username(username).unwrap();
2281 assert_eq!(
2282 client.get(realm_url.clone()).send().await?.status(),
2283 200,
2284 "The first realm request with a username will succeed"
2285 );
2286
2287 let mut url = index_url.clone();
2288 url.set_username(username).unwrap();
2289 assert_eq!(
2290 client.get(url.clone()).send().await?.status(),
2291 200,
2292 "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2293 );
2294 assert_eq!(
2295 client
2296 .get(base_url.join("prefix_1/foo")?)
2297 .send()
2298 .await?
2299 .status(),
2300 200,
2301 "Requests to other paths with that prefix will also succeed"
2302 );
2303
2304 Ok(())
2305 }
2306
2307 fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2308 let mut url = DisplaySafeUrl::from_url(url.clone());
2309 url.set_password(None).ok();
2310 url.set_username("").ok();
2311 Indexes::from_indexes(vec![Index {
2312 url: url.clone(),
2313 root_url: url.clone(),
2314 auth_policy: policy,
2315 }])
2316 }
2317
2318 #[test(tokio::test)]
2321 async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2322 let username = "user";
2323 let password = "password";
2324
2325 let server = start_test_server(username, password).await;
2326
2327 let base_url = Url::parse(&server.uri())?;
2328
2329 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2330 let client = test_client_builder()
2331 .with(
2332 AuthMiddleware::new()
2333 .with_cache(CredentialsCache::new())
2334 .with_indexes(indexes),
2335 )
2336 .build();
2337
2338 Mock::given(method("GET"))
2339 .and(path_regex("/*"))
2340 .and(basic_auth(username, password))
2341 .respond_with(ResponseTemplate::new(200))
2342 .mount(&server)
2343 .await;
2344
2345 Mock::given(method("GET"))
2346 .respond_with(ResponseTemplate::new(401))
2347 .mount(&server)
2348 .await;
2349
2350 let mut url = base_url.clone();
2351 url.set_username(username).unwrap();
2352 url.set_password(Some(password)).unwrap();
2353 assert_eq!(client.get(url).send().await?.status(), 200);
2354
2355 assert_eq!(
2356 client
2357 .get(format!("{}/foo", server.uri()))
2358 .send()
2359 .await?
2360 .status(),
2361 200,
2362 "Requests can be to different paths with index URL as prefix"
2363 );
2364
2365 let mut url = base_url.clone();
2366 url.set_username(username).unwrap();
2367 url.set_password(Some("invalid")).unwrap();
2368 assert_eq!(
2369 client.get(url).send().await?.status(),
2370 401,
2371 "Incorrect credentials should fail"
2372 );
2373
2374 Ok(())
2375 }
2376
2377 #[test(tokio::test)]
2380 async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2381 let server = MockServer::start().await;
2382
2383 Mock::given(method("GET"))
2384 .and(path_regex("/*"))
2385 .respond_with(ResponseTemplate::new(200))
2386 .mount(&server)
2387 .await;
2388
2389 Mock::given(method("GET"))
2390 .respond_with(ResponseTemplate::new(401))
2391 .mount(&server)
2392 .await;
2393
2394 let base_url = Url::parse(&server.uri())?;
2395
2396 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2397 let client = test_client_builder()
2398 .with(
2399 AuthMiddleware::new()
2400 .with_cache(CredentialsCache::new())
2401 .with_indexes(indexes),
2402 )
2403 .build();
2404
2405 assert!(matches!(
2407 client.get(server.uri()).send().await,
2408 Err(reqwest_middleware::Error::Middleware(_))
2409 ));
2410
2411 Ok(())
2412 }
2413
2414 #[test(tokio::test)]
2417 async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2418 let username = "user";
2419 let password = "password";
2420
2421 let server = start_test_server(username, password).await;
2422 let base_url = Url::parse(&server.uri())?;
2423
2424 Mock::given(method("GET"))
2425 .and(path_regex("/*"))
2426 .and(basic_auth(username, password))
2427 .respond_with(ResponseTemplate::new(200))
2428 .mount(&server)
2429 .await;
2430
2431 Mock::given(method("GET"))
2432 .respond_with(ResponseTemplate::new(401))
2433 .mount(&server)
2434 .await;
2435
2436 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2437 let client = test_client_builder()
2438 .with(
2439 AuthMiddleware::new()
2440 .with_cache(CredentialsCache::new())
2441 .with_indexes(indexes),
2442 )
2443 .build();
2444
2445 let mut url = base_url.clone();
2446 url.set_username(username).unwrap();
2447 url.set_password(Some(password)).unwrap();
2448
2449 assert_eq!(
2450 client
2451 .get(format!("{}/foo", server.uri()))
2452 .send()
2453 .await?
2454 .status(),
2455 401,
2456 "Requests should not be completed if credentials are required"
2457 );
2458
2459 Ok(())
2460 }
2461
2462 #[test(tokio::test)]
2465 async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2466 let server = MockServer::start().await;
2467
2468 Mock::given(method("GET"))
2469 .and(path_regex("/*"))
2470 .respond_with(ResponseTemplate::new(200))
2471 .mount(&server)
2472 .await;
2473
2474 Mock::given(method("GET"))
2475 .respond_with(ResponseTemplate::new(401))
2476 .mount(&server)
2477 .await;
2478
2479 let base_url = Url::parse(&server.uri())?;
2480
2481 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2482 let client = test_client_builder()
2483 .with(
2484 AuthMiddleware::new()
2485 .with_cache(CredentialsCache::new())
2486 .with_indexes(indexes),
2487 )
2488 .build();
2489
2490 assert_eq!(
2491 client.get(server.uri()).send().await?.status(),
2492 200,
2493 "Requests should succeed if unauthenticated requests can succeed"
2494 );
2495
2496 Ok(())
2497 }
2498
2499 #[test]
2500 fn test_tracing_url() {
2501 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2503 assert_eq!(
2504 tracing_url(&req, None),
2505 DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2506 );
2507
2508 let creds = Authentication::from(Credentials::Basic {
2509 username: Username::new(Some(String::from("user"))),
2510 password: None,
2511 });
2512 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2513 assert_eq!(
2514 tracing_url(&req, Some(&creds)),
2515 DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2516 );
2517
2518 let creds = Authentication::from(Credentials::Basic {
2519 username: Username::new(Some(String::from("user"))),
2520 password: Some(Password::new(String::from("password"))),
2521 });
2522 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2523 assert_eq!(
2524 tracing_url(&req, Some(&creds)),
2525 DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2526 .unwrap()
2527 );
2528 }
2529
2530 #[test(tokio::test)]
2531 async fn test_text_store_basic_auth() -> Result<(), Error> {
2532 let username = "user";
2533 let password = "password";
2534
2535 let server = start_test_server(username, password).await;
2536 let base_url = Url::parse(&server.uri())?;
2537
2538 let mut store = TextCredentialStore::default();
2540 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2541 let credentials =
2542 Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2543 store.insert(service.clone(), credentials);
2544
2545 let client = test_client_builder()
2546 .with(
2547 AuthMiddleware::new()
2548 .with_cache(CredentialsCache::new())
2549 .with_text_store(Some(store)),
2550 )
2551 .build();
2552
2553 assert_eq!(
2554 client.get(server.uri()).send().await?.status(),
2555 200,
2556 "Credentials should be pulled from the text store"
2557 );
2558
2559 Ok(())
2560 }
2561
2562 #[test(tokio::test)]
2563 async fn test_text_store_disabled() -> Result<(), Error> {
2564 let username = "user";
2565 let password = "password";
2566 let server = start_test_server(username, password).await;
2567
2568 let client = test_client_builder()
2569 .with(
2570 AuthMiddleware::new()
2571 .with_cache(CredentialsCache::new())
2572 .with_text_store(None), )
2574 .build();
2575
2576 assert_eq!(
2577 client.get(server.uri()).send().await?.status(),
2578 401,
2579 "Credentials should not be found when text store is disabled"
2580 );
2581
2582 Ok(())
2583 }
2584
2585 #[test(tokio::test)]
2586 async fn test_text_store_by_username() -> Result<(), Error> {
2587 let username = "testuser";
2588 let password = "testpass";
2589 let wrong_username = "wronguser";
2590
2591 let server = start_test_server(username, password).await;
2592 let base_url = Url::parse(&server.uri())?;
2593
2594 let mut store = TextCredentialStore::default();
2595 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2596 let credentials =
2597 crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2598 store.insert(service.clone(), credentials);
2599
2600 let client = test_client_builder()
2601 .with(
2602 AuthMiddleware::new()
2603 .with_cache(CredentialsCache::new())
2604 .with_text_store(Some(store)),
2605 )
2606 .build();
2607
2608 let url_with_username = format!(
2610 "{}://{}@{}",
2611 base_url.scheme(),
2612 username,
2613 base_url.host_str().unwrap()
2614 );
2615 let url_with_port = if let Some(port) = base_url.port() {
2616 format!("{}:{}{}", url_with_username, port, base_url.path())
2617 } else {
2618 format!("{}{}", url_with_username, base_url.path())
2619 };
2620
2621 assert_eq!(
2622 client.get(&url_with_port).send().await?.status(),
2623 200,
2624 "Request with matching username should succeed"
2625 );
2626
2627 let url_with_wrong_username = format!(
2629 "{}://{}@{}",
2630 base_url.scheme(),
2631 wrong_username,
2632 base_url.host_str().unwrap()
2633 );
2634 let url_with_port = if let Some(port) = base_url.port() {
2635 format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2636 } else {
2637 format!("{}{}", url_with_wrong_username, base_url.path())
2638 };
2639
2640 assert_eq!(
2641 client.get(&url_with_port).send().await?.status(),
2642 401,
2643 "Request with non-matching username should fail"
2644 );
2645
2646 assert_eq!(
2648 client.get(server.uri()).send().await?.status(),
2649 200,
2650 "Request with no username should succeed"
2651 );
2652
2653 Ok(())
2654 }
2655
2656 fn create_request(url: &str) -> Request {
2657 Request::new(Method::GET, Url::parse(url).unwrap())
2658 }
2659
2660 #[test(tokio::test)]
2665 async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2666 let username = "";
2667 let password = "token";
2668
2669 let server = MockServer::start().await;
2670
2671 Mock::given(method("GET"))
2672 .and(basic_auth(username, password))
2673 .respond_with(ResponseTemplate::new(200))
2674 .mount(&server)
2675 .await;
2676
2677 Mock::given(method("GET"))
2678 .respond_with(ResponseTemplate::new(401))
2679 .mount(&server)
2680 .await;
2681
2682 let client = test_client_builder()
2683 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2684 .build();
2685
2686 let base_url = Url::parse(&server.uri())?;
2687
2688 let mut url = base_url.clone();
2690 url.set_password(Some(password)).unwrap();
2691 assert_eq!(
2692 client.get(url).send().await?.status(),
2693 200,
2694 "URL with empty username but password should authenticate successfully"
2695 );
2696
2697 assert_eq!(
2699 client.get(server.uri()).send().await?.status(),
2700 200,
2701 "Subsequent requests should use cached credentials"
2702 );
2703
2704 assert_eq!(
2705 client
2706 .get(format!("{}/foo", server.uri()))
2707 .send()
2708 .await?
2709 .status(),
2710 200,
2711 "Requests to different paths in the same realm should succeed"
2712 );
2713
2714 Ok(())
2715 }
2716}