Skip to main content

uv_auth/
middleware.rs

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