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
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

View File

@@ -70,10 +70,7 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
.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()

View File

@@ -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
{

View File

@@ -8,7 +8,9 @@ use async_trait::async_trait;
pub trait AuthenticationProvider: 'static {
async fn get_principal(&self, id: &str) -> Result<Option<User>, 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
async fn add_app_token(
&self,

View File

@@ -75,29 +75,37 @@ impl AuthenticationProvider for TomlPrincipalStore {
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? {
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<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)
}

View File

@@ -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<Option<rustical_store::auth::User>, rustical_store::Error> {
Err(rustical_store::Error::NotFound)
}
async fn validate_app_token(
&self,
user_id: &str,
token: &str,