From f491aa0f58c896e8d1fc930990837901bc29210d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 1 Apr 2025 17:48:21 -0400 Subject: [PATCH] Add `Bearer` support to `Credentials` (#12610) ## Summary I noticed that these only support Basic credentials, but we may want to allow users to provide Bearer tokens? This PR just generalizes the type. --- crates/uv-auth/src/cache.rs | 8 +- crates/uv-auth/src/credentials.rs | 154 ++++++++++++++++++++---------- crates/uv-auth/src/keyring.rs | 16 ++-- crates/uv-auth/src/middleware.rs | 16 ++-- 4 files changed, 125 insertions(+), 69 deletions(-) diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index d794b52a1..0203efe8b 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -245,19 +245,19 @@ mod tests { #[test] fn test_trie() { - let credentials1 = Arc::new(Credentials::new( + let credentials1 = Arc::new(Credentials::basic( Some("username1".to_string()), Some("password1".to_string()), )); - let credentials2 = Arc::new(Credentials::new( + let credentials2 = Arc::new(Credentials::basic( Some("username2".to_string()), Some("password2".to_string()), )); - let credentials3 = Arc::new(Credentials::new( + let credentials3 = Arc::new(Credentials::basic( Some("username3".to_string()), Some("password3".to_string()), )); - let credentials4 = Arc::new(Credentials::new( + let credentials4 = Arc::new(Credentials::basic( Some("username4".to_string()), Some("password4".to_string()), )); diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index a0a79133d..c1a957dae 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -1,6 +1,7 @@ use base64::prelude::BASE64_STANDARD; use base64::read::DecoderReader; use base64::write::EncoderWriter; +use std::borrow::Cow; use netrc::Netrc; use reqwest::header::HeaderValue; @@ -12,15 +13,21 @@ use url::Url; use uv_static::EnvVars; #[derive(Clone, Debug, PartialEq)] -pub struct Credentials { - /// The name of the user for authentication. - username: Username, - /// The password to use for authentication. - password: Option, +pub enum Credentials { + Basic { + /// The username to use for authentication. + username: Username, + /// The password to use for authentication. + password: Option, + }, + Bearer { + /// The token to use for authentication. + token: Vec, + }, } #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default)] -pub(crate) struct Username(Option); +pub struct Username(Option); impl Username { /// Create a new username. @@ -61,31 +68,54 @@ impl From> for Username { } impl Credentials { - pub(crate) fn new(username: Option, password: Option) -> Self { - Self { + /// Create a set of HTTP Basic Authentication credentials. + #[allow(dead_code)] + pub(crate) fn basic(username: Option, password: Option) -> Self { + Self::Basic { username: Username::new(username), password, } } + /// Create a set of Bearer Authentication credentials. + #[allow(dead_code)] + pub(crate) fn bearer(token: Vec) -> Self { + Self::Bearer { token } + } + pub fn username(&self) -> Option<&str> { - self.username.as_deref() + match self { + Self::Basic { username, .. } => username.as_deref(), + Self::Bearer { .. } => None, + } } pub(crate) fn to_username(&self) -> Username { - self.username.clone() + match self { + Self::Basic { username, .. } => username.clone(), + Self::Bearer { .. } => Username::none(), + } } - pub(crate) fn as_username(&self) -> &Username { - &self.username + pub(crate) fn as_username(&self) -> Cow<'_, Username> { + match self { + Self::Basic { username, .. } => Cow::Borrowed(username), + Self::Bearer { .. } => Cow::Owned(Username::none()), + } } pub fn password(&self) -> Option<&str> { - self.password.as_deref() + match self { + Self::Basic { password, .. } => password.as_deref(), + Self::Bearer { .. } => None, + } } pub(crate) fn is_empty(&self) -> bool { - self.password.is_none() && self.username.is_none() + match self { + Self::Basic { username, password } => username.is_none() && password.is_none(), + Self::Bearer { token } => token.is_empty(), + } } /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any. @@ -103,7 +133,7 @@ impl Credentials { return None; }; - Some(Credentials { + Some(Credentials::Basic { username: Username::new(Some(entry.login.clone())), password: Some(entry.password.clone()), }) @@ -116,7 +146,7 @@ impl Credentials { if url.username().is_empty() && url.password().is_none() { return None; } - Some(Self { + Some(Self::Basic { // Remove percent-encoding from URL credentials // See username: if url.username().is_empty() { @@ -149,7 +179,7 @@ impl Credentials { if username.is_none() && password.is_none() { None } else { - Some(Self::new(username, password)) + Some(Self::basic(username, password)) } } @@ -169,52 +199,78 @@ impl Credentials { /// Parse [`Credentials`] from an authorization header, if any. /// - /// Only HTTP Basic Authentication is supported. + /// HTTP Basic and Bearer Authentication are both supported. /// [`None`] will be returned if another authorization scheme is detected. /// /// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme: /// - The contents must be base64 encoded /// - There must be a `:` separator pub(crate) fn from_header_value(header: &HeaderValue) -> Option { - let mut value = header.as_bytes().strip_prefix(b"Basic ")?; - let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD); - let mut buf = String::new(); - decoder - .read_to_string(&mut buf) - .expect("HTTP Basic Authentication should be base64 encoded"); - let (username, password) = buf - .split_once(':') - .expect("HTTP Basic Authentication should include a `:` separator"); - let username = if username.is_empty() { - None - } else { - Some(username.to_string()) - }; - let password = if password.is_empty() { - None - } else { - Some(password.to_string()) - }; - Some(Self::new(username, password)) + // Parse a `Basic` authentication header. + if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") { + let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD); + let mut buf = String::new(); + decoder + .read_to_string(&mut buf) + .expect("HTTP Basic Authentication should be base64 encoded"); + let (username, password) = buf + .split_once(':') + .expect("HTTP Basic Authentication should include a `:` separator"); + let username = if username.is_empty() { + None + } else { + Some(username.to_string()) + }; + let password = if password.is_empty() { + None + } else { + Some(password.to_string()) + }; + return Some(Self::Basic { + username: Username::new(username), + password, + }); + } + + // Parse a `Bearer` authentication header. + if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") { + return Some(Self::Bearer { + token: token.to_vec(), + }); + } + + None } /// Create an HTTP Basic Authentication header for the credentials. /// /// Panics if the username or password cannot be base64 encoded. pub(crate) fn to_header_value(&self) -> HeaderValue { - // See: - let mut buf = b"Basic ".to_vec(); - { - let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); - write!(encoder, "{}:", self.username().unwrap_or_default()) - .expect("Write to base64 encoder should succeed"); - if let Some(password) = self.password() { - write!(encoder, "{password}").expect("Write to base64 encoder should succeed"); + match self { + Self::Basic { .. } => { + // See: + let mut buf = b"Basic ".to_vec(); + { + let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); + write!(encoder, "{}:", self.username().unwrap_or_default()) + .expect("Write to base64 encoder should succeed"); + if let Some(password) = self.password() { + write!(encoder, "{password}") + .expect("Write to base64 encoder should succeed"); + } + } + let mut header = + HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); + header.set_sensitive(true); + header + } + Self::Bearer { token } => { + let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat()) + .expect("Bearer token is always valid HeaderValue"); + header.set_sensitive(true); + header } } - let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); - header.set_sensitive(true); - header } /// Apply the credentials to the given URL. diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index 60817a82e..842f2853a 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -80,7 +80,7 @@ impl KeyringProvider { }; } - credentials.map(|(username, password)| Credentials::new(Some(username), Some(password))) + credentials.map(|(username, password)| Credentials::basic(Some(username), Some(password))) } #[instrument(skip(self))] @@ -265,7 +265,7 @@ mod tests { let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); assert_eq!( keyring.fetch(&url, Some("user")).await, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) @@ -274,7 +274,7 @@ mod tests { keyring .fetch(&url.join("test").unwrap(), Some("user")) .await, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) @@ -298,21 +298,21 @@ mod tests { ]); assert_eq!( keyring.fetch(&url.join("foo").unwrap(), Some("user")).await, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); assert_eq!( keyring.fetch(&url, Some("user")).await, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) )) ); assert_eq!( keyring.fetch(&url.join("bar").unwrap(), Some("user")).await, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) )) @@ -326,7 +326,7 @@ mod tests { let credentials = keyring.fetch(&url, Some("user")).await; assert_eq!( credentials, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) @@ -340,7 +340,7 @@ mod tests { let credentials = keyring.fetch(&url, None).await; assert_eq!( credentials, - Some(Credentials::new( + Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 07b8570b8..38c268f29 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -397,7 +397,7 @@ impl AuthMiddleware { None } else if let Some(credentials) = self .cache() - .get_url(request.url(), credentials.as_username()) + .get_url(request.url(), credentials.as_username().as_ref()) { request = credentials.authenticate(request); // Do not insert already-cached credentials @@ -653,7 +653,7 @@ mod tests { let cache = CredentialsCache::new(); cache.insert( &base_url, - Arc::new(Credentials::new( + Arc::new(Credentials::basic( Some(username.to_string()), Some(password.to_string()), )), @@ -707,7 +707,7 @@ mod tests { let cache = CredentialsCache::new(); cache.insert( &base_url, - Arc::new(Credentials::new(Some(username.to_string()), None)), + Arc::new(Credentials::basic(Some(username.to_string()), None)), ); let client = test_client_builder() @@ -1097,7 +1097,7 @@ mod tests { // URL. cache.insert( &base_url, - Arc::new(Credentials::new(Some(username.to_string()), None)), + Arc::new(Credentials::basic(Some(username.to_string()), None)), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some( @@ -1146,14 +1146,14 @@ mod tests { // Seed the cache with our credentials cache.insert( &base_url_1, - Arc::new(Credentials::new( + Arc::new(Credentials::basic( Some(username_1.to_string()), Some(password_1.to_string()), )), ); cache.insert( &base_url_2, - Arc::new(Credentials::new( + Arc::new(Credentials::basic( Some(username_2.to_string()), Some(password_2.to_string()), )), @@ -1341,14 +1341,14 @@ mod tests { // Seed the cache with our credentials cache.insert( &base_url_1, - Arc::new(Credentials::new( + Arc::new(Credentials::basic( Some(username_1.to_string()), Some(password_1.to_string()), )), ); cache.insert( &base_url_2, - Arc::new(Credentials::new( + Arc::new(Credentials::basic( Some(username_2.to_string()), Some(password_2.to_string()), )),