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