dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
//! User password-based authentication.

pub use argon2::password_hash::Error as PasswordError;
use argon2::{
    Argon2,
    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use serde::{Deserialize, Serialize};

use crate::{
    fnd::{
        authn::{Authn, UserHandle},
        identifier::{Uid, UserIdentifier},
    },
    prelude::*,
};

impl<U> Authn<U>
where
    U: UserHandle,
{
    /// Verify the user password and return the user ID.
    pub async fn verify_user_password(
        &self,
        depot: &mut Depot,
        user_ident: &UserIdentifier,
        password: String,
    ) -> Result<Uid, ApiError> {
        let uid_password_hash = self.user_handle.get_password_hash(depot, user_ident).await?;
        if !Password(password)
            .verify_password(uid_password_hash.password_hash.as_str())
            .unwrap_or_default()
        {
            return Err(api_err!(
                ET_USER_VERIF_CODE,
                "Password is not matched.",
                &ERR_PATH_AUTHN
            ));
        }
        Ok(uid_password_hash.user_id)
    }

    /// Reset the user's password.
    pub async fn reset_user_password(
        &self,
        depot: &mut Depot,
        user_ident: &UserIdentifier,
        new_password: String,
    ) -> Result<(), ApiError> {
        let password_hash = Password(new_password)
            .password_hash()
            .map_err(|err| api_err!(ET_USER_REQ_PARAM, &ERR_PATH_AUTHN).with_source(err.into_error(), true))?;
        self.user_handle()
            .set_password_hash(depot, user_ident, password_hash)
            .await
    }
}

/// User password-based authentication.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct Password(
    /// User password
    pub String,
);

impl From<&str> for Password {
    fn from(value: &str) -> Self {
        Self(value.to_owned())
    }
}

impl From<String> for Password {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl Password {
    /// This method hashes a password to a "PHC string" suitable for the
    /// purposes of password-based authentication.
    /// NOTE: The hash string should be stored in the database.
    pub fn password_hash(&self) -> Result<String, PasswordError> {
        let salt = SaltString::generate(&mut OsRng);
        // Argon2 with default params (Argon2id v19)
        let argon2 = Argon2::default();
        // Hash password to PHC string ($argon2id$v=19$...)
        Ok(argon2.hash_password(self.0.as_bytes(), &salt)?.to_string())
    }
    /// Verify password against PHC string.
    pub fn verify_password(&self, password_hash: &str) -> Result<bool, PasswordError> {
        let parsed_hash = PasswordHash::new(password_hash)?;
        Argon2::default()
            .verify_password(self.0.as_bytes(), &parsed_hash)
            .map(|_| true)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test() -> Result<(), PasswordError> {
        let pwd = Password("super_secret_password".to_string());

        let password_hash = pwd.password_hash()?;
        println!("Hashed password: {}", password_hash);

        let is_valid = pwd.verify_password(&password_hash)?;
        println!("Password is valid: {}", is_valid);
        Ok(())
    }
}