From acd5939410affa5a2145a2fa03f04051c3e78cd0 Mon Sep 17 00:00:00 2001 From: Kent Tristan Yves Sarmiento Date: Tue, 26 Dec 2023 06:49:19 +0000 Subject: [PATCH] feat: password hashing using argon2 --- Cargo.lock | 39 +++++++++++++++++++++++++++++ link-for-later/Cargo.toml | 1 + link-for-later/src/service/users.rs | 38 ++++++++++++++++++++++------ link-for-later/tests/users.rs | 27 +++++++++++++++++--- 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8b80e6..bad8d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,18 @@ version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -276,6 +288,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -300,6 +318,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1315,6 +1342,7 @@ checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" name = "link-for-later" version = "0.1.0" dependencies = [ + "argon2", "axum 0.7.2", "axum-extra", "bson", @@ -1757,6 +1785,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/link-for-later/Cargo.toml b/link-for-later/Cargo.toml index b55b6ee..301df1f 100644 --- a/link-for-later/Cargo.toml +++ b/link-for-later/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/kentSarmiento/link-for-later-service" publish = false [dependencies] +argon2 = "0.5.2" axum = "0.7.2" axum-extra = { version = "0.9.0", default-features = false, features=["typed-header"] } bson = "2.8.1" diff --git a/link-for-later/src/service/users.rs b/link-for-later/src/service/users.rs index 5fe8618..71f399c 100644 --- a/link-for-later/src/service/users.rs +++ b/link-for-later/src/service/users.rs @@ -1,3 +1,7 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; use axum::async_trait; use chrono::{DateTime, Duration, Utc}; use jsonwebtoken::{encode, EncodingKey, Header}; @@ -33,8 +37,16 @@ impl UsersService for ServiceProvider { }; let now = Utc::now().to_rfc3339(); - // TODO: secure password - let registered_user_info = UserInfoBuilder::from(user_info.clone()) + + let password_hash = Argon2::default() + .hash_password( + user_info.password().as_bytes(), + &SaltString::generate(&mut OsRng), + ) + .map_err(|e| AppError::ServerError(format!("hash_password() {e:?}")))? + .to_string(); + + let registered_user_info = UserInfoBuilder::new(user_info.email(), &password_hash) .created_at(&now) .updated_at(&now) .verified(true) @@ -51,9 +63,11 @@ impl UsersService for ServiceProvider { let user_query = UserQueryBuilder::new(user_info.email()).build(); let retrieved_user_info = users_repo.get(&user_query).await?; - if retrieved_user_info.password() != user_info.password() { - return Err(AppError::IncorrectPassword(user_info.email().to_owned())); - } + let parsed_hash = PasswordHash::new(retrieved_user_info.password()) + .map_err(|e| AppError::ServerError(format!("PasswordHash::new() {e:?}")))?; + Argon2::default() + .verify_password(user_info.password().as_bytes(), &parsed_hash) + .map_err(|_| AppError::IncorrectPassword(user_info.email().to_owned()))?; let timestamp = |timestamp: DateTime| -> Result { let timestamp: usize = timestamp @@ -199,9 +213,14 @@ mod tests { async fn test_login_user() { let repo_query = UserQueryBuilder::new("user@test.com").build(); let user_to_login = UserInfoBuilder::new("user@test.com", "test").build(); - let registered_user = UserInfoBuilder::new("user@test.com", "test").build(); let request_item = user_to_login.clone(); + let password_hash = Argon2::default() + .hash_password(b"test", &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + let registered_user = UserInfoBuilder::new("user@test.com", &password_hash).build(); + let mut mock_users_repo = MockUsersRepo::new(); mock_users_repo .expect_get() @@ -245,9 +264,14 @@ mod tests { async fn test_login_user_incorrect_password() { let repo_query = UserQueryBuilder::new("user@test.com").build(); let user_to_login = UserInfoBuilder::new("user@test.com", "incorrect").build(); - let registered_user = UserInfoBuilder::new("user@test.com", "test").build(); let request_item = user_to_login.clone(); + let password_hash = Argon2::default() + .hash_password(b"test", &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + let registered_user = UserInfoBuilder::new("user@test.com", &password_hash).build(); + let mut mock_users_repo = MockUsersRepo::new(); mock_users_repo .expect_get() diff --git a/link-for-later/tests/users.rs b/link-for-later/tests/users.rs index a635263..b12cbed 100644 --- a/link-for-later/tests/users.rs +++ b/link-for-later/tests/users.rs @@ -1,3 +1,7 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; use axum::{ body::Body, http::{Request, StatusCode}, @@ -49,7 +53,7 @@ async fn test_register_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseTy let db_item = repository.get_user("user@test.com").await; assert!(db_item.email == "user@test.com"); - assert!(db_item.password == "test"); + assert!(db_item.password != "test"); } #[rstest] @@ -132,7 +136,12 @@ async fn test_register_user_already_registered( async fn test_login_user(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { let repository = repository::new(&db_type); - repository.add_user("user@test.com", "test").await; + let password_hash = Argon2::default() + .hash_password(b"test", &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + repository.add_user("user@test.com", &password_hash).await; + let request = r#"{ "email": "user@test.com", "password": "test" @@ -196,7 +205,12 @@ async fn test_login_user_invalid_email(#[values(DatabaseType::MongoDb)] db_type: async fn test_login_user_not_found(#[values(DatabaseType::MongoDb)] db_type: DatabaseType) { let repository = repository::new(&db_type); - repository.add_user("user@test.com", "test").await; + let password_hash = Argon2::default() + .hash_password(b"test", &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + repository.add_user("user@test.com", &password_hash).await; + let request = r#"{ "email": "user2@test.com", "password": "test" @@ -230,7 +244,12 @@ async fn test_login_user_incorrect_password( ) { let repository = repository::new(&db_type); - repository.add_user("user@test.com", "test").await; + let password_hash = Argon2::default() + .hash_password(b"test", &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string(); + repository.add_user("user@test.com", &password_hash).await; + let request = r#"{ "email": "user@test.com", "password": "incorrect"