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    pub(crate) 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    pub(crate) 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    pub(crate) 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    pub(crate) fn to_username(&self) -> Username {
161        match self {
162            Self::Basic { username, .. } => username.clone(),
163            Self::Bearer { .. } => Username::none(),
164        }
165    }
166
167    pub(crate) 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    pub(crate) 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    pub(crate) 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()
239                        .expect("An encoded username should always decode")
240                        .into_owned(),
241                )
242            }
243            .into(),
244            password: url.password().map(|password| {
245                Password(
246                    percent_encoding::percent_decode_str(password)
247                        .decode_utf8()
248                        .expect("An encoded password should always decode")
249                        .into_owned(),
250                )
251            }),
252        })
253    }
254
255    /// Extract the [`Credentials`] from the environment, given a named source.
256    ///
257    /// For example, given a name of `"pytorch"`, search for `UV_INDEX_PYTORCH_USERNAME` and
258    /// `UV_INDEX_PYTORCH_PASSWORD`.
259    pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
260        let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
261        let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
262        if username.is_none() && password.is_none() {
263            None
264        } else {
265            Some(Self::basic(username, password))
266        }
267    }
268
269    /// Parse [`Credentials`] from an HTTP request, if any.
270    ///
271    /// Only HTTP Basic Authentication is supported.
272    pub(crate) fn from_request(request: &Request) -> Option<Self> {
273        // First, attempt to retrieve the credentials from the URL
274        Self::from_url(request.url()).or(
275            // Then, attempt to pull the credentials from the headers
276            request
277                .headers()
278                .get(reqwest::header::AUTHORIZATION)
279                .map(Self::from_header_value)?,
280        )
281    }
282
283    /// Parse [`Credentials`] from an authorization header, if any.
284    ///
285    /// HTTP Basic and Bearer Authentication are both supported.
286    /// [`None`] will be returned if another authorization scheme is detected.
287    ///
288    /// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme:
289    /// - The contents must be base64 encoded
290    /// - There must be a `:` separator
291    fn from_header_value(header: &HeaderValue) -> Option<Self> {
292        // Parse a `Basic` authentication header.
293        if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
294            let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
295            let mut buf = String::new();
296            decoder
297                .read_to_string(&mut buf)
298                .expect("HTTP Basic Authentication should be base64 encoded");
299            let (username, password) = buf
300                .split_once(':')
301                .expect("HTTP Basic Authentication should include a `:` separator");
302            let username = if username.is_empty() {
303                None
304            } else {
305                Some(username.to_string())
306            };
307            let password = if password.is_empty() {
308                None
309            } else {
310                Some(password.to_string())
311            };
312            return Some(Self::Basic {
313                username: Username::new(username),
314                password: password.map(Password),
315            });
316        }
317
318        // Parse a `Bearer` authentication header.
319        if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
320            return Some(Self::Bearer {
321                token: Token::new(token.to_vec()),
322            });
323        }
324
325        None
326    }
327
328    /// Create an HTTP Basic Authentication header for the credentials.
329    ///
330    /// Panics if the username or password cannot be base64 encoded.
331    pub fn to_header_value(&self) -> HeaderValue {
332        match self {
333            Self::Basic { .. } => {
334                // See: <https://github.com/seanmonstar/reqwest/blob/2c11ef000b151c2eebeed2c18a7b81042220c6b0/src/util.rs#L3>
335                let mut buf = b"Basic ".to_vec();
336                {
337                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
338                    write!(encoder, "{}:", self.username().unwrap_or_default())
339                        .expect("Write to base64 encoder should succeed");
340                    if let Some(password) = self.password() {
341                        write!(encoder, "{password}")
342                            .expect("Write to base64 encoder should succeed");
343                    }
344                }
345                let mut header =
346                    HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
347                header.set_sensitive(true);
348                header
349            }
350            Self::Bearer { token } => {
351                let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
352                    .expect("Bearer token is always valid HeaderValue");
353                header.set_sensitive(true);
354                header
355            }
356        }
357    }
358
359    /// Apply the credentials to the given URL.
360    ///
361    /// Any existing credentials will be overridden.
362    #[must_use]
363    pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
364        if let Some(username) = self.username() {
365            let _ = url.set_username(username);
366        }
367        if let Some(password) = self.password() {
368            let _ = url.set_password(Some(password));
369        }
370        url
371    }
372
373    /// Attach the credentials to the given request.
374    ///
375    /// Any existing credentials will be overridden.
376    #[must_use]
377    pub fn authenticate(&self, mut request: Request) -> Request {
378        request
379            .headers_mut()
380            .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
381        request
382    }
383}
384
385#[derive(Clone, Debug)]
386pub(crate) enum Authentication {
387    /// HTTP Basic or Bearer Authentication credentials.
388    Credentials(Credentials),
389
390    /// AWS Signature Version 4 signing.
391    AwsSigner(AwsDefaultSigner),
392
393    /// Google Cloud signing.
394    GcsSigner(GcsDefaultSigner),
395
396    /// Azure Storage signing.
397    AzureSigner(AzureDefaultSigner),
398}
399
400#[derive(Debug, Error)]
401pub(crate) enum AuthenticationError {
402    #[error("Failed to convert request URL to URI")]
403    InvalidUri(#[from] http::uri::InvalidUri),
404
405    #[error("Failed to build request for {provider} signing")]
406    BuildRequest {
407        provider: &'static str,
408        #[source]
409        source: http::Error,
410    },
411
412    #[error("Failed to sign request with {provider} credentials")]
413    Sign {
414        provider: &'static str,
415        #[source]
416        source: reqsign::Error,
417    },
418}
419
420impl PartialEq for Authentication {
421    fn eq(&self, other: &Self) -> bool {
422        match (self, other) {
423            (Self::Credentials(a), Self::Credentials(b)) => a == b,
424            (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
425            (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
426            (Self::AzureSigner(..), Self::AzureSigner(..)) => true,
427            _ => false,
428        }
429    }
430}
431
432impl Eq for Authentication {}
433
434impl From<Credentials> for Authentication {
435    fn from(credentials: Credentials) -> Self {
436        Self::Credentials(credentials)
437    }
438}
439
440impl From<AwsDefaultSigner> for Authentication {
441    fn from(signer: AwsDefaultSigner) -> Self {
442        Self::AwsSigner(signer)
443    }
444}
445
446impl From<GcsDefaultSigner> for Authentication {
447    fn from(signer: GcsDefaultSigner) -> Self {
448        Self::GcsSigner(signer)
449    }
450}
451
452impl From<AzureDefaultSigner> for Authentication {
453    fn from(signer: AzureDefaultSigner) -> Self {
454        Self::AzureSigner(signer)
455    }
456}
457
458impl Authentication {
459    /// Return the password used for authentication, if any.
460    pub(crate) fn password(&self) -> Option<&str> {
461        match self {
462            Self::Credentials(credentials) => credentials.password(),
463            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
464        }
465    }
466
467    /// Return the username used for authentication, if any.
468    pub(crate) fn username(&self) -> Option<&str> {
469        match self {
470            Self::Credentials(credentials) => credentials.username(),
471            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
472        }
473    }
474
475    /// Return the username used for authentication, if any.
476    pub(crate) fn as_username(&self) -> Cow<'_, Username> {
477        match self {
478            Self::Credentials(credentials) => credentials.as_username(),
479            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => {
480                Cow::Owned(Username::none())
481            }
482        }
483    }
484
485    /// Return the username used for authentication, if any.
486    pub(crate) fn to_username(&self) -> Username {
487        match self {
488            Self::Credentials(credentials) => credentials.to_username(),
489            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(),
490        }
491    }
492
493    /// Return `true` if the object contains a means of authenticating.
494    pub(crate) fn is_authenticated(&self) -> bool {
495        match self {
496            Self::Credentials(credentials) => credentials.is_authenticated(),
497            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true,
498        }
499    }
500
501    /// Return `true` if the object contains no credentials.
502    pub(crate) fn is_empty(&self) -> bool {
503        match self {
504            Self::Credentials(credentials) => credentials.is_empty(),
505            Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false,
506        }
507    }
508
509    /// Apply the authentication to the given request.
510    ///
511    /// Any existing credentials will be overridden.
512    pub(crate) async fn authenticate(
513        &self,
514        mut request: Request,
515    ) -> Result<Request, AuthenticationError> {
516        match self {
517            Self::Credentials(credentials) => Ok(credentials.authenticate(request)),
518            Self::AwsSigner(signer) => {
519                // Build an `http::Request` from the `reqwest::Request`.
520                let uri = Uri::from_str(request.url().as_str())?;
521                let mut http_req = http::Request::builder()
522                    .method(request.method().clone())
523                    .uri(uri)
524                    .body(())
525                    .map_err(|source| AuthenticationError::BuildRequest {
526                        provider: "AWS",
527                        source,
528                    })?;
529                *http_req.headers_mut() = request.headers().clone();
530
531                // Sign the parts.
532                let (mut parts, ()) = http_req.into_parts();
533                signer.sign(&mut parts, None).await.map_err(|source| {
534                    AuthenticationError::Sign {
535                        provider: "AWS",
536                        source,
537                    }
538                })?;
539
540                // Copy over the signed headers.
541                request.headers_mut().extend(parts.headers);
542
543                // Copy over the signed path and query, if any.
544                if let Some(path_and_query) = parts.uri.path_and_query() {
545                    request.url_mut().set_path(path_and_query.path());
546                    request.url_mut().set_query(path_and_query.query());
547                }
548                Ok(request)
549            }
550            Self::GcsSigner(signer) => {
551                // Build an `http::Request` from the `reqwest::Request`.
552                let uri = Uri::from_str(request.url().as_str())?;
553                let mut http_req = http::Request::builder()
554                    .method(request.method().clone())
555                    .uri(uri)
556                    .body(())
557                    .map_err(|source| AuthenticationError::BuildRequest {
558                        provider: "GCS",
559                        source,
560                    })?;
561                *http_req.headers_mut() = request.headers().clone();
562
563                // Sign the parts.
564                let (mut parts, ()) = http_req.into_parts();
565                signer.sign(&mut parts, None).await.map_err(|source| {
566                    AuthenticationError::Sign {
567                        provider: "GCS",
568                        source,
569                    }
570                })?;
571
572                // Copy over the signed headers.
573                request.headers_mut().extend(parts.headers);
574
575                // Copy over the signed path and query, if any.
576                if let Some(path_and_query) = parts.uri.path_and_query() {
577                    request.url_mut().set_path(path_and_query.path());
578                    request.url_mut().set_query(path_and_query.query());
579                }
580                Ok(request)
581            }
582            Self::AzureSigner(signer) => {
583                // Build an `http::Request` from the `reqwest::Request`.
584                let uri = Uri::from_str(request.url().as_str())?;
585                let mut http_req = http::Request::builder()
586                    .method(request.method().clone())
587                    .uri(uri)
588                    .body(())
589                    .map_err(|source| AuthenticationError::BuildRequest {
590                        provider: "Azure",
591                        source,
592                    })?;
593                *http_req.headers_mut() = request.headers().clone();
594                http_req
595                    .headers_mut()
596                    .entry(HeaderName::from_static("x-ms-version"))
597                    .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION));
598
599                // Sign the parts.
600                let (mut parts, ()) = http_req.into_parts();
601                signer.sign(&mut parts, None).await.map_err(|source| {
602                    AuthenticationError::Sign {
603                        provider: "Azure",
604                        source,
605                    }
606                })?;
607
608                // Copy over the signed headers.
609                request.headers_mut().extend(parts.headers);
610
611                // Copy over the signed path and query, if any.
612                if let Some(path_and_query) = parts.uri.path_and_query() {
613                    request.url_mut().set_path(path_and_query.path());
614                    request.url_mut().set_query(path_and_query.query());
615                }
616                Ok(request)
617            }
618        }
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use insta::assert_debug_snapshot;
625    use reqsign::aws::Credential as AwsCredential;
626    use reqsign::azure::Credential as AzureCredential;
627    use reqsign::{Context, ProvideCredential};
628
629    use super::*;
630
631    #[derive(Debug)]
632    struct EmptyAwsCredentialProvider;
633
634    impl ProvideCredential for EmptyAwsCredentialProvider {
635        type Credential = AwsCredential;
636
637        async fn provide_credential(
638            &self,
639            _ctx: &Context,
640        ) -> reqsign::Result<Option<Self::Credential>> {
641            Ok(None)
642        }
643    }
644
645    #[derive(Debug)]
646    struct EmptyAzureCredentialProvider;
647
648    impl ProvideCredential for EmptyAzureCredentialProvider {
649        type Credential = AzureCredential;
650
651        async fn provide_credential(
652            &self,
653            _ctx: &Context,
654        ) -> reqsign::Result<Option<Self::Credential>> {
655            Ok(None)
656        }
657    }
658
659    #[test]
660    fn from_url_no_credentials() {
661        let url = &Url::parse("https://example.com/simple/first/").unwrap();
662        assert_eq!(Credentials::from_url(url), None);
663    }
664
665    #[test]
666    fn from_url_username_and_password() {
667        let url = &Url::parse("https://example.com/simple/first/").unwrap();
668        let mut auth_url = url.clone();
669        auth_url.set_username("user").unwrap();
670        auth_url.set_password(Some("password")).unwrap();
671        let credentials = Credentials::from_url(&auth_url).unwrap();
672        assert_eq!(credentials.username(), Some("user"));
673        assert_eq!(credentials.password(), Some("password"));
674    }
675
676    #[test]
677    fn from_url_no_username() {
678        let url = &Url::parse("https://example.com/simple/first/").unwrap();
679        let mut auth_url = url.clone();
680        auth_url.set_password(Some("password")).unwrap();
681        let credentials = Credentials::from_url(&auth_url).unwrap();
682        assert_eq!(credentials.username(), None);
683        assert_eq!(credentials.password(), Some("password"));
684    }
685
686    /// Test for <https://github.com/astral-sh/uv/issues/17343>
687    ///
688    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
689    /// should be recognized as having credentials.
690    #[test]
691    fn from_url_empty_username_with_password() {
692        // Parse a URL with the format `:password@host` directly
693        let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
694        let credentials = Credentials::from_url(&url).unwrap();
695        assert_eq!(credentials.username(), None);
696        assert_eq!(credentials.password(), Some("token"));
697        assert!(
698            credentials.is_authenticated(),
699            "URL with empty username but password should be considered authenticated"
700        );
701    }
702
703    #[test]
704    fn from_url_no_password() {
705        let url = &Url::parse("https://example.com/simple/first/").unwrap();
706        let mut auth_url = url.clone();
707        auth_url.set_username("user").unwrap();
708        let credentials = Credentials::from_url(&auth_url).unwrap();
709        assert_eq!(credentials.username(), Some("user"));
710        assert_eq!(credentials.password(), None);
711    }
712
713    #[test]
714    fn authenticated_request_from_url() {
715        let url = Url::parse("https://example.com/simple/first/").unwrap();
716        let mut auth_url = url.clone();
717        auth_url.set_username("user").unwrap();
718        auth_url.set_password(Some("password")).unwrap();
719        let credentials = Credentials::from_url(&auth_url).unwrap();
720
721        let mut request = Request::new(reqwest::Method::GET, url);
722        request = credentials.authenticate(request);
723
724        let mut header = request
725            .headers()
726            .get(reqwest::header::AUTHORIZATION)
727            .expect("Authorization header should be set")
728            .clone();
729        header.set_sensitive(false);
730
731        assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
732        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
733    }
734
735    #[test]
736    fn authenticated_request_from_url_with_percent_encoded_user() {
737        let url = Url::parse("https://example.com/simple/first/").unwrap();
738        let mut auth_url = url.clone();
739        auth_url.set_username("user@domain").unwrap();
740        auth_url.set_password(Some("password")).unwrap();
741        let credentials = Credentials::from_url(&auth_url).unwrap();
742
743        let mut request = Request::new(reqwest::Method::GET, url);
744        request = credentials.authenticate(request);
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 dXNlckBkb21haW46cGFzc3dvcmQ=""#);
754        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
755    }
756
757    #[test]
758    fn authenticated_request_from_url_with_percent_encoded_password() {
759        let url = Url::parse("https://example.com/simple/first/").unwrap();
760        let mut auth_url = url.clone();
761        auth_url.set_username("user").unwrap();
762        auth_url.set_password(Some("password==")).unwrap();
763        let credentials = Credentials::from_url(&auth_url).unwrap();
764
765        let mut request = Request::new(reqwest::Method::GET, url);
766        request = credentials.authenticate(request);
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 dXNlcjpwYXNzd29yZD09""#);
776        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
777    }
778
779    #[tokio::test]
780    async fn authenticated_request_with_azure_signer() {
781        let signer = reqsign::azure::default_signer().with_credential_provider(
782            reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
783        );
784        let authentication = Authentication::from(signer);
785
786        let request = Request::new(
787            reqwest::Method::GET,
788            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
789        );
790        let request = authentication.authenticate(request).await.unwrap();
791
792        let authorization = request
793            .headers()
794            .get(reqwest::header::AUTHORIZATION)
795            .expect("Authorization header should be set");
796        assert_eq!(authorization.to_str().unwrap(), "Bearer token");
797        assert!(request.headers().contains_key("x-ms-date"));
798        assert_eq!(
799            request
800                .headers()
801                .get("x-ms-version")
802                .expect("x-ms-version header should be set")
803                .to_str()
804                .unwrap(),
805            AZURE_STORAGE_VERSION
806        );
807    }
808
809    #[tokio::test]
810    async fn authenticated_request_with_aws_signer_missing_credentials() {
811        let signer = reqsign::aws::default_signer("s3", "us-east-1")
812            .with_credential_provider(EmptyAwsCredentialProvider);
813        let authentication = Authentication::from(signer);
814
815        let request = Request::new(
816            reqwest::Method::GET,
817            Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
818        );
819        let err = authentication.authenticate(request).await.unwrap_err();
820
821        insta::assert_snapshot!(
822            err.to_string(),
823            @"Failed to sign request with AWS credentials"
824        );
825    }
826
827    #[tokio::test]
828    async fn authenticated_request_with_azure_signer_missing_credentials() {
829        let signer =
830            reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
831        let authentication = Authentication::from(signer);
832
833        let request = Request::new(
834            reqwest::Method::GET,
835            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
836        );
837        let err = authentication.authenticate(request).await.unwrap_err();
838
839        insta::assert_snapshot!(
840            err.to_string(),
841            @"Failed to sign request with Azure credentials"
842        );
843    }
844
845    /// Passwords should be redacted in debug output.
846    #[test]
847    fn test_password_redaction() {
848        let credentials =
849            Credentials::basic(Some(String::from("user")), Some(String::from("password")));
850        insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
851    }
852
853    /// Bearer credentials should be redacted in debug output.
854    #[test]
855    fn test_bearer_token_redaction() {
856        let token = "super_secret_token";
857        let credentials = Credentials::bearer(token.into());
858        insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
859    }
860}