pub mod hash;
use base64::{Engine, engine::general_purpose::STANDARD};
#[derive(Debug, Clone)]
pub struct Credential {
username: String,
password: String,
}
impl Credential {
pub fn new(username: String, password: String) -> Self {
Self { username, password }
}
pub fn parse(value: &str) -> Option<Self> {
let (user, pass) = value.split_once(':')?;
if user.is_empty() {
return None;
}
Some(Self::new(user.to_string(), pass.to_string()))
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
}
#[derive(Debug, Clone)]
pub struct Credentials {
entries: Vec<Credential>,
}
impl Credentials {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn from_entries(entries: Vec<Credential>) -> Self {
Self { entries }
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn verify(&self, auth_header: &str) -> bool {
let Some(encoded) = auth_header.strip_prefix("Basic ") else {
return false;
};
let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
return false;
};
let Ok(decoded_str) = String::from_utf8(decoded) else {
return false;
};
let Some((user, password)) = decoded_str.split_once(':') else {
return false;
};
self.check(user, password)
}
fn check(&self, username: &str, password: &str) -> bool {
let mut found = false;
for cred in &self.entries {
let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
let pass_match = hash::verify(password, &cred.password);
if user_match && pass_match {
found = true;
}
}
found
}
}
impl Default for Credentials {
fn default() -> Self {
Self::new()
}
}
pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
if credentials.is_empty() {
return true;
}
match auth_header {
Some(header) => credentials.verify(header),
None => false,
}
}
pub fn check_bearer_token(auth_header: Option<&str>, expected_token: Option<&str>) -> bool {
let Some(expected) = expected_token else {
return true;
};
if expected.is_empty() {
return true;
}
let Some(header) = auth_header else {
return false;
};
let Some(token) = header
.get(..7)
.filter(|prefix| prefix.eq_ignore_ascii_case("bearer "))
.map(|_| &header[7..])
else {
return false;
};
hash::constant_time_eq(token.trim().as_bytes(), expected.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credential_parse_valid() {
let cred = Credential::parse("admin:secret").unwrap();
assert_eq!(cred.username(), "admin");
assert_eq!(cred.password(), "secret");
}
#[test]
fn test_credential_parse_with_colon_in_password() {
let cred = Credential::parse("admin:sec:ret").unwrap();
assert_eq!(cred.username(), "admin");
assert_eq!(cred.password(), "sec:ret");
}
#[test]
fn test_credential_parse_no_colon() {
assert!(Credential::parse("invalid").is_none());
}
#[test]
fn test_credential_parse_empty_user() {
assert!(Credential::parse(":password").is_none());
}
#[test]
fn test_credentials_empty() {
let creds = Credentials::new();
assert!(creds.is_empty());
assert_eq!(creds.len(), 0);
}
#[test]
fn test_credentials_from_entries() {
let entries = vec![
Credential::new("admin".to_string(), "secret".to_string()),
Credential::new("user".to_string(), "pass".to_string()),
];
let creds = Credentials::from_entries(entries);
assert!(!creds.is_empty());
assert_eq!(creds.len(), 2);
}
#[test]
fn test_verify_plain_text() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
let header = format!("Basic {}", STANDARD.encode("admin:secret"));
assert!(creds.verify(&header));
}
#[test]
fn test_verify_wrong_password() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
let header = format!("Basic {}", STANDARD.encode("admin:wrong"));
assert!(!creds.verify(&header));
}
#[test]
fn test_verify_wrong_user() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
let header = format!("Basic {}", STANDARD.encode("wrong:secret"));
assert!(!creds.verify(&header));
}
#[test]
fn test_verify_bcrypt() {
let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
let creds =
Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
let header = format!("Basic {}", STANDARD.encode("user:password"));
assert!(creds.verify(&header));
}
#[test]
fn test_verify_sha1() {
let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let creds =
Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
let header = format!("Basic {}", STANDARD.encode("user:password"));
assert!(creds.verify(&header));
}
#[test]
fn test_verify_apr1() {
let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
let creds =
Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
let header = format!("Basic {}", STANDARD.encode("user:password"));
assert!(creds.verify(&header));
}
#[test]
fn test_verify_multiple_credentials() {
let creds = Credentials::from_entries(vec![
Credential::new("admin".to_string(), "admin123".to_string()),
Credential::new("user1".to_string(), "pass1".to_string()),
Credential::new("user2".to_string(), "pass2".to_string()),
]);
assert!(creds.verify(&format!("Basic {}", STANDARD.encode("admin:admin123"))));
assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user1:pass1"))));
assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user2:pass2"))));
assert!(!creds.verify(&format!("Basic {}", STANDARD.encode("unknown:pass"))));
}
#[test]
fn test_verify_invalid_base64() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
assert!(!creds.verify("Basic not-valid-base64!!!"));
}
#[test]
fn test_verify_non_basic_auth() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
assert!(!creds.verify("Bearer some-token"));
assert!(!creds.verify("bearer some-token"));
}
#[test]
fn test_verify_missing_colon_in_decoded() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
assert!(!creds.verify(&header));
}
#[test]
fn test_check_basic_auth_no_credentials() {
let creds = Credentials::new();
assert!(check_basic_auth(None, &creds));
assert!(check_basic_auth(Some("Basic anything"), &creds));
}
#[test]
fn test_check_basic_auth_with_credentials() {
let creds = Credentials::from_entries(vec![Credential::new(
"admin".to_string(),
"secret".to_string(),
)]);
assert!(!check_basic_auth(None, &creds));
let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
assert!(check_basic_auth(Some(&valid_header), &creds));
let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
assert!(!check_basic_auth(Some(&invalid_header), &creds));
}
#[test]
fn test_check_bearer_token_no_token_configured() {
assert!(check_bearer_token(None, None));
assert!(check_bearer_token(Some("Bearer anything"), None));
}
#[test]
fn test_check_bearer_token_empty_token() {
assert!(check_bearer_token(None, Some("")));
assert!(check_bearer_token(Some("Bearer anything"), Some("")));
}
#[test]
fn test_check_bearer_token_valid() {
let token = "my-secret-token";
let header = "Bearer my-secret-token";
assert!(check_bearer_token(Some(header), Some(token)));
}
#[test]
fn test_check_bearer_token_valid_with_whitespace() {
let token = "my-secret-token";
let header = "Bearer my-secret-token ";
assert!(check_bearer_token(Some(header), Some(token)));
}
#[test]
fn test_check_bearer_token_invalid() {
let token = "my-secret-token";
let header = "Bearer wrong-token";
assert!(!check_bearer_token(Some(header), Some(token)));
}
#[test]
fn test_check_bearer_token_no_header() {
let token = "my-secret-token";
assert!(!check_bearer_token(None, Some(token)));
}
#[test]
fn test_check_bearer_token_basic_auth_header() {
let token = "my-secret-token";
let header = format!("Basic {}", STANDARD.encode("user:pass"));
assert!(!check_bearer_token(Some(&header), Some(token)));
}
#[test]
fn test_check_bearer_token_value_case_sensitive() {
let token = "MySecretToken";
assert!(check_bearer_token(
Some("Bearer MySecretToken"),
Some(token)
));
assert!(!check_bearer_token(
Some("Bearer mysecrettoken"),
Some(token)
));
}
#[test]
fn test_check_bearer_token_prefix_case_insensitive() {
let token = "my-secret-token";
assert!(check_bearer_token(
Some("bearer my-secret-token"),
Some(token)
));
assert!(check_bearer_token(
Some("BEARER my-secret-token"),
Some(token)
));
assert!(check_bearer_token(
Some("Bearer my-secret-token"),
Some(token)
));
}
}