Refactor error handling in IAM and CMS crates
- Introduced `RepositoryError` and `ServiceError` enums in both IAM and CMS domains for better error management. - Updated domain traits and services to return specific error types instead of `anyhow::Result`. - Enhanced session and OAuth repository implementations to handle errors more explicitly. - Refactored session service methods to return `Result<T, ServiceError>` for improved error handling. - Updated HTTP handlers to utilize the new error types. - Modified password hashing functions to run in a blocking context using `tokio::task::spawn_blocking`. - Added tests for new error handling mechanisms and async password functions.
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
//! Uses the `argon2` crate (Argon2id variant) with default parameters,
|
||||
//! which provide a good security / performance trade-off for interactive
|
||||
//! authentication.
|
||||
//!
|
||||
//! CPU-bound hashing is wrapped in `tokio::task::spawn_blocking` so the
|
||||
//! async runtime is not blocked by Argon2's memory-hard computation.
|
||||
|
||||
use anyhow::Result;
|
||||
use argon2::{
|
||||
@@ -10,6 +13,7 @@ use argon2::{
|
||||
Argon2,
|
||||
};
|
||||
use rand_core::OsRng;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing;
|
||||
|
||||
/// Hash a plaintext password using Argon2id with a random salt.
|
||||
@@ -17,68 +21,85 @@ use tracing;
|
||||
/// The returned string is in the PHC string format
|
||||
/// (`$argon2id$v=19$...`) and can be stored directly in the database.
|
||||
///
|
||||
/// The CPU-bound hashing runs on a blocking thread pool via
|
||||
/// `spawn_blocking` so it does not starve the async runtime.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the argon2 library fails (extremely rare —
|
||||
/// typically indicates an OOM or system-level crypto failure).
|
||||
pub fn hash_password(password: &str) -> Result<String> {
|
||||
let salt = SaltString::generate(&mut OsRng); // cryptographic random salt
|
||||
let argon2 = Argon2::default(); // Argon2id with default params
|
||||
let hash = argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?;
|
||||
tracing::debug!("password hashed successfully");
|
||||
Ok(hash.to_string())
|
||||
/// typically indicates an OOM or system-level crypto failure), or if
|
||||
/// the blocking task fails to spawn.
|
||||
pub async fn hash_password(password: &str) -> Result<String> {
|
||||
let password = password.to_string();
|
||||
spawn_blocking(move || {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?;
|
||||
tracing::debug!("password hashed successfully");
|
||||
Ok(hash.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("blocking task failed: {e}"))?
|
||||
}
|
||||
|
||||
/// Verify a plaintext password against a previously-hashed PHC string.
|
||||
///
|
||||
/// Returns `Ok(true)` if the password matches, `Ok(false)` if it does not,
|
||||
/// and `Err` if the hash string is malformed.
|
||||
/// and `Err` if the hash string is malformed or the blocking task fails
|
||||
/// to spawn.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the hash string is not a valid PHC string or if
|
||||
/// the argon2 library encounters an internal failure.
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
||||
let parsed_hash = PasswordHash::new(hash)
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse password hash: {e}"))?;
|
||||
let argon2 = Argon2::default(); // Argon2id with default params
|
||||
let valid = argon2
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok();
|
||||
tracing::debug!("password verification result: {valid}");
|
||||
Ok(valid)
|
||||
/// Returns an error if the hash string is not a valid PHC string, if the
|
||||
/// argon2 library encounters an internal failure, or if the blocking task
|
||||
/// fails to spawn.
|
||||
pub async fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
||||
let password = password.to_string();
|
||||
let hash = hash.to_string();
|
||||
spawn_blocking(move || {
|
||||
let parsed_hash = PasswordHash::new(&hash)
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse password hash: {e}"))?;
|
||||
let argon2 = Argon2::default();
|
||||
let valid = argon2
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok();
|
||||
tracing::debug!("password verification result: {valid}");
|
||||
Ok(valid)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("blocking task failed: {e}"))?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash_and_verify() {
|
||||
#[tokio::test]
|
||||
async fn test_hash_and_verify() {
|
||||
let password = "my-secure-password-123!";
|
||||
let hash = hash_password(password).unwrap();
|
||||
assert!(verify_password(password, &hash).unwrap());
|
||||
let hash = hash_password(password).await.unwrap();
|
||||
assert!(verify_password(password, &hash).await.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_password_fails() {
|
||||
let hash = hash_password("correct-password").unwrap();
|
||||
assert!(!verify_password("wrong-password", &hash).unwrap());
|
||||
#[tokio::test]
|
||||
async fn test_wrong_password_fails() {
|
||||
let hash = hash_password("correct-password").await.unwrap();
|
||||
assert!(!verify_password("wrong-password", &hash).await.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashes_are_different() {
|
||||
let h1 = hash_password("same-password").unwrap();
|
||||
let h2 = hash_password("same-password").unwrap();
|
||||
#[tokio::test]
|
||||
async fn test_hashes_are_different() {
|
||||
let h1 = hash_password("same-password").await.unwrap();
|
||||
let h2 = hash_password("same-password").await.unwrap();
|
||||
// Different salts → different hashes.
|
||||
assert_ne!(h1, h2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_hash_returns_error() {
|
||||
let result = verify_password("password", "not-a-valid-hash");
|
||||
#[tokio::test]
|
||||
async fn test_invalid_hash_returns_error() {
|
||||
let result = verify_password("password", "not-a-valid-hash").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user