Make stricter distinction between password and app tokens

This commit is contained in:
Lennart
2025-04-14 18:00:07 +02:00
parent 34b20d4ead
commit 93b967093c
6 changed files with 37 additions and 22 deletions

View File

@@ -105,13 +105,13 @@ docker run -it --rm ghcr.io/lennart-k/rustical rustical pwhash
### Password vs app tokens ### 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, Since it's sensitive information,
the secure but slow hash algorithm `argon2` is chosen. 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). App tokens are used by your CalDAV/CardDAV client (which can be managed through the frontend).
These can use the faster `pbkdf2` algorithm. 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 ### WebDAV Push

View File

@@ -70,10 +70,7 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
.and_then(|uri| req.full_url().make_relative(&uri)) .and_then(|uri| req.full_url().make_relative(&uri))
.unwrap_or(default_redirect); .unwrap_or(default_redirect);
if let Ok(Some(user)) = auth_provider if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
.validate_user_token(&username, &password)
.await
{
session.insert("user", user.id).unwrap(); session.insert("user", user.id).unwrap();
Redirect::to(redirect_uri) Redirect::to(redirect_uri)
.see_other() .see_other()

View File

@@ -70,7 +70,7 @@ where
let user_id = auth.as_ref().user_id(); let user_id = auth.as_ref().user_id();
if let Some(password) = auth.as_ref().password() { if let Some(password) = auth.as_ref().password() {
if let Ok(Some(user)) = auth_provider 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")) .instrument(info_span!("validate_user_token"))
.await .await
{ {

View File

@@ -8,7 +8,9 @@ use async_trait::async_trait;
pub trait AuthenticationProvider: 'static { pub trait AuthenticationProvider: 'static {
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>; async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
async fn insert_principal(&self, user: User) -> 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<Option<User>, Error>; async fn validate_password(&self, user_id: &str, password: &str)
-> Result<Option<User>, Error>;
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>;
/// Returns a token identifier /// Returns a token identifier
async fn add_app_token( async fn add_app_token(
&self, &self,

View File

@@ -75,29 +75,37 @@ impl AuthenticationProvider for TomlPrincipalStore {
Ok(()) Ok(())
} }
async fn validate_user_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error> { async fn validate_password(
&self,
user_id: &str,
password_input: &str,
) -> Result<Option<User>, Error> {
let user: User = match self.get_principal(user_id).await? { let user: User = match self.get_principal(user_id).await? {
Some(user) => user, Some(user) => user,
None => return Ok(None), 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 { let password = match &user.password {
Some(password) => password, Some(password) => password,
None => return Ok(None), 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)); return Ok(Some(user));
} }
Ok(None)
}
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, 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) Ok(None)
} }

View File

@@ -147,7 +147,15 @@ mod tests {
Err(rustical_store::Error::NotFound) Err(rustical_store::Error::NotFound)
} }
async fn validate_user_token( async fn validate_password(
&self,
user_id: &str,
password: &str,
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
Err(rustical_store::Error::NotFound)
}
async fn validate_app_token(
&self, &self,
user_id: &str, user_id: &str,
token: &str, token: &str,