Skip to main content

kellnr_http/
password.rs

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