1use kellnr_common::login_throttle::LoginThrottle;
2use kellnr_db::{DbProvider, User};
3
4#[derive(Debug, PartialEq, Eq)]
5pub enum PasswordAuthError {
6 Throttled,
8 Invalid,
10}
11
12pub 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 #[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 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}