use base64::Engine;
use reqwest::header::HeaderValue;
use crate::ClientError;
use crate::auth::AuthProvider;
#[derive(Clone)]
pub struct Basic {
user: String,
pass: String,
}
impl Basic {
pub fn new(user: impl Into<String>, pass: impl Into<String>) -> crate::Result<Self> {
let user = user.into();
if user.is_empty() {
return Err(ClientError::Config(
"Basic username must not be empty".into(),
));
}
if user.contains(':') {
return Err(ClientError::Config(
"Basic username must not contain ':' per RFC 7617".into(),
));
}
Ok(Self {
user,
pass: pass.into(),
})
}
}
impl std::fmt::Debug for Basic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Basic")
.field("user", &self.user)
.field("pass", &"<redacted>")
.finish()
}
}
#[async_trait::async_trait]
impl AuthProvider for Basic {
async fn authorization_header(&self) -> crate::Result<HeaderValue> {
let credentials = format!("{}:{}", self.user, self.pass);
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials.as_bytes());
let header = format!("Basic {encoded}");
let mut value = HeaderValue::from_str(&header)
.map_err(|e| ClientError::Auth(format!("invalid Basic header value: {e}")))?;
value.set_sensitive(true);
Ok(value)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
reason = "test code: unwrap on a constructor success is the expected diagnostic"
)]
mod tests {
use super::{AuthProvider, Basic};
#[tokio::test]
async fn header_encodes_user_and_password_in_base64() {
let basic = Basic::new("alice", "wonderland").unwrap();
let header = basic.authorization_header().await.unwrap();
assert_eq!(header, "Basic YWxpY2U6d29uZGVybGFuZA==");
}
#[tokio::test]
async fn header_is_marked_sensitive() {
let basic = Basic::new("alice", "wonderland").unwrap();
let header = basic.authorization_header().await.unwrap();
assert!(
header.is_sensitive(),
"Basic header must be sensitive so downstream log paths redact it"
);
}
#[test]
fn new_rejects_empty_username() {
let err = Basic::new("", "pw").unwrap_err();
assert!(matches!(err, crate::ClientError::Config(_)), "got {err:?}");
}
#[test]
fn new_rejects_username_with_colon_per_rfc_7617() {
let err = Basic::new("alice:bob", "pw").unwrap_err();
assert!(matches!(err, crate::ClientError::Config(_)), "got {err:?}");
}
#[test]
fn debug_redacts_the_password() {
let basic = Basic::new("alice", "wonderland").unwrap();
let s = format!("{basic:?}");
assert!(s.contains("alice"), "user should be visible: {s}");
assert!(!s.contains("wonderland"), "password must be redacted: {s}");
assert!(s.contains("<redacted>"), "redaction marker missing: {s}");
}
}