use dashmap::DashMap;
use std::sync::Arc;
use super::AuthResult;
use crate::config::AuthConfig;
pub trait Authenticator: Send + Sync {
fn authenticate(&self, username: &str, password: &str) -> AuthResult<()>;
}
#[derive(Debug, Clone)]
struct LoginAttempt {
remaining_attempts: u32,
}
pub type UserVerifier = Arc<dyn Fn(&str, &str) -> AuthResult<bool> + Send + Sync>;
pub struct PasswordAuthenticator {
user_verifier: UserVerifier,
config: AuthConfig,
login_attempts: Arc<DashMap<String, LoginAttempt>>,
}
impl Clone for PasswordAuthenticator {
fn clone(&self) -> Self {
Self {
user_verifier: Arc::clone(&self.user_verifier),
config: self.config.clone(),
login_attempts: Arc::clone(&self.login_attempts),
}
}
}
impl PasswordAuthenticator {
pub fn new<F>(user_verifier: F, config: AuthConfig) -> Self
where
F: Fn(&str, &str) -> AuthResult<bool> + Send + Sync + 'static,
{
Self {
user_verifier: Arc::new(user_verifier),
config,
login_attempts: Arc::new(DashMap::new()),
}
}
pub fn new_default(config: AuthConfig) -> Self {
let default_username = config.default_username.clone();
let default_password = config.bootstrap_password.clone();
Self::new(
move |username: &str, password: &str| {
if username == default_username && password == default_password {
Ok(true)
} else {
Ok(false)
}
},
config,
)
}
pub fn new_with_storage<S>(storage: Arc<S>, config: AuthConfig) -> Self
where
S: crate::storage::StorageAuthOps + 'static,
{
Self::new(
move |username: &str, password: &str| {
let Some(user) = storage.get_user(username) else {
return Ok(false);
};
if user.is_locked {
return Err(crate::auth::AuthError::AccountLocked(username.to_string()));
}
Ok(user.verify_password(password))
},
config,
)
}
pub fn config(&self) -> &AuthConfig {
&self.config
}
fn record_failed_attempt(&self, username: &str) {
if self.config.failed_login_attempts == 0 {
return;
}
let username_key = username.to_string();
self.login_attempts
.entry(username_key.clone())
.or_insert(LoginAttempt {
remaining_attempts: self.config.failed_login_attempts,
});
if let Some(mut attempt) = self.login_attempts.get_mut(&username_key) {
if attempt.remaining_attempts > 0 {
attempt.remaining_attempts -= 1;
}
}
}
fn reset_attempts(&self, username: &str) {
self.login_attempts.remove(username);
}
fn verify_password(&self, username: &str, password: &str) -> AuthResult<bool> {
(self.user_verifier)(username, password)
}
}
impl Authenticator for PasswordAuthenticator {
fn authenticate(&self, username: &str, password: &str) -> AuthResult<()> {
use crate::auth::AuthError;
if !self.config.enable_authorize {
return Ok(());
}
if username.is_empty() || password.is_empty() {
return Err(AuthError::EmptyCredentials);
}
match self.verify_password(username, password) {
Ok(true) => {
self.reset_attempts(username);
Ok(())
}
Ok(false) => {
self.record_failed_attempt(username);
let username_key = username.to_string();
if let Some(attempt) = self.login_attempts.get(&username_key) {
if attempt.remaining_attempts > 0 {
return Err(AuthError::InvalidCredentials(attempt.remaining_attempts));
} else {
return Err(AuthError::MaxAttemptsExceeded);
}
}
Err(AuthError::InvalidCredentials(0))
}
Err(e) => Err(e),
}
}
}
pub struct AuthenticatorFactory;
impl AuthenticatorFactory {
pub fn create<F>(config: &AuthConfig, user_verifier: F) -> PasswordAuthenticator
where
F: Fn(&str, &str) -> AuthResult<bool> + Send + Sync + 'static,
{
PasswordAuthenticator::new(user_verifier, config.clone())
}
pub fn create_default(config: &AuthConfig) -> PasswordAuthenticator {
PasswordAuthenticator::new_default(config.clone())
}
pub fn create_with_storage<S>(config: &AuthConfig, storage: Arc<S>) -> PasswordAuthenticator
where
S: crate::storage::StorageAuthOps + 'static,
{
PasswordAuthenticator::new_with_storage(storage, config.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> AuthConfig {
AuthConfig {
enable_authorize: true,
failed_login_attempts: 3,
session_idle_timeout_secs: 3600,
default_username: "test".to_string(),
bootstrap_password: "test123".to_string(),
force_change_default_password: true,
bcrypt_cost: 12,
}
}
#[test]
fn test_password_authenticator_success() {
let config = create_test_config();
let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(true), config);
assert!(auth.authenticate("user", "pass").is_ok());
}
#[test]
fn test_password_authenticator_failure() {
let config = create_test_config();
let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(false), config);
assert!(auth.authenticate("user", "wrong_pass").is_err());
}
#[test]
fn test_password_authenticator_default() {
let config = AuthConfig {
enable_authorize: true,
failed_login_attempts: 0, session_idle_timeout_secs: 3600,
default_username: "admin".to_string(),
bootstrap_password: "admin123".to_string(),
force_change_default_password: false,
bcrypt_cost: 12,
};
let auth = PasswordAuthenticator::new_default(config);
assert!(auth.authenticate("admin", "admin123").is_ok());
assert!(auth.authenticate("admin", "wrong").is_err());
assert!(auth.authenticate("user", "admin123").is_err());
}
#[test]
fn test_login_attempt_limit() {
let config = AuthConfig {
enable_authorize: true,
failed_login_attempts: 2, session_idle_timeout_secs: 3600,
default_username: "test".to_string(),
bootstrap_password: "test123".to_string(),
force_change_default_password: false,
bcrypt_cost: 12,
};
let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(false), config);
let result1 = auth.authenticate("user", "wrong");
assert!(result1.is_err());
assert!(result1
.unwrap_err()
.to_string()
.contains("1 attempts remaining"));
let result2 = auth.authenticate("user", "wrong");
assert!(result2.is_err());
assert!(result2
.unwrap_err()
.to_string()
.contains("Maximum attempts exceeded"));
let result3 = auth.authenticate("user", "wrong");
assert!(result3.is_err());
assert!(result3
.unwrap_err()
.to_string()
.contains("Maximum attempts exceeded"));
}
#[test]
fn test_successful_login_resets_attempts() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let config = AuthConfig {
enable_authorize: true,
failed_login_attempts: 2,
session_idle_timeout_secs: 3600,
default_username: "test".to_string(),
bootstrap_password: "test123".to_string(),
force_change_default_password: false,
bcrypt_cost: 12,
};
let success = Arc::new(AtomicBool::new(false));
let success_clone = success.clone();
let auth = PasswordAuthenticator::new(
move |_username: &str, _password: &str| {
if success_clone.load(Ordering::SeqCst) {
Ok(true)
} else {
Ok(false)
}
},
config,
);
assert!(auth.authenticate("user", "wrong").is_err());
success.store(true, Ordering::SeqCst);
assert!(auth.authenticate("user", "correct").is_ok());
success.store(false, Ordering::SeqCst);
assert!(auth.authenticate("user", "wrong").is_err());
assert!(auth.authenticate("user", "wrong").is_err());
}
#[test]
fn test_authenticator_factory() {
let config = AuthConfig {
enable_authorize: true,
failed_login_attempts: 0,
session_idle_timeout_secs: 3600,
default_username: "test".to_string(),
bootstrap_password: "test123".to_string(),
force_change_default_password: false,
bcrypt_cost: 12,
};
let _auth =
AuthenticatorFactory::create(&config, |_username: &str, _password: &str| Ok(true));
let _auth_default = AuthenticatorFactory::create_default(&config);
}
}