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 fn as_str(&self) -> &str {
95        self.0.as_str()
96    }
97
98    /// Convert the [`Password`] into its underlying [`String`].
99    pub fn into_string(self) -> String {
100        self.0
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 fn new(token: Vec<u8>) -> Self {
116        Self(token)
117    }
118
119    /// Return the [`Token`] as a byte slice.
120    pub fn as_slice(&self) -> &[u8] {
121        self.0.as_slice()
122    }
123
124    /// Convert the [`Token`] into its underlying [`Vec<u8>`].
125    pub fn into_bytes(self) -> Vec<u8> {
126        self.0
127    }
128
129    /// Return whether the [`Token`] is empty.
130    pub 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    pub(crate) fn to_username(&self) -> Username {
166        match self {
167            Self::Basic { username, .. } => username.clone(),
168            Self::Bearer { .. } => Username::none(),
169        }
170    }
171
172    pub(crate) 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    pub 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    pub(crate) fn is_empty(&self) -> bool {
197        match self {
198            Self::Basic { username, password } => username.is_none() && password.is_none(),
199            Self::Bearer { token } => token.is_empty(),
200        }
201    }
202
203    /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any.
204    ///
205    /// If a username is provided, it must match the login in the netrc file or [`None`] is returned.
206    pub(crate) fn from_netrc(
207        netrc: &Netrc,
208        url: &DisplaySafeUrl,
209        username: Option<&str>,
210    ) -> Option<Self> {
211        let host = url.host_str()?;
212        let entry = netrc
213            .hosts
214            .get(host)
215            .or_else(|| netrc.hosts.get("default"))?;
216
217        // Ensure the username matches if provided
218        if username.is_some_and(|username| username != entry.login) {
219            return None;
220        }
221
222        Some(Self::Basic {
223            username: Username::new(Some(entry.login.clone())),
224            password: Some(Password(entry.password.clone())),
225        })
226    }
227
228    /// Parse [`Credentials`] from a URL, if any.
229    ///
230    /// Returns [`None`] if both [`Url::username`] and [`Url::password`] are not populated.
231    pub fn from_url(url: &Url) -> Option<Self> {
232        if url.username().is_empty() && url.password().is_none() {
233            return None;
234        }
235        Some(Self::Basic {
236            // Remove percent-encoding from URL credentials
237            // See <https://github.com/pypa/pip/blob/06d21db4ff1ab69665c22a88718a4ea9757ca293/src/pip/_internal/utils/misc.py#L497-L499>
238            username: if url.username().is_empty() {
239                None
240            } else {
241                Some(
242                    percent_encoding::percent_decode_str(url.username())
243                        .decode_utf8()
244                        .expect("An encoded username should always decode")
245                        .into_owned(),
246                )
247            }
248            .into(),
249            password: url.password().map(|password| {
250                Password(
251                    percent_encoding::percent_decode_str(password)
252                        .decode_utf8()
253                        .expect("An encoded password should always decode")
254                        .into_owned(),
255                )
256            }),
257        })
258    }
259
260    /// Extract the [`Credentials`] from the environment, given a named source.
261    ///
262    /// For example, given a name of `"pytorch"`, search for `UV_INDEX_PYTORCH_USERNAME` and
263    /// `UV_INDEX_PYTORCH_PASSWORD`.
264    pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
265        let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
266        let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
267        if username.is_none() && password.is_none() {
268            None
269        } else {
270            Some(Self::basic(username, password))
271        }
272    }
273
274    /// Parse [`Credentials`] from an HTTP request, if any.
275    ///
276    /// Only HTTP Basic Authentication is supported.
277    pub(crate) fn from_request(request: &Request) -> Option<Self> {
278        // First, attempt to retrieve the credentials from the URL
279        Self::from_url(request.url()).or(
280            // Then, attempt to pull the credentials from the headers
281            request
282                .headers()
283                .get(reqwest::header::AUTHORIZATION)
284                .map(Self::from_header_value)?,
285        )
286    }
287
288    /// Parse [`Credentials`] from an authorization header, if any.
289    ///
290    /// HTTP Basic and Bearer Authentication are both supported.
291    /// [`None`] will be returned if another authorization scheme is detected.
292    ///
293    /// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme:
294    /// - The contents must be base64 encoded
295    /// - There must be a `:` separator
296    pub(crate) fn from_header_value(header: &HeaderValue) -> Option<Self> {
297        // Parse a `Basic` authentication header.
298        if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
299            let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
300            let mut buf = String::new();
301            decoder
302                .read_to_string(&mut buf)
303                .expect("HTTP Basic Authentication should be base64 encoded");
304            let (username, password) = buf
305                .split_once(':')
306                .expect("HTTP Basic Authentication should include a `:` separator");
307            let username = if username.is_empty() {
308                None
309            } else {
310                Some(username.to_string())
311            };
312            let password = if password.is_empty() {
313                None
314            } else {
315                Some(password.to_string())
316            };
317            return Some(Self::Basic {
318                username: Username::new(username),
319                password: password.map(Password),
320            });
321        }
322
323        // Parse a `Bearer` authentication header.
324        if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
325            return Some(Self::Bearer {
326                token: Token::new(token.to_vec()),
327            });
328        }
329
330        None
331    }
332
333    /// Create an HTTP Basic Authentication header for the credentials.
334    ///
335    /// Panics if the username or password cannot be base64 encoded.
336    pub fn to_header_value(&self) -> HeaderValue {
337        match self {
338            Self::Basic { .. } => {
339                // See: <https://github.com/seanmonstar/reqwest/blob/2c11ef000b151c2eebeed2c18a7b81042220c6b0/src/util.rs#L3>
340                let mut buf = b"Basic ".to_vec();
341                {
342                    let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
343                    write!(encoder, "{}:", self.username().unwrap_or_default())
344                        .expect("Write to base64 encoder should succeed");
345                    if let Some(password) = self.password() {
346                        write!(encoder, "{password}")
347                            .expect("Write to base64 encoder should succeed");
348                    }
349                }
350                let mut header =
351                    HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
352                header.set_sensitive(true);
353                header
354            }
355            Self::Bearer { token } => {
356                let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
357                    .expect("Bearer token is always valid HeaderValue");
358                header.set_sensitive(true);
359                header
360            }
361        }
362    }
363
364    /// Apply the credentials to the given URL.
365    ///
366    /// Any existing credentials will be overridden.
367    #[must_use]
368    pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
369        if let Some(username) = self.username() {
370            let _ = url.set_username(username);
371        }
372        if let Some(password) = self.password() {
373            let _ = url.set_password(Some(password));
374        }
375        url
376    }
377
378    /// Attach the credentials to the given request.
379    ///
380    /// Any existing credentials will be overridden.
381    #[must_use]
382    pub fn authenticate(&self, mut request: Request) -> Request {
383        request
384            .headers_mut()
385            .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
386        request
387    }
388}
389
390#[derive(Clone, Debug)]
391pub(crate) enum Authentication {
392    /// HTTP Basic or Bearer Authentication credentials.
393    Credentials(Credentials),
394
395    /// AWS Signature Version 4 signing.
396    AwsSigner(AwsDefaultSigner),
397
398    /// Google Cloud signing.
399    GcsSigner(GcsDefaultSigner),
400
401    /// Azure Storage signing.
402    AzureSigner(AzureDefaultSigner),
403}
404
405#[derive(Debug, Error)]
406pub(crate) enum AuthenticationError {
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                http_req
600                    .headers_mut()
601                    .entry(HeaderName::from_static("x-ms-version"))
602                    .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION));
603
604                // Sign the parts.
605                let (mut parts, ()) = http_req.into_parts();
606                signer.sign(&mut parts, None).await.map_err(|source| {
607                    AuthenticationError::Sign {
608                        provider: "Azure",
609                        source,
610                    }
611                })?;
612
613                // Copy over the signed headers.
614                request.headers_mut().extend(parts.headers);
615
616                // Copy over the signed path and query, if any.
617                if let Some(path_and_query) = parts.uri.path_and_query() {
618                    request.url_mut().set_path(path_and_query.path());
619                    request.url_mut().set_query(path_and_query.query());
620                }
621                Ok(request)
622            }
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use insta::assert_debug_snapshot;
630    use reqsign::aws::Credential as AwsCredential;
631    use reqsign::azure::Credential as AzureCredential;
632    use reqsign::{Context, ProvideCredential};
633
634    use super::*;
635
636    #[derive(Debug)]
637    struct EmptyAwsCredentialProvider;
638
639    impl ProvideCredential for EmptyAwsCredentialProvider {
640        type Credential = AwsCredential;
641
642        async fn provide_credential(
643            &self,
644            _ctx: &Context,
645        ) -> reqsign::Result<Option<Self::Credential>> {
646            Ok(None)
647        }
648    }
649
650    #[derive(Debug)]
651    struct EmptyAzureCredentialProvider;
652
653    impl ProvideCredential for EmptyAzureCredentialProvider {
654        type Credential = AzureCredential;
655
656        async fn provide_credential(
657            &self,
658            _ctx: &Context,
659        ) -> reqsign::Result<Option<Self::Credential>> {
660            Ok(None)
661        }
662    }
663
664    #[test]
665    fn from_url_no_credentials() {
666        let url = &Url::parse("https://example.com/simple/first/").unwrap();
667        assert_eq!(Credentials::from_url(url), None);
668    }
669
670    #[test]
671    fn from_url_username_and_password() {
672        let url = &Url::parse("https://example.com/simple/first/").unwrap();
673        let mut auth_url = url.clone();
674        auth_url.set_username("user").unwrap();
675        auth_url.set_password(Some("password")).unwrap();
676        let credentials = Credentials::from_url(&auth_url).unwrap();
677        assert_eq!(credentials.username(), Some("user"));
678        assert_eq!(credentials.password(), Some("password"));
679    }
680
681    #[test]
682    fn from_url_no_username() {
683        let url = &Url::parse("https://example.com/simple/first/").unwrap();
684        let mut auth_url = url.clone();
685        auth_url.set_password(Some("password")).unwrap();
686        let credentials = Credentials::from_url(&auth_url).unwrap();
687        assert_eq!(credentials.username(), None);
688        assert_eq!(credentials.password(), Some("password"));
689    }
690
691    /// Test for <https://github.com/astral-sh/uv/issues/17343>
692    ///
693    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
694    /// should be recognized as having credentials.
695    #[test]
696    fn from_url_empty_username_with_password() {
697        // Parse a URL with the format `:password@host` directly
698        let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
699        let credentials = Credentials::from_url(&url).unwrap();
700        assert_eq!(credentials.username(), None);
701        assert_eq!(credentials.password(), Some("token"));
702        assert!(
703            credentials.is_authenticated(),
704            "URL with empty username but password should be considered authenticated"
705        );
706    }
707
708    #[test]
709    fn from_url_no_password() {
710        let url = &Url::parse("https://example.com/simple/first/").unwrap();
711        let mut auth_url = url.clone();
712        auth_url.set_username("user").unwrap();
713        let credentials = Credentials::from_url(&auth_url).unwrap();
714        assert_eq!(credentials.username(), Some("user"));
715        assert_eq!(credentials.password(), None);
716    }
717
718    #[test]
719    fn authenticated_request_from_url() {
720        let url = Url::parse("https://example.com/simple/first/").unwrap();
721        let mut auth_url = url.clone();
722        auth_url.set_username("user").unwrap();
723        auth_url.set_password(Some("password")).unwrap();
724        let credentials = Credentials::from_url(&auth_url).unwrap();
725
726        let mut request = Request::new(reqwest::Method::GET, url);
727        request = credentials.authenticate(request);
728
729        let mut header = request
730            .headers()
731            .get(reqwest::header::AUTHORIZATION)
732            .expect("Authorization header should be set")
733            .clone();
734        header.set_sensitive(false);
735
736        assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
737        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
738    }
739
740    #[test]
741    fn authenticated_request_from_url_with_percent_encoded_user() {
742        let url = Url::parse("https://example.com/simple/first/").unwrap();
743        let mut auth_url = url.clone();
744        auth_url.set_username("user@domain").unwrap();
745        auth_url.set_password(Some("password")).unwrap();
746        let credentials = Credentials::from_url(&auth_url).unwrap();
747
748        let mut request = Request::new(reqwest::Method::GET, url);
749        request = credentials.authenticate(request);
750
751        let mut header = request
752            .headers()
753            .get(reqwest::header::AUTHORIZATION)
754            .expect("Authorization header should be set")
755            .clone();
756        header.set_sensitive(false);
757
758        assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
759        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
760    }
761
762    #[test]
763    fn authenticated_request_from_url_with_percent_encoded_password() {
764        let url = Url::parse("https://example.com/simple/first/").unwrap();
765        let mut auth_url = url.clone();
766        auth_url.set_username("user").unwrap();
767        auth_url.set_password(Some("password==")).unwrap();
768        let credentials = Credentials::from_url(&auth_url).unwrap();
769
770        let mut request = Request::new(reqwest::Method::GET, url);
771        request = credentials.authenticate(request);
772
773        let mut header = request
774            .headers()
775            .get(reqwest::header::AUTHORIZATION)
776            .expect("Authorization header should be set")
777            .clone();
778        header.set_sensitive(false);
779
780        assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
781        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
782    }
783
784    #[tokio::test]
785    async fn authenticated_request_with_azure_signer() {
786        let signer = reqsign::azure::default_signer().with_credential_provider(
787            reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
788        );
789        let authentication = Authentication::from(signer);
790
791        let request = Request::new(
792            reqwest::Method::GET,
793            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
794        );
795        let request = authentication.authenticate(request).await.unwrap();
796
797        let authorization = request
798            .headers()
799            .get(reqwest::header::AUTHORIZATION)
800            .expect("Authorization header should be set");
801        assert_eq!(authorization.to_str().unwrap(), "Bearer token");
802        assert!(request.headers().contains_key("x-ms-date"));
803        assert_eq!(
804            request
805                .headers()
806                .get("x-ms-version")
807                .expect("x-ms-version header should be set")
808                .to_str()
809                .unwrap(),
810            AZURE_STORAGE_VERSION
811        );
812    }
813
814    #[tokio::test]
815    async fn authenticated_request_with_aws_signer_missing_credentials() {
816        let signer = reqsign::aws::default_signer("s3", "us-east-1")
817            .with_credential_provider(EmptyAwsCredentialProvider);
818        let authentication = Authentication::from(signer);
819
820        let request = Request::new(
821            reqwest::Method::GET,
822            Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
823        );
824        let err = authentication.authenticate(request).await.unwrap_err();
825
826        insta::assert_snapshot!(
827            err.to_string(),
828            @"Failed to sign request with AWS credentials"
829        );
830    }
831
832    #[tokio::test]
833    async fn authenticated_request_with_azure_signer_missing_credentials() {
834        let signer =
835            reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
836        let authentication = Authentication::from(signer);
837
838        let request = Request::new(
839            reqwest::Method::GET,
840            Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
841        );
842        let err = authentication.authenticate(request).await.unwrap_err();
843
844        insta::assert_snapshot!(
845            err.to_string(),
846            @"Failed to sign request with Azure credentials"
847        );
848    }
849
850    /// Passwords should be redacted in debug output.
851    #[test]
852    fn test_password_redaction() {
853        let credentials =
854            Credentials::basic(Some(String::from("user")), Some(String::from("password")));
855        insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
856    }
857
858    /// Bearer credentials should be redacted in debug output.
859    #[test]
860    fn test_bearer_token_redaction() {
861        let token = "super_secret_token";
862        let credentials = Credentials::bearer(token.into());
863        insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
864    }
865}