Skip to main content

linkrs_server/auth/
authenticator.rs

1use dashmap::DashMap;
2use std::sync::Arc;
3
4use super::AuthResult;
5use crate::config::AuthConfig;
6
7/// Authentication trait
8pub trait Authenticator: Send + Sync {
9    fn authenticate(&self, username: &str, password: &str) -> AuthResult<()>;
10}
11
12/// Login Failure Record
13#[derive(Debug, Clone)]
14struct LoginAttempt {
15    /// Remaining attempts
16    remaining_attempts: u32,
17}
18
19/// User Authentication Callback Function Types
20pub type UserVerifier = Arc<dyn Fn(&str, &str) -> AuthResult<bool> + Send + Sync>;
21
22/// Password Authenticator - Supports failed login rate limiting and storage-backed account lockout.
23///
24/// The in-memory failure counter is rate limiting only and never locks the
25/// account. The storage `is_locked` flag is the sole source of truth for
26/// lockout; enabling the account clears it.
27pub struct PasswordAuthenticator {
28    /// User Authentication Callbacks
29    user_verifier: UserVerifier,
30    config: AuthConfig,
31    /// Logging of user login attempts - using DashMap for high-performance concurrent access
32    login_attempts: Arc<DashMap<String, LoginAttempt>>,
33}
34
35impl Clone for PasswordAuthenticator {
36    fn clone(&self) -> Self {
37        Self {
38            user_verifier: Arc::clone(&self.user_verifier),
39            config: self.config.clone(),
40            login_attempts: Arc::clone(&self.login_attempts),
41        }
42    }
43}
44
45impl PasswordAuthenticator {
46    pub fn new<F>(user_verifier: F, config: AuthConfig) -> Self
47    where
48        F: Fn(&str, &str) -> AuthResult<bool> + Send + Sync + 'static,
49    {
50        Self {
51            user_verifier: Arc::new(user_verifier),
52            config,
53            login_attempts: Arc::new(DashMap::new()),
54        }
55    }
56
57    /// Create default password authenticator (supports configured username and password)
58    pub fn new_default(config: AuthConfig) -> Self {
59        let default_username = config.default_username.clone();
60        let default_password = config.bootstrap_password.clone();
61
62        Self::new(
63            move |username: &str, password: &str| {
64                // Use the configured default username and password
65                if username == default_username && password == default_password {
66                    Ok(true)
67                } else {
68                    Ok(false)
69                }
70            },
71            config,
72        )
73    }
74
75    /// Create a storage-backed verifier querying user records.
76    ///
77    /// Unknown users and wrong passwords both report credential failure so
78    /// they stay indistinguishable; locked accounts report a distinct error.
79    /// Password hash verification runs wherever `authenticate` runs, so call
80    /// sites must execute it on a blocking thread.
81    pub fn new_with_storage<S>(storage: Arc<S>, config: AuthConfig) -> Self
82    where
83        S: crate::storage::StorageAuthOps + 'static,
84    {
85        Self::new(
86            move |username: &str, password: &str| {
87                let Some(user) = storage.get_user(username) else {
88                    return Ok(false);
89                };
90                if user.is_locked {
91                    return Err(crate::auth::AuthError::AccountLocked(username.to_string()));
92                }
93                Ok(user.verify_password(password))
94            },
95            config,
96        )
97    }
98
99    /// Auth configuration backing this authenticator.
100    pub fn config(&self) -> &AuthConfig {
101        &self.config
102    }
103
104    /// Record Login Failure
105    fn record_failed_attempt(&self, username: &str) {
106        // If the login failure restriction is not enabled, it returns directly
107        if self.config.failed_login_attempts == 0 {
108            return;
109        }
110
111        let username_key = username.to_string();
112        self.login_attempts
113            .entry(username_key.clone())
114            .or_insert(LoginAttempt {
115                remaining_attempts: self.config.failed_login_attempts,
116            });
117
118        // Reduce the number of remaining attempts
119        if let Some(mut attempt) = self.login_attempts.get_mut(&username_key) {
120            if attempt.remaining_attempts > 0 {
121                attempt.remaining_attempts -= 1;
122            }
123        }
124    }
125
126    /// Reset logging of login attempts (called on successful login)
127    fn reset_attempts(&self, username: &str) {
128        self.login_attempts.remove(username);
129    }
130
131    /// Verify user passwords
132    fn verify_password(&self, username: &str, password: &str) -> AuthResult<bool> {
133        (self.user_verifier)(username, password)
134    }
135}
136
137impl Authenticator for PasswordAuthenticator {
138    fn authenticate(&self, username: &str, password: &str) -> AuthResult<()> {
139        use crate::auth::AuthError;
140
141        // Check if authorization is enabled
142        if !self.config.enable_authorize {
143            return Ok(());
144        }
145
146        if username.is_empty() || password.is_empty() {
147            return Err(AuthError::EmptyCredentials);
148        }
149
150        // Verify Password
151        match self.verify_password(username, password) {
152            Ok(true) => {
153                // Login successful, reset attempt log
154                self.reset_attempts(username);
155                Ok(())
156            }
157            Ok(false) => {
158                // Login failed, logging attempt
159                self.record_failed_attempt(username);
160
161                let username_key = username.to_string();
162                if let Some(attempt) = self.login_attempts.get(&username_key) {
163                    if attempt.remaining_attempts > 0 {
164                        return Err(AuthError::InvalidCredentials(attempt.remaining_attempts));
165                    } else {
166                        return Err(AuthError::MaxAttemptsExceeded);
167                    }
168                }
169
170                Err(AuthError::InvalidCredentials(0))
171            }
172            Err(e) => Err(e),
173        }
174    }
175}
176
177/// Authenticator Factory
178pub struct AuthenticatorFactory;
179
180impl AuthenticatorFactory {
181    /// Creating a Password Authenticator
182    pub fn create<F>(config: &AuthConfig, user_verifier: F) -> PasswordAuthenticator
183    where
184        F: Fn(&str, &str) -> AuthResult<bool> + Send + Sync + 'static,
185    {
186        PasswordAuthenticator::new(user_verifier, config.clone())
187    }
188
189    /// Creating a default password authenticator
190    pub fn create_default(config: &AuthConfig) -> PasswordAuthenticator {
191        PasswordAuthenticator::new_default(config.clone())
192    }
193
194    /// Storage-backed authenticator querying user records.
195    pub fn create_with_storage<S>(config: &AuthConfig, storage: Arc<S>) -> PasswordAuthenticator
196    where
197        S: crate::storage::StorageAuthOps + 'static,
198    {
199        PasswordAuthenticator::new_with_storage(storage, config.clone())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn create_test_config() -> AuthConfig {
208        AuthConfig {
209            enable_authorize: true,
210            failed_login_attempts: 3,
211            session_idle_timeout_secs: 3600,
212            default_username: "test".to_string(),
213            bootstrap_password: "test123".to_string(),
214            force_change_default_password: true,
215            bcrypt_cost: 12,
216        }
217    }
218
219    #[test]
220    fn test_password_authenticator_success() {
221        let config = create_test_config();
222        let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(true), config);
223
224        assert!(auth.authenticate("user", "pass").is_ok());
225    }
226
227    #[test]
228    fn test_password_authenticator_failure() {
229        let config = create_test_config();
230        let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(false), config);
231
232        assert!(auth.authenticate("user", "wrong_pass").is_err());
233    }
234
235    #[test]
236    fn test_password_authenticator_default() {
237        let config = AuthConfig {
238            enable_authorize: true,
239            failed_login_attempts: 0, // Disable Login Restrictions
240            session_idle_timeout_secs: 3600,
241            default_username: "admin".to_string(),
242            bootstrap_password: "admin123".to_string(),
243            force_change_default_password: false,
244            bcrypt_cost: 12,
245        };
246
247        let auth = PasswordAuthenticator::new_default(config);
248
249        // Use the correct default credentials
250        assert!(auth.authenticate("admin", "admin123").is_ok());
251        // Using the wrong credentials
252        assert!(auth.authenticate("admin", "wrong").is_err());
253        assert!(auth.authenticate("user", "admin123").is_err());
254    }
255
256    #[test]
257    fn test_login_attempt_limit() {
258        let config = AuthConfig {
259            enable_authorize: true,
260            failed_login_attempts: 2, // Maximum 2 failures
261            session_idle_timeout_secs: 3600,
262            default_username: "test".to_string(),
263            bootstrap_password: "test123".to_string(),
264            force_change_default_password: false,
265            bcrypt_cost: 12,
266        };
267
268        let auth = PasswordAuthenticator::new(|_username: &str, _password: &str| Ok(false), config);
269
270        // First failure - 1 to go
271        let result1 = auth.authenticate("user", "wrong");
272        assert!(result1.is_err());
273        assert!(result1
274            .unwrap_err()
275            .to_string()
276            .contains("1 attempts remaining"));
277
278        // Second failure - maximum number of attempts reached
279        let result2 = auth.authenticate("user", "wrong");
280        assert!(result2.is_err());
281        assert!(result2
282            .unwrap_err()
283            .to_string()
284            .contains("Maximum attempts exceeded"));
285
286        // Third Failure - still showing that the maximum number of attempts has been reached
287        let result3 = auth.authenticate("user", "wrong");
288        assert!(result3.is_err());
289        assert!(result3
290            .unwrap_err()
291            .to_string()
292            .contains("Maximum attempts exceeded"));
293    }
294
295    #[test]
296    fn test_successful_login_resets_attempts() {
297        // Using Arc<AtomicBool> to share mutable state in a closure
298        use std::sync::atomic::{AtomicBool, Ordering};
299        use std::sync::Arc;
300
301        let config = AuthConfig {
302            enable_authorize: true,
303            failed_login_attempts: 2,
304            session_idle_timeout_secs: 3600,
305            default_username: "test".to_string(),
306            bootstrap_password: "test123".to_string(),
307            force_change_default_password: false,
308            bcrypt_cost: 12,
309        };
310
311        let success = Arc::new(AtomicBool::new(false));
312        let success_clone = success.clone();
313
314        let auth = PasswordAuthenticator::new(
315            move |_username: &str, _password: &str| {
316                if success_clone.load(Ordering::SeqCst) {
317                    Ok(true)
318                } else {
319                    Ok(false)
320                }
321            },
322            config,
323        );
324
325        // First failure.
326        assert!(auth.authenticate("user", "wrong").is_err());
327
328        // Successful logins should reset the failure count
329        success.store(true, Ordering::SeqCst);
330        assert!(auth.authenticate("user", "correct").is_ok());
331
332        // Failed again and should be recounted
333        success.store(false, Ordering::SeqCst);
334        assert!(auth.authenticate("user", "wrong").is_err());
335        // One more chance.
336        assert!(auth.authenticate("user", "wrong").is_err());
337    }
338
339    #[test]
340    fn test_authenticator_factory() {
341        let config = AuthConfig {
342            enable_authorize: true,
343            failed_login_attempts: 0,
344            session_idle_timeout_secs: 3600,
345            default_username: "test".to_string(),
346            bootstrap_password: "test123".to_string(),
347            force_change_default_password: false,
348            bcrypt_cost: 12,
349        };
350
351        let _auth =
352            AuthenticatorFactory::create(&config, |_username: &str, _password: &str| Ok(true));
353        // Verify successful creation (no longer return Result, just create successfully)
354
355        let _auth_default = AuthenticatorFactory::create_default(&config);
356        // Verify successful creation
357    }
358}