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 netrc::Netrc;
12use reqsign::aws::DefaultSigner;
13use reqwest::Request;
14use reqwest::header::HeaderValue;
15use serde::{Deserialize, Serialize};
16use url::Url;
17
18use uv_redacted::DisplaySafeUrl;
19use uv_static::EnvVars;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Credentials {
23    /// RFC 7617 HTTP Basic Authentication
24    Basic {
25        /// The username to use for authentication.
26        username: Username,
27        /// The password to use for authentication.
28        password: Option<Password>,
29    },
30    /// RFC 6750 Bearer Token Authentication
31    Bearer {
32        /// The token to use for authentication.
33        token: Token,
34    },
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
38#[serde(transparent)]
39pub struct Username(Option<String>);
40
41impl Username {
42    /// Create a new username.
43    ///
44    /// Unlike `reqwest`, empty usernames are be encoded as `None` instead of an empty string.
45    pub(crate) fn new(value: Option<String>) -> Self {
46        // Ensure empty strings are `None`
47        Self(value.filter(|s| !s.is_empty()))
48    }
49
50    pub(crate) fn none() -> Self {
51        Self::new(None)
52    }
53
54    pub(crate) fn is_none(&self) -> bool {
55        self.0.is_none()
56    }
57
58    pub(crate) fn is_some(&self) -> bool {
59        self.0.is_some()
60    }
61
62    pub(crate) fn as_deref(&self) -> Option<&str> {
63        self.0.as_deref()
64    }
65}
66
67impl From<String> for Username {
68    fn from(value: String) -> Self {
69        Self::new(Some(value))
70    }
71}
72
73impl From<Option<String>> for Username {
74    fn from(value: Option<String>) -> Self {
75        Self::new(value)
76    }
77}
78
79#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
80#[serde(transparent)]
81pub struct Password(String);
82
83impl Password {
84    pub fn new(password: String) -> Self {
85        Self(password)
86    }
87
88    /// Return the [`Password`] as a string slice.
89    pub fn as_str(&self) -> &str {
90        self.0.as_str()
91    }
92
93    /// Convert the [`Password`] into its underlying [`String`].
94    pub fn into_string(self) -> String {
95        self.0
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 fn new(token: Vec<u8>) -> Self {
111        Self(token)
112    }
113
114    /// Return the [`Token`] as a byte slice.
115    pub fn as_slice(&self) -> &[u8] {
116        self.0.as_slice()
117    }
118
119    /// Convert the [`Token`] into its underlying [`Vec<u8>`].
120    pub fn into_bytes(self) -> Vec<u8> {
121        self.0
122    }
123
124    /// Return whether the [`Token`] is empty.
125    pub 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 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    pub(crate) 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    Signer(DefaultSigner),
392}
393
394impl PartialEq for Authentication {
395    fn eq(&self, other: &Self) -> bool {
396        match (self, other) {
397            (Self::Credentials(a), Self::Credentials(b)) => a == b,
398            (Self::Signer(..), Self::Signer(..)) => true,
399            _ => false,
400        }
401    }
402}
403
404impl Eq for Authentication {}
405
406impl From<Credentials> for Authentication {
407    fn from(credentials: Credentials) -> Self {
408        Self::Credentials(credentials)
409    }
410}
411
412impl From<DefaultSigner> for Authentication {
413    fn from(signer: DefaultSigner) -> Self {
414        Self::Signer(signer)
415    }
416}
417
418impl Authentication {
419    /// Return the password used for authentication, if any.
420    pub(crate) fn password(&self) -> Option<&str> {
421        match self {
422            Self::Credentials(credentials) => credentials.password(),
423            Self::Signer(..) => None,
424        }
425    }
426
427    /// Return the username used for authentication, if any.
428    pub(crate) fn username(&self) -> Option<&str> {
429        match self {
430            Self::Credentials(credentials) => credentials.username(),
431            Self::Signer(..) => None,
432        }
433    }
434
435    /// Return the username used for authentication, if any.
436    pub(crate) fn as_username(&self) -> Cow<'_, Username> {
437        match self {
438            Self::Credentials(credentials) => credentials.as_username(),
439            Self::Signer(..) => Cow::Owned(Username::none()),
440        }
441    }
442
443    /// Return the username used for authentication, if any.
444    pub(crate) fn to_username(&self) -> Username {
445        match self {
446            Self::Credentials(credentials) => credentials.to_username(),
447            Self::Signer(..) => Username::none(),
448        }
449    }
450
451    /// Return `true` if the object contains a means of authenticating.
452    pub(crate) fn is_authenticated(&self) -> bool {
453        match self {
454            Self::Credentials(credentials) => credentials.is_authenticated(),
455            Self::Signer(..) => true,
456        }
457    }
458
459    /// Return `true` if the object contains no credentials.
460    pub(crate) fn is_empty(&self) -> bool {
461        match self {
462            Self::Credentials(credentials) => credentials.is_empty(),
463            Self::Signer(..) => false,
464        }
465    }
466
467    /// Apply the authentication to the given request.
468    ///
469    /// Any existing credentials will be overridden.
470    #[must_use]
471    pub(crate) async fn authenticate(&self, mut request: Request) -> Request {
472        match self {
473            Self::Credentials(credentials) => credentials.authenticate(request),
474            Self::Signer(signer) => {
475                // Build an `http::Request` from the `reqwest::Request`.
476                // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid.
477                let uri = Uri::from_str(request.url().as_str()).unwrap();
478                let mut http_req = http::Request::builder()
479                    .method(request.method().clone())
480                    .uri(uri)
481                    .body(())
482                    .unwrap();
483                *http_req.headers_mut() = request.headers().clone();
484
485                // Sign the parts.
486                let (mut parts, ()) = http_req.into_parts();
487                signer
488                    .sign(&mut parts, None)
489                    .await
490                    .expect("AWS signing should succeed");
491
492                // Copy over the signed headers.
493                request.headers_mut().extend(parts.headers);
494
495                // Copy over the signed path and query, if any.
496                if let Some(path_and_query) = parts.uri.path_and_query() {
497                    request.url_mut().set_path(path_and_query.path());
498                    request.url_mut().set_query(path_and_query.query());
499                }
500                request
501            }
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use insta::assert_debug_snapshot;
509
510    use super::*;
511
512    #[test]
513    fn from_url_no_credentials() {
514        let url = &Url::parse("https://example.com/simple/first/").unwrap();
515        assert_eq!(Credentials::from_url(url), None);
516    }
517
518    #[test]
519    fn from_url_username_and_password() {
520        let url = &Url::parse("https://example.com/simple/first/").unwrap();
521        let mut auth_url = url.clone();
522        auth_url.set_username("user").unwrap();
523        auth_url.set_password(Some("password")).unwrap();
524        let credentials = Credentials::from_url(&auth_url).unwrap();
525        assert_eq!(credentials.username(), Some("user"));
526        assert_eq!(credentials.password(), Some("password"));
527    }
528
529    #[test]
530    fn from_url_no_username() {
531        let url = &Url::parse("https://example.com/simple/first/").unwrap();
532        let mut auth_url = url.clone();
533        auth_url.set_password(Some("password")).unwrap();
534        let credentials = Credentials::from_url(&auth_url).unwrap();
535        assert_eq!(credentials.username(), None);
536        assert_eq!(credentials.password(), Some("password"));
537    }
538
539    #[test]
540    fn from_url_no_password() {
541        let url = &Url::parse("https://example.com/simple/first/").unwrap();
542        let mut auth_url = url.clone();
543        auth_url.set_username("user").unwrap();
544        let credentials = Credentials::from_url(&auth_url).unwrap();
545        assert_eq!(credentials.username(), Some("user"));
546        assert_eq!(credentials.password(), None);
547    }
548
549    #[test]
550    fn authenticated_request_from_url() {
551        let url = Url::parse("https://example.com/simple/first/").unwrap();
552        let mut auth_url = url.clone();
553        auth_url.set_username("user").unwrap();
554        auth_url.set_password(Some("password")).unwrap();
555        let credentials = Credentials::from_url(&auth_url).unwrap();
556
557        let mut request = Request::new(reqwest::Method::GET, url);
558        request = credentials.authenticate(request);
559
560        let mut header = request
561            .headers()
562            .get(reqwest::header::AUTHORIZATION)
563            .expect("Authorization header should be set")
564            .clone();
565        header.set_sensitive(false);
566
567        assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###);
568        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
569    }
570
571    #[test]
572    fn authenticated_request_from_url_with_percent_encoded_user() {
573        let url = Url::parse("https://example.com/simple/first/").unwrap();
574        let mut auth_url = url.clone();
575        auth_url.set_username("user@domain").unwrap();
576        auth_url.set_password(Some("password")).unwrap();
577        let credentials = Credentials::from_url(&auth_url).unwrap();
578
579        let mut request = Request::new(reqwest::Method::GET, url);
580        request = credentials.authenticate(request);
581
582        let mut header = request
583            .headers()
584            .get(reqwest::header::AUTHORIZATION)
585            .expect("Authorization header should be set")
586            .clone();
587        header.set_sensitive(false);
588
589        assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###);
590        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
591    }
592
593    #[test]
594    fn authenticated_request_from_url_with_percent_encoded_password() {
595        let url = Url::parse("https://example.com/simple/first/").unwrap();
596        let mut auth_url = url.clone();
597        auth_url.set_username("user").unwrap();
598        auth_url.set_password(Some("password==")).unwrap();
599        let credentials = Credentials::from_url(&auth_url).unwrap();
600
601        let mut request = Request::new(reqwest::Method::GET, url);
602        request = credentials.authenticate(request);
603
604        let mut header = request
605            .headers()
606            .get(reqwest::header::AUTHORIZATION)
607            .expect("Authorization header should be set")
608            .clone();
609        header.set_sensitive(false);
610
611        assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###);
612        assert_eq!(Credentials::from_header_value(&header), Some(credentials));
613    }
614
615    // Test that we don't include the password in debug messages.
616    #[test]
617    fn test_password_obfuscation() {
618        let credentials =
619            Credentials::basic(Some(String::from("user")), Some(String::from("password")));
620        let debugged = format!("{credentials:?}");
621        assert_eq!(
622            debugged,
623            "Basic { username: Username(Some(\"user\")), password: Some(****) }"
624        );
625    }
626
627    #[test]
628    fn test_bearer_token_obfuscation() {
629        let token = "super_secret_token";
630        let credentials = Credentials::bearer(token.into());
631        let debugged = format!("{credentials:?}");
632        assert!(
633            !debugged.contains(token),
634            "Token should be obfuscated in Debug impl: {debugged}"
635        );
636    }
637}