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