Skip to main content

uv_auth/
credentials.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::io::Read;
4use std::io::Write;
5use std::str::{FromStr, Utf8Error};
6
7use base64::prelude::BASE64_STANDARD;
8use base64::read::DecoderReader;
9use base64::write::EncoderWriter;
10use http::Uri;
11use reqsign::aws::DefaultSigner as AwsDefaultSigner;
12use reqsign::azure::DefaultSigner as AzureDefaultSigner;
13use reqsign::google::DefaultSigner as GcsDefaultSigner;
14use reqwest::Request;
15use reqwest::header::{HeaderValue, InvalidHeaderValue};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use url::Url;
19
20use uv_netrc::Netrc;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum Credentials {
26    /// RFC 7617 HTTP Basic Authentication
27    Basic {
28        /// The username to use for authentication.
29        username: Username,
30        /// The password to use for authentication.
31        password: Option<Password>,
32    },
33    /// RFC 6750 Bearer Token Authentication
34    Bearer {
35        /// The token to use for authentication.
36        token: Token,
37    },
38}
39
40#[derive(Debug, Error)]
41pub enum CredentialsFromUrlError {
42    #[error("URL username contains invalid UTF-8")]
43    InvalidUsernameUtf8(#[source] Utf8Error),
44    #[error("URL password contains invalid UTF-8")]
45    InvalidPasswordUtf8(#[source] Utf8Error),
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
49#[serde(transparent)]
50pub struct Username(Option<String>);
51
52impl Username {
53    /// Create a new username.
54    ///
55    /// Unlike `reqwest`, empty usernames are be encoded as `None` instead of an empty string.
56    pub(crate) fn new(value: Option<String>) -> Self {
57        // Ensure empty strings are `None`
58        Self(value.filter(|s| !s.is_empty()))
59    }
60
61    pub(crate) fn none() -> Self {
62        Self::new(None)
63    }
64
65    pub(crate) fn is_none(&self) -> bool {
66        self.0.is_none()
67    }
68
69    pub(crate) fn is_some(&self) -> bool {
70        self.0.is_some()
71    }
72
73    pub(crate) fn as_deref(&self) -> Option<&str> {
74        self.0.as_deref()
75    }
76}
77
78impl From<String> for Username {
79    fn from(value: String) -> Self {
80        Self::new(Some(value))
81    }
82}
83
84impl From<Option<String>> for Username {
85    fn from(value: Option<String>) -> Self {
86        Self::new(value)
87    }
88}
89
90#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
91#[serde(transparent)]
92pub struct Password(String);
93
94impl Password {
95    pub fn new(password: String) -> Self {
96        Self(password)
97    }
98
99    /// Return the [`Password`] as a string slice.
100    fn as_str(&self) -> &str {
101        self.0.as_str()
102    }
103}
104
105impl fmt::Debug for Password {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "****")
108    }
109}
110
111#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
112#[serde(transparent)]
113pub struct Token(Vec<u8>);
114
115impl Token {
116    pub(crate) fn new(token: Vec<u8>) -> Self {
117        Self(token)
118    }
119
120    /// Return the [`Token`] as a byte slice.
121    fn as_slice(&self) -> &[u8] {
122        self.0.as_slice()
123    }
124
125    /// Convert the [`Token`] into its underlying [`Vec<u8>`].
126    pub(crate) fn into_bytes(self) -> Vec<u8> {
127        self.0
128    }
129
130    /// Return whether the [`Token`] is empty.
131    fn is_empty(&self) -> bool {
132        self.0.is_empty()
133    }
134}
135
136impl fmt::Debug for Token {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "****")
139    }
140}
141impl Credentials {
142    /// Create a set of HTTP Basic Authentication credentials.
143    #[allow(dead_code)]
144    pub fn basic(username: Option<String>, password: Option<String>) -> Self {
145        Self::Basic {
146            username: Username::new(username),
147            password: password.map(Password),
148        }
149    }
150
151    /// Create a set of Bearer Authentication credentials.
152    #[allow(dead_code)]
153    pub fn bearer(token: Vec<u8>) -> Self {
154        Self::Bearer {
155            token: Token::new(token),
156        }
157    }
158
159    pub fn username(&self) -> Option<&str> {
160        match self {
161            Self::Basic { username, .. } => username.as_deref(),
162            Self::Bearer { .. } => None,
163        }
164    }
165
166    fn to_username(&self) -> Username {
167        match self {
168            Self::Basic { username, .. } => username.clone(),
169            Self::Bearer { .. } => Username::none(),
170        }
171    }
172
173    fn as_username(&self) -> Cow<'_, Username> {
174        match self {
175            Self::Basic { username, .. } => Cow::Borrowed(username),
176            Self::Bearer { .. } => Cow::Owned(Username::none()),
177        }
178    }
179
180    pub fn password(&self) -> Option<&str> {
181        match self {
182            Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
183            Self::Bearer { .. } => None,
184        }
185    }
186
187    fn is_authenticated(&self) -> bool {
188        match self {
189            Self::Basic {
190                username: _,
191                password,
192            } => password.is_some(),
193            Self::Bearer { token } => !token.is_empty(),
194        }
195    }
196
197    fn is_empty(&self) -> bool {
198        match self {
199            Self::Basic { username, password } => username.is_none() && password.is_none(),
200            Self::Bearer { token } => token.is_empty(),
201        }
202    }
203
204    /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any.
205    ///
206    /// If a username is provided, it must match the login in the netrc file or [`None`] is returned.
207    pub(crate) fn from_netrc(
208        netrc: &Netrc,
209        url: &DisplaySafeUrl,
210        username: Option<&str>,
211    ) -> Option<Self> {
212        let host = url.host_str()?;
213        let entry = netrc
214            .hosts
215            .get(host)
216            .or_else(|| netrc.hosts.get("default"))?;
217
218        // Ensure the username matches if provided
219        if username.is_some_and(|username| username != entry.login) {
220            return None;
221        }
222
223        Some(Self::Basic {
224            username: Username::new(Some(entry.login.clone())),
225            password: Some(Password(entry.password.clone())),
226        })
227    }
228
229    /// Parse [`Credentials`] from a URL, if any.
230    ///
231    /// Returns [`None`] if both [`Url::username`] and [`Url::password`] are not populated.
232    pub fn from_url(url: &Url) -> Result<Option<Self>, CredentialsFromUrlError> {
233        if url.username().is_empty() && url.password().is_none() {
234            return Ok(None);
235        }
236
237        // Remove percent-encoding from URL credentials.
238        // See <https://github.com/pypa/pip/blob/06d21db4ff1ab69665c22a88718a4ea9757ca293/src/pip/_internal/utils/misc.py#L497-L499>
239        let username = if url.username().is_empty() {
240            None
241        } else {
242            Some(
243                percent_encoding::percent_decode_str(url.username())
244                    .decode_utf8()
245                    .map_err(CredentialsFromUrlError::InvalidUsernameUtf8)?
246                    .into_owned(),
247            )
248        };
249        let password = url
250            .password()
251            .map(|password| {
252                percent_encoding::percent_decode_str(password)
253                    .decode_utf8()
254                    .map(|password| Password(password.into_owned()))
255                    .map_err(CredentialsFromUrlError::InvalidPasswordUtf8)
256            })
257            .transpose()?;
258
259        Ok(Some(Self::Basic {
260            username: username.into(),
261            password,
262        }))
263    }
264
265    /// Extract the [`Credentials`] from the environment, given a named source.
266    ///
267    /// For example, given a name of `"pytorch"`, search for `UV_INDEX_PYTORCH_USERNAME` and
268    /// `UV_INDEX_PYTORCH_PASSWORD`.
269    pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
270        let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
271        let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
272        if username.is_none() && password.is_none() {
273            None
274        } else {
275            Some(Self::basic(username, password))
276        }
277    }
278
279    /// Parse [`Credentials`] from an HTTP request, if any.
280    ///
281    /// Only HTTP Basic Authentication is supported.
282    pub(crate) fn from_request(request: &Request) -> Result<Option<Self>, CredentialsFromUrlError> {
283        // First, attempt to retrieve the credentials from the URL
284        if let Some(credentials) = Self::from_url(request.url())? {
285            return Ok(Some(credentials));
286        }
287
288        // Then, attempt to pull the credentials from the headers
289        Ok(request
290            .headers()
291            .get(reqwest::header::AUTHORIZATION)
292            .and_then(Self::from_header_value))
293    }
294
295    /// Parse [`Credentials`] from an authorization header, if any.
296    ///
297    /// HTTP Basic and Bearer Authentication are both supported.
298    /// [`None`] will be returned if another authorization scheme is detected.
299    ///
300    /// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme:
301    /// - The contents must be base64 encoded
302    /// - There must be a `:` separator
303    fn from_header_value(header: &HeaderValue) -> Option<Self> {
304        // Parse a `Basic` authentication header.
305        if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
306            let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
307            let mut buf = String::new();
308            decoder
309                .read_to_string(&mut buf)
310                .expect("HTTP Basic Authentication should be base64 encoded");
311            let (username, password) = buf
312                .split_once(':')
313                .expect("HTTP Basic Authentication should include a `:` separator");
314            let username = if username.is_empty() {
315                None
316            } else {
317                Some(username.to_string())
318            };
319            let password = if password.is_empty() {
320                None
321            } else {
322                Some(password.to_string())
323            };
324            return Some(Self::Basic {
325                username: Username::new(username),
326                password: password.map(Password),
327            });
328        }
329
330        // Parse a `Bearer` authentication header.
331        if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
332            return Some(Self::Bearer {
333                token: Token::new(token.to_vec()),
334            });
335        }
336
337        None
338    }
339
340    /// Create an HTTP authorization header for the credentials.
341    ///
342    /// Returns an error if the bearer token contains invalid header characters.
343    pub fn to_header_value(&self) -> Result<HeaderValue, InvalidHeaderValue> {
344        let header_bytes = match self {
345            Self::Basic { .. } => {
346                // See: <https://github.com/seanmonstar/reqwest/blob/2c11ef000b151c2eebeed2c18a7b81042220c6b0/src/util.rs#L3>
347                let mut buf = b"Basic ".to_vec();
348                {
349                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
350                    write!(encoder, "{}:", self.username().unwrap_or_default())
351                        .expect("Write to base64 encoder should succeed");
352                    if let Some(password) = self.password() {
353                        write!(encoder, "{password}")
354                            .expect("Write to base64 encoder should succeed");
355                    }
356                }
357                buf
358            }
359            Self::Bearer { token } => [b"Bearer ", token.as_slice()].concat(),
360        };
361        let mut header = HeaderValue::from_bytes(&header_bytes)?;
362        header.set_sensitive(true);
363        Ok(header)
364    }
365
366    /// Apply the credentials to the given URL.
367    ///
368    /// Any existing credentials will be overridden.
369    #[must_use]
370    pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
371        if let Some(username) = self.username() {
372            let _ = url.set_username(username);
373        }
374        if let Some(password) = self.password() {
375            let _ = url.set_password(Some(password));
376        }
377        url
378    }
379
380    /// Attach the credentials to the given request.
381    ///
382    /// Any existing credentials will be overridden.
383    fn authenticate(&self, mut request: Request) -> Result<Request, InvalidHeaderValue> {
384        request
385            .headers_mut()
386            .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self)?);
387        Ok(request)
388    }
389}
390
391#[derive(Clone, Debug)]
392pub(crate) enum Authentication {
393    /// HTTP Basic or Bearer Authentication credentials.
394    Credentials(Credentials),
395
396    /// AWS Signature Version 4 signing.
397    AwsSigner(AwsDefaultSigner),
398
399    /// Google Cloud signing.
400    GcsSigner(GcsDefaultSigner),
401
402    /// Azure Storage signing.
403    AzureSigner(AzureDefaultSigner),
404}
405
406#[derive(Debug, Error)]
407pub(crate) enum AuthenticationError {
408    #[error("Invalid authorization header")]
409    InvalidHeaderValue(#[from] InvalidHeaderValue),
410
411    #[error("Failed to convert request URL to URI")]
412    InvalidUri(#[from] http::uri::InvalidUri),
413
414    #[error("Failed to build request for {provider} signing")]
415    BuildRequest {
416        provider: &'static str,
417        #[source]
418        source: http::Error,
419    },
420
421    #[error("Failed to sign request with {provider} credentials")]
422    Sign {
423        provider: &'static str,
424        #[source]
425        source: reqsign::Error,
426    },
427}
428
429impl PartialEq for Authentication {
430    fn eq(&self, other: &Self) -> bool {
431        match (self, other) {
432            (Self::Credentials(a), Self::Credentials(b)) => a == b,
433            (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
434            (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
435            (Self::AzureSigner(..), Self::AzureSigner(..)) => true,
436            _ => false,
437        }
438    }
439}
440
441impl Eq for Authentication {}
442
443impl From<Credentials> for Authentication {
444    fn from(credentials: Credentials) -> Self {
445        Self::Credentials(credentials)
446    }
447}
448
449impl From<AwsDefaultSigner> for Authentication {
450    fn from(signer: AwsDefaultSigner) -> Self {
451        Self::AwsSigner(signer)
452    }
453}
454
455impl From<GcsDefaultSigner> for Authentication {
456    fn from(signer: GcsDefaultSigner) -> Self {
457        Self::GcsSigner(signer)
458    }
459}
460
461impl From<AzureDefaultSigner> for Authentication {
462    fn from(signer: AzureDefaultSigner) -> Self {
463        Self::AzureSigner(signer)
464    }
465}
466
467impl Authentication {
468    /// Return the password used for authentication, if any.
469    pub(crate) fn password(&self) -> Option<&str> {
470        match self {
471            Self::Credentials(credentials) => credentials.password(),
472            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
473        }
474    }
475
476    /// Return the username used for authentication, if any.
477    pub(crate) fn username(&self) -> Option<&str> {
478        match self {
479            Self::Credentials(credentials) => credentials.username(),
480            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
481        }
482    }
483
484    /// Return the username used for authentication, if any.
485    pub(crate) fn as_username(&self) -> Cow<'_, Username> {
486        match self {
487            Self::Credentials(credentials) => credentials.as_username(),
488            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => {
489                Cow::Owned(Username::none())
490            }
491        }
492    }
493
494    /// Return the username used for authentication, if any.
495    pub(crate) fn to_username(&self) -> Username {
496        match self {
497            Self::Credentials(credentials) => credentials.to_username(),
498            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(),
499        }
500    }
501
502    /// Return `true` if the object contains a means of authenticating.
503    pub(crate) fn is_authenticated(&self) -> bool {
504        match self {
505            Self::Credentials(credentials) => credentials.is_authenticated(),
506            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true,
507        }
508    }
509
510    /// Return `true` if the object contains no credentials.
511    pub(crate) fn is_empty(&self) -> bool {
512        match self {
513            Self::Credentials(credentials) => credentials.is_empty(),
514            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false,
515        }
516    }
517
518    /// Apply the authentication to the given request.
519    ///
520    /// Any existing credentials will be overridden.
521    pub(crate) async fn authenticate(
522        &self,
523        mut request: Request,
524    ) -> Result<Request, AuthenticationError> {
525        match self {
526            Self::Credentials(credentials) => Ok(credentials.authenticate(request)?),
527            Self::AwsSigner(signer) => {
528                // Build an `http::Request` from the `reqwest::Request`.
529                let uri = Uri::from_str(request.url().as_str())?;
530                let mut http_req = http::Request::builder()
531                    .method(request.method().clone())
532                    .uri(uri)
533                    .body(())
534                    .map_err(|source| AuthenticationError::BuildRequest {
535                        provider: "AWS",
536                        source,
537                    })?;
538                *http_req.headers_mut() = request.headers().clone();
539
540                // Sign the parts.
541                let (mut parts, ()) = http_req.into_parts();
542                signer.sign(&mut parts, None).await.map_err(|source| {
543                    AuthenticationError::Sign {
544                        provider: "AWS",
545                        source,
546                    }
547                })?;
548
549                // Copy over the signed headers.
550                request.headers_mut().extend(parts.headers);
551
552                // Copy over the signed path and query, if any.
553                if let Some(path_and_query) = parts.uri.path_and_query() {
554                    request.url_mut().set_path(path_and_query.path());
555                    request.url_mut().set_query(path_and_query.query());
556                }
557                Ok(request)
558            }
559            Self::GcsSigner(signer) => {
560                // Build an `http::Request` from the `reqwest::Request`.
561                let uri = Uri::from_str(request.url().as_str())?;
562                let mut http_req = http::Request::builder()
563                    .method(request.method().clone())
564                    .uri(uri)
565                    .body(())
566                    .map_err(|source| AuthenticationError::BuildRequest {
567                        provider: "GCS",
568                        source,
569                    })?;
570                *http_req.headers_mut() = request.headers().clone();
571
572                // Sign the parts.
573                let (mut parts, ()) = http_req.into_parts();
574                signer.sign(&mut parts, None).await.map_err(|source| {
575                    AuthenticationError::Sign {
576                        provider: "GCS",
577                        source,
578                    }
579                })?;
580
581                // Copy over the signed headers.
582                request.headers_mut().extend(parts.headers);
583
584                // Copy over the signed path and query, if any.
585                if let Some(path_and_query) = parts.uri.path_and_query() {
586                    request.url_mut().set_path(path_and_query.path());
587                    request.url_mut().set_query(path_and_query.query());
588                }
589                Ok(request)
590            }
591            Self::AzureSigner(signer) => {
592                // Build an `http::Request` from the `reqwest::Request`.
593                let uri = Uri::from_str(request.url().as_str())?;
594                let mut http_req = http::Request::builder()
595                    .method(request.method().clone())
596                    .uri(uri)
597                    .body(())
598                    .map_err(|source| AuthenticationError::BuildRequest {
599                        provider: "Azure",
600                        source,
601                    })?;
602                *http_req.headers_mut() = request.headers().clone();
603
604                // Sign the parts.
605                let (mut parts, ()) = http_req.into_parts();
606                signer.sign(&mut parts, None).await.map_err(|source| {
607                    AuthenticationError::Sign {
608                        provider: "Azure",
609                        source,
610                    }
611                })?;
612
613                // Copy over the signed headers.
614                request.headers_mut().extend(parts.headers);
615
616                // Copy over the signed path and query, if any.
617                if let Some(path_and_query) = parts.uri.path_and_query() {
618                    request.url_mut().set_path(path_and_query.path());
619                    request.url_mut().set_query(path_and_query.query());
620                }
621                Ok(request)
622            }
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use std::assert_matches;
630    use std::future::{self, Future};
631
632    use insta::{assert_debug_snapshot, assert_snapshot};
633    use reqsign::aws::Credential as AwsCredential;
634    use reqsign::azure::Credential as AzureCredential;
635    use reqsign::{Context, ProvideCredential};
636
637    use super::*;
638
639    #[derive(Debug)]
640    struct EmptyAwsCredentialProvider;
641
642    impl ProvideCredential for EmptyAwsCredentialProvider {
643        type Credential = AwsCredential;
644
645        fn provide_credential(
646            &self,
647            _ctx: &Context,
648        ) -> impl Future<Output = reqsign::Result<Option<Self::Credential>>> {
649            future::ready(Ok(None))
650        }
651    }
652
653    #[derive(Debug)]
654    struct EmptyAzureCredentialProvider;
655
656    impl ProvideCredential for EmptyAzureCredentialProvider {
657        type Credential = AzureCredential;
658
659        fn provide_credential(
660            &self,
661            _ctx: &Context,
662        ) -> impl Future<Output = reqsign::Result<Option<Self::Credential>>> {
663            future::ready(Ok(None))
664        }
665    }
666
667    #[test]
668    fn from_url_no_credentials() {
669        let url = &Url::parse("https://example.com/simple/first/").unwrap();
670        assert_matches!(Credentials::from_url(url), Ok(None));
671    }
672
673    #[test]
674    fn from_url_username_and_password() {
675        let url = &Url::parse("https://example.com/simple/first/").unwrap();
676        let mut auth_url = url.clone();
677        auth_url.set_username("user").unwrap();
678        auth_url.set_password(Some("password")).unwrap();
679        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
680        assert_eq!(credentials.username(), Some("user"));
681        assert_eq!(credentials.password(), Some("password"));
682    }
683
684    #[test]
685    fn from_url_invalid_utf8_username() {
686        let url = Url::parse("https://%FF:password@example.com/simple/first/").unwrap();
687        let error = Credentials::from_url(&url).unwrap_err();
688        assert_snapshot!(error, @"URL username contains invalid UTF-8");
689    }
690
691    #[test]
692    fn from_url_invalid_utf8_password() {
693        let url = Url::parse("https://user:%FF@example.com/simple/first/").unwrap();
694        let error = Credentials::from_url(&url).unwrap_err();
695        assert_snapshot!(error, @"URL password contains invalid UTF-8");
696    }
697
698    #[test]
699    fn from_url_no_username() {
700        let url = &Url::parse("https://example.com/simple/first/").unwrap();
701        let mut auth_url = url.clone();
702        auth_url.set_password(Some("password")).unwrap();
703        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
704        assert_eq!(credentials.username(), None);
705        assert_eq!(credentials.password(), Some("password"));
706    }
707
708    /// Test for <https://github.com/astral-sh/uv/issues/17343>
709    ///
710    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
711    /// should be recognized as having credentials.
712    #[test]
713    fn from_url_empty_username_with_password() {
714        // Parse a URL with the format `:password@host` directly
715        let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
716        let credentials = Credentials::from_url(&url).unwrap().unwrap();
717        assert_eq!(credentials.username(), None);
718        assert_eq!(credentials.password(), Some("token"));
719        assert!(
720            credentials.is_authenticated(),
721            "URL with empty username but password should be considered authenticated"
722        );
723    }
724
725    #[test]
726    fn from_url_no_password() {
727        let url = &Url::parse("https://example.com/simple/first/").unwrap();
728        let mut auth_url = url.clone();
729        auth_url.set_username("user").unwrap();
730        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
731        assert_eq!(credentials.username(), Some("user"));
732        assert_eq!(credentials.password(), None);
733    }
734
735    #[test]
736    fn authenticated_request_from_url() {
737        let url = Url::parse("https://example.com/simple/first/").unwrap();
738        let mut auth_url = url.clone();
739        auth_url.set_username("user").unwrap();
740        auth_url.set_password(Some("password")).unwrap();
741        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
742
743        let mut request = Request::new(reqwest::Method::GET, url);
744        request = credentials.authenticate(request).unwrap();
745
746        let mut header = request
747            .headers()
748            .get(reqwest::header::AUTHORIZATION)
749            .expect("Authorization header should be set")
750            .clone();
751        header.set_sensitive(false);
752
753        assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
754        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
755    }
756
757    #[test]
758    fn authenticated_request_from_url_with_percent_encoded_user() {
759        let url = Url::parse("https://example.com/simple/first/").unwrap();
760        let mut auth_url = url.clone();
761        auth_url.set_username("user@domain").unwrap();
762        auth_url.set_password(Some("password")).unwrap();
763        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
764
765        let mut request = Request::new(reqwest::Method::GET, url);
766        request = credentials.authenticate(request).unwrap();
767
768        let mut header = request
769            .headers()
770            .get(reqwest::header::AUTHORIZATION)
771            .expect("Authorization header should be set")
772            .clone();
773        header.set_sensitive(false);
774
775        assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
776        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
777    }
778
779    #[test]
780    fn authenticated_request_from_url_with_percent_encoded_password() {
781        let url = Url::parse("https://example.com/simple/first/").unwrap();
782        let mut auth_url = url.clone();
783        auth_url.set_username("user").unwrap();
784        auth_url.set_password(Some("password==")).unwrap();
785        let credentials = Credentials::from_url(&auth_url).unwrap().unwrap();
786
787        let mut request = Request::new(reqwest::Method::GET, url);
788        request = credentials.authenticate(request).unwrap();
789
790        let mut header = request
791            .headers()
792            .get(reqwest::header::AUTHORIZATION)
793            .expect("Authorization header should be set")
794            .clone();
795        header.set_sensitive(false);
796
797        assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
798        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
799    }
800
801    #[tokio::test]
802    async fn authenticated_request_with_azure_signer() {
803        let signer = reqsign::azure::default_signer().with_credential_provider(
804            reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
805        );
806        let authentication = Authentication::from(signer);
807
808        let request = Request::new(
809            reqwest::Method::GET,
810            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
811        );
812        let request = authentication.authenticate(request).await.unwrap();
813
814        let authorization = request
815            .headers()
816            .get(reqwest::header::AUTHORIZATION)
817            .expect("Authorization header should be set");
818        assert_eq!(authorization.to_str().unwrap(), "Bearer token");
819        assert!(request.headers().contains_key("x-ms-date"));
820    }
821
822    #[tokio::test]
823    async fn authenticated_request_with_aws_signer_missing_credentials() {
824        let signer = reqsign::aws::default_signer("s3", "us-east-1")
825            .with_credential_provider(EmptyAwsCredentialProvider);
826        let authentication = Authentication::from(signer);
827
828        let request = Request::new(
829            reqwest::Method::GET,
830            Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
831        );
832        let err = authentication.authenticate(request).await.unwrap_err();
833
834        insta::assert_snapshot!(
835            err.to_string(),
836            @"Failed to sign request with AWS credentials"
837        );
838    }
839
840    #[tokio::test]
841    async fn authenticated_request_with_azure_signer_missing_credentials() {
842        let signer =
843            reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
844        let authentication = Authentication::from(signer);
845
846        let request = Request::new(
847            reqwest::Method::GET,
848            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
849        );
850        let err = authentication.authenticate(request).await.unwrap_err();
851
852        insta::assert_snapshot!(
853            err.to_string(),
854            @"Failed to sign request with Azure credentials"
855        );
856    }
857
858    /// Passwords should be redacted in debug output.
859    #[test]
860    fn test_password_redaction() {
861        let credentials =
862            Credentials::basic(Some(String::from("user")), Some(String::from("password")));
863        insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
864    }
865
866    /// Bearer credentials should be redacted in debug output.
867    #[test]
868    fn test_bearer_token_redaction() {
869        let token = "super_secret_token";
870        let credentials = Credentials::bearer(token.into());
871        insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
872    }
873}