Skip to main content

kellnr_auth/
password.rs

1use kellnr_common::login_throttle::LoginThrottle;
2use kellnr_db::{DbProvider, User};
3
4#[derive(Debug, PartialEq, Eq)]
5pub enum PasswordAuthError {
6    /// Too many attempts for this username; the password was not checked.
7    Throttled,
8    /// Unknown user or wrong password.
9    Invalid,
10}
11
12/// Checks a username and password, subject to the per-username attempt
13/// throttle. Every password check in Kellnr goes through here, so brute
14/// forcing is limited no matter which endpoint is used.
15pub async fn authenticate_password(
16    db: &dyn DbProvider,
17    throttle: &LoginThrottle,
18    user: &str,
19    pwd: &str,
20) -> Result<User, PasswordAuthError> {
21    if !throttle.try_attempt(user).await {
22        return Err(PasswordAuthError::Throttled);
23    }
24
25    let user = db
26        .authenticate_user(user, pwd)
27        .await
28        .map_err(|_| PasswordAuthError::Invalid)?;
29    throttle.succeeded(&user.name).await;
30    Ok(user)
31}
32
33#[cfg(test)]
34mod tests {
35    use std::time::Duration;
36
37    use kellnr_db::error::DbError;
38    use kellnr_db::mock::MockDb;
39
40    use super::*;
41
42    fn throttle(max_attempts: u32) -> LoginThrottle {
43        LoginThrottle::new(max_attempts, Duration::from_mins(5))
44    }
45
46    fn user() -> User {
47        User {
48            name: "user".to_string(),
49            ..User::default()
50        }
51    }
52
53    #[tokio::test]
54    async fn valid_password_authenticates() {
55        let mut db = MockDb::new();
56        db.expect_authenticate_user().returning(|_, _| Ok(user()));
57
58        let result = authenticate_password(&db, &throttle(3), "user", "pwd").await;
59
60        assert_eq!(result.unwrap().name, "user");
61    }
62
63    /// Once the limit is reached, the password is not checked at all, so a
64    /// correct guess cannot be confirmed either.
65    #[tokio::test]
66    async fn throttles_after_limit_without_checking_password() {
67        let mut db = MockDb::new();
68        db.expect_authenticate_user()
69            .times(2)
70            .returning(|_, _| Err(DbError::PasswordMismatch));
71        let throttle = throttle(2);
72
73        for _ in 0..2 {
74            let result = authenticate_password(&db, &throttle, "user", "wrong").await;
75            assert_eq!(result.unwrap_err(), PasswordAuthError::Invalid);
76        }
77        let result = authenticate_password(&db, &throttle, "user", "right").await;
78        assert_eq!(result.unwrap_err(), PasswordAuthError::Throttled);
79    }
80
81    #[tokio::test]
82    async fn unknown_user_is_throttled() {
83        let mut db = MockDb::new();
84        db.expect_authenticate_user()
85            .times(1)
86            .returning(|name, _| Err(DbError::UserNotFound(name.to_string())));
87        let throttle = throttle(1);
88
89        let first = authenticate_password(&db, &throttle, "ghost", "pwd").await;
90        let second = authenticate_password(&db, &throttle, "ghost", "pwd").await;
91
92        assert_eq!(first.unwrap_err(), PasswordAuthError::Invalid);
93        assert_eq!(second.unwrap_err(), PasswordAuthError::Throttled);
94    }
95
96    #[tokio::test]
97    async fn success_resets_attempts() {
98        let mut db = MockDb::new();
99        db.expect_authenticate_user()
100            .withf(|_, pwd| pwd == "right")
101            .returning(|_, _| Ok(user()));
102        db.expect_authenticate_user()
103            .withf(|_, pwd| pwd == "wrong")
104            .returning(|_, _| Err(DbError::PasswordMismatch));
105        let throttle = throttle(2);
106
107        let _ = authenticate_password(&db, &throttle, "user", "wrong").await;
108        authenticate_password(&db, &throttle, "user", "right")
109            .await
110            .unwrap();
111
112        // Both attempts are available again after the successful login.
113        for _ in 0..2 {
114            let result = authenticate_password(&db, &throttle, "user", "wrong").await;
115            assert_eq!(result.unwrap_err(), PasswordAuthError::Invalid);
116        }
117    }
118}