From 93b967093c3146a16b572049bf5ebd686dcf1eae Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:00:07 +0200 Subject: [PATCH] Make stricter distinction between password and app tokens --- README.md | 8 +++---- crates/frontend/src/routes/login.rs | 5 +--- crates/store/src/auth/middleware.rs | 2 +- crates/store/src/auth/mod.rs | 4 +++- crates/store/src/auth/toml_user_store.rs | 30 +++++++++++++++--------- src/main.rs | 10 +++++++- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7a158b6..ac9da4c 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,13 @@ docker run -it --rm ghcr.io/lennart-k/rustical rustical pwhash ### Password vs app tokens -The password is meant as a password you use to log in to the frontend. +The password is optional (if you have configured OpenID Connect) and is only used to log in to the frontend. Since it's sensitive information, the secure but slow hash algorithm `argon2` is chosen. -If you've configured OpenID Connect you can also completely omit the password. -I recommend to generate random app tokens for each CalDAV/CardDAV client (which can also be done through the frontend). -These can use the faster `pbkdf2` algorithm. +App tokens are used by your CalDAV/CardDAV client (which can be managed through the frontend). +I recommend to generate random app tokens for each CalDAV/CardDAV client. +Since the app tokens are random they use the faster `pbkdf2` algorithm. ### WebDAV Push diff --git a/crates/frontend/src/routes/login.rs b/crates/frontend/src/routes/login.rs index 4932ab3..25240ed 100644 --- a/crates/frontend/src/routes/login.rs +++ b/crates/frontend/src/routes/login.rs @@ -70,10 +70,7 @@ pub async fn route_post_login( .and_then(|uri| req.full_url().make_relative(&uri)) .unwrap_or(default_redirect); - if let Ok(Some(user)) = auth_provider - .validate_user_token(&username, &password) - .await - { + if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await { session.insert("user", user.id).unwrap(); Redirect::to(redirect_uri) .see_other() diff --git a/crates/store/src/auth/middleware.rs b/crates/store/src/auth/middleware.rs index 292b1b9..70dfde4 100644 --- a/crates/store/src/auth/middleware.rs +++ b/crates/store/src/auth/middleware.rs @@ -70,7 +70,7 @@ where let user_id = auth.as_ref().user_id(); if let Some(password) = auth.as_ref().password() { if let Ok(Some(user)) = auth_provider - .validate_user_token(user_id, password) + .validate_app_token(user_id, password) .instrument(info_span!("validate_user_token")) .await { diff --git a/crates/store/src/auth/mod.rs b/crates/store/src/auth/mod.rs index f3c8a66..c5e335a 100644 --- a/crates/store/src/auth/mod.rs +++ b/crates/store/src/auth/mod.rs @@ -8,7 +8,9 @@ use async_trait::async_trait; pub trait AuthenticationProvider: 'static { async fn get_principal(&self, id: &str) -> Result, crate::Error>; async fn insert_principal(&self, user: User) -> Result<(), crate::Error>; - async fn validate_user_token(&self, user_id: &str, token: &str) -> Result, Error>; + async fn validate_password(&self, user_id: &str, password: &str) + -> Result, Error>; + async fn validate_app_token(&self, user_id: &str, token: &str) -> Result, Error>; /// Returns a token identifier async fn add_app_token( &self, diff --git a/crates/store/src/auth/toml_user_store.rs b/crates/store/src/auth/toml_user_store.rs index ca0780f..db954f4 100644 --- a/crates/store/src/auth/toml_user_store.rs +++ b/crates/store/src/auth/toml_user_store.rs @@ -75,29 +75,37 @@ impl AuthenticationProvider for TomlPrincipalStore { Ok(()) } - async fn validate_user_token(&self, user_id: &str, token: &str) -> Result, Error> { + async fn validate_password( + &self, + user_id: &str, + password_input: &str, + ) -> Result, Error> { let user: User = match self.get_principal(user_id).await? { Some(user) => user, None => return Ok(None), }; - - // Try app tokens first since they are cheaper to calculate - // They can afford less iterations since they can be generated with high entropy - for app_token in &user.app_tokens { - if password_auth::verify_password(token, &app_token.token).is_ok() { - return Ok(Some(user)); - } - } - let password = match &user.password { Some(password) => password, None => return Ok(None), }; - if password_auth::verify_password(token, password).is_ok() { + if password_auth::verify_password(password_input, password).is_ok() { return Ok(Some(user)); } + Ok(None) + } + async fn validate_app_token(&self, user_id: &str, token: &str) -> Result, Error> { + let user: User = match self.get_principal(user_id).await? { + Some(user) => user, + None => return Ok(None), + }; + + for app_token in &user.app_tokens { + if password_auth::verify_password(token, &app_token.token).is_ok() { + return Ok(Some(user)); + } + } Ok(None) } diff --git a/src/main.rs b/src/main.rs index f2e8213..6433681 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,7 +147,15 @@ mod tests { Err(rustical_store::Error::NotFound) } - async fn validate_user_token( + async fn validate_password( + &self, + user_id: &str, + password: &str, + ) -> Result, rustical_store::Error> { + Err(rustical_store::Error::NotFound) + } + + async fn validate_app_token( &self, user_id: &str, token: &str,