use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use std::time::Instant;
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, Clone)]
pub enum BasicAuthCredentialError {
#[error("Username is invalid: {reason}")]
InvalidUsername { reason: &'static str },
#[error("Password is invalid: {reason}")]
InvalidPassword { reason: &'static str },
}
#[derive(Clone, Debug)]
pub struct BasicAuthCredential {
username: Arc<SecretString>,
password: Arc<SecretString>,
expires_on: Option<Instant>,
}
impl BasicAuthCredential {
pub fn new(
username: impl Into<SecretString>,
password: impl Into<SecretString>,
) -> Result<Self, BasicAuthCredentialError> {
let username: SecretString = username.into();
Self::validate_username(&username)?;
let password: SecretString = password.into();
Self::validate_password(&password)?;
Ok(Self {
username: Arc::new(username),
password: Arc::new(password),
expires_on: None,
})
}
#[must_use]
pub const fn with_expiry(mut self, expires_on: Instant) -> Self {
self.expires_on = Some(expires_on);
self
}
#[must_use]
pub fn expose_username(&self) -> &str {
self.username.expose_secret()
}
#[must_use]
pub fn expose_password(&self) -> &str {
self.password.expose_secret()
}
#[must_use]
pub const fn expires_on(&self) -> Option<Instant> {
self.expires_on
}
pub fn validate_username(username: &SecretString) -> Result<(), BasicAuthCredentialError> {
let username_str = username.expose_secret();
if username_str.is_empty() {
return Err(BasicAuthCredentialError::InvalidUsername {
reason: "Username cannot be empty",
});
}
for c in username_str.chars() {
match c {
':' => {
return Err(BasicAuthCredentialError::InvalidUsername {
reason: "Username cannot contain the ':' character",
});
}
c if c.is_control() => {
return Err(BasicAuthCredentialError::InvalidUsername {
reason: "Username cannot contain control characters",
});
}
_ => {}
}
}
Ok(())
}
pub fn validate_password(password: &SecretString) -> Result<(), BasicAuthCredentialError> {
let password_str = password.expose_secret();
if password_str.is_empty() {
return Err(BasicAuthCredentialError::InvalidPassword {
reason: "Password cannot be empty",
});
}
if password_str.chars().any(char::is_control) {
return Err(BasicAuthCredentialError::InvalidPassword {
reason: "Password cannot contain control characters",
});
}
Ok(())
}
}