From 927337331ec1c2d2ceb44613c8ab59977f571408 Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 12:40:22 -0400 Subject: [PATCH 01/11] Rename `crockford` crate to `crockford_deprecated` - Gotta update references --- crates/lib/{crockford => crockford_deprecated}/Cargo.toml | 4 ++-- .../src/crockford_entropy.rs | 0 crates/lib/{crockford => crockford_deprecated}/src/lib.rs | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename crates/lib/{crockford => crockford_deprecated}/Cargo.toml (86%) rename crates/lib/{crockford => crockford_deprecated}/src/crockford_entropy.rs (100%) rename crates/lib/{crockford => crockford_deprecated}/src/lib.rs (100%) diff --git a/crates/lib/crockford/Cargo.toml b/crates/lib/crockford_deprecated/Cargo.toml similarity index 86% rename from crates/lib/crockford/Cargo.toml rename to crates/lib/crockford_deprecated/Cargo.toml index 89cea5e711..4c0706bbb8 100644 --- a/crates/lib/crockford/Cargo.toml +++ b/crates/lib/crockford_deprecated/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "crockford" +name = "crockford_deprecated" edition = "2018" version = "0.0.1" authors = [ @@ -11,7 +11,7 @@ publish = false [lib] # NB: Public "crockford" crate is non-sensically only for encoding and decoding u64s (wtf?) # Our crate is for generating entropy and crockford encoding it. -name = "crockford" +name = "crockford_deprecated" path = "src/lib.rs" [dependencies] diff --git a/crates/lib/crockford/src/crockford_entropy.rs b/crates/lib/crockford_deprecated/src/crockford_entropy.rs similarity index 100% rename from crates/lib/crockford/src/crockford_entropy.rs rename to crates/lib/crockford_deprecated/src/crockford_entropy.rs diff --git a/crates/lib/crockford/src/lib.rs b/crates/lib/crockford_deprecated/src/lib.rs similarity index 100% rename from crates/lib/crockford/src/lib.rs rename to crates/lib/crockford_deprecated/src/lib.rs From 054d7db0a9e23960b724dec7f3b6b44fbfc5bcce Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 12:42:00 -0400 Subject: [PATCH 02/11] Update `crockford_deprecated` references --- Cargo.toml | 2 +- crates/_types/buckets/Cargo.toml | 2 +- crates/_types/buckets/src/public/media_files/directory.rs | 2 +- crates/_types/buckets/src/public/media_files/original_file.rs | 2 +- crates/_types/buckets/src/public/media_uploads/directory.rs | 2 +- .../_types/buckets/src/public/media_uploads/original_file.rs | 2 +- .../src/public/voice_conversion_results/original_file.rs | 2 +- crates/service/job/download_job/Cargo.toml | 2 +- .../job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6470185b75..fc4c582678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ members = [ "crates/lib/caching/redis_caching", "crates/lib/concurrency", "crates/lib/cookies", - "crates/lib/crockford", + "crates/lib/crockford_deprecated", "crates/lib/datetimes", "crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose "crates/lib/deprecated/container_common", # TODO(bt, 2023-02-13): Pull apart and rehome pieces diff --git a/crates/_types/buckets/Cargo.toml b/crates/_types/buckets/Cargo.toml index 7e78465536..cadb4c77be 100644 --- a/crates/_types/buckets/Cargo.toml +++ b/crates/_types/buckets/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] # Internal -crockford = { path = "../../lib/crockford" } +crockford_deprecated = { path = "../../lib/crockford" } errors = { path = "../../lib/errors" } # External diff --git a/crates/_types/buckets/src/public/media_files/directory.rs b/crates/_types/buckets/src/public/media_files/directory.rs index e0083da128..50d7c6bcac 100644 --- a/crates/_types/buckets/src/public/media_files/directory.rs +++ b/crates/_types/buckets/src/public/media_files/directory.rs @@ -1,4 +1,4 @@ -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; diff --git a/crates/_types/buckets/src/public/media_files/original_file.rs b/crates/_types/buckets/src/public/media_files/original_file.rs index efd0fd9458..8a67104bf1 100644 --- a/crates/_types/buckets/src/public/media_files/original_file.rs +++ b/crates/_types/buckets/src/public/media_files/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use crate::public::media_files::directory::MediaFileBucketDirectory; use crate::public::public_path::PublicPath; diff --git a/crates/_types/buckets/src/public/media_uploads/directory.rs b/crates/_types/buckets/src/public/media_uploads/directory.rs index aa4ffc0470..c5938e8c41 100644 --- a/crates/_types/buckets/src/public/media_uploads/directory.rs +++ b/crates/_types/buckets/src/public/media_uploads/directory.rs @@ -1,4 +1,4 @@ -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; diff --git a/crates/_types/buckets/src/public/media_uploads/original_file.rs b/crates/_types/buckets/src/public/media_uploads/original_file.rs index 7f0a40a6b7..b7a3595361 100644 --- a/crates/_types/buckets/src/public/media_uploads/original_file.rs +++ b/crates/_types/buckets/src/public/media_uploads/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use crate::public::media_uploads::directory::MediaUploadDirectory; use crate::public::public_path::PublicPath; diff --git a/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs b/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs index 9e256e89d2..43493f1a01 100644 --- a/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs +++ b/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::public::voice_conversion_results::directory::VoiceConversionResultDirectory; diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index 6cf50484c0..890733d568 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -20,7 +20,7 @@ cloud_storage = { path = "../../../lib/api_clients/cloud_storage" } concurrency = { path = "../../../lib/concurrency" } config = { path = "../../../lib/deprecated/config" } container_common = { path = "../../../lib/deprecated/container_common" } -crockford = { path = "../../../lib/crockford" } +crockford_deprecated = { path = "../../../lib/crockford" } easyenv = { path = "../../../lib/easyenv" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } diff --git a/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs b/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs index d09be8287b..d33d0468c5 100644 --- a/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs +++ b/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs @@ -8,7 +8,7 @@ use container_common::filesystem::check_file_exists::check_file_exists; use container_common::filesystem::safe_delete_possible_temp_file::safe_delete_possible_temp_file; use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file; -use crockford::crockford_entropy_lower; +use crockford_deprecated::crockford_entropy_lower; use enums::by_table::voice_conversion_models::voice_conversion_model_type::VoiceConversionModelType; use enums::common::visibility::Visibility; use errors::AnyhowResult; @@ -183,7 +183,7 @@ pub async fn process_rvc_v2_model<'a, 'b>( //#[cfg(test)] //mod tests { -// use crockford::crockford_entropy_lower; +// use crockford_deprecated::crockford_entropy_lower; // // #[test] // fn temp() { From 1cc3c5eee4300257a627c54c08ab62a4b41aafa4 Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 12:44:44 -0400 Subject: [PATCH 03/11] Update more `crockford_deprecated` stuff for real this time --- Cargo.lock | 6 +++--- crates/_types/buckets/Cargo.toml | 2 +- crates/service/job/download_job/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcde783143..4bc447730c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,7 +1178,7 @@ dependencies = [ name = "buckets" version = "0.0.1" dependencies = [ - "crockford", + "crockford_deprecated", "errors", ] @@ -1654,7 +1654,7 @@ dependencies = [ ] [[package]] -name = "crockford" +name = "crockford_deprecated" version = "0.0.1" dependencies = [ "rand 0.8.5", @@ -2187,7 +2187,7 @@ dependencies = [ "concurrency", "config", "container_common", - "crockford", + "crockford_deprecated", "dotenv", "easyenv", "enums", diff --git a/crates/_types/buckets/Cargo.toml b/crates/_types/buckets/Cargo.toml index cadb4c77be..f7be10c9b7 100644 --- a/crates/_types/buckets/Cargo.toml +++ b/crates/_types/buckets/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] # Internal -crockford_deprecated = { path = "../../lib/crockford" } +crockford_deprecated = { path = "../../lib/crockford_deprecated" } errors = { path = "../../lib/errors" } # External diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index 890733d568..a209dc7162 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -20,7 +20,7 @@ cloud_storage = { path = "../../../lib/api_clients/cloud_storage" } concurrency = { path = "../../../lib/concurrency" } config = { path = "../../../lib/deprecated/config" } container_common = { path = "../../../lib/deprecated/container_common" } -crockford_deprecated = { path = "../../../lib/crockford" } +crockford_deprecated = { path = "../../../lib/crockford_deprecated" } easyenv = { path = "../../../lib/easyenv" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } From 16d6542911f488a1b152fd4ef18b0e009fbed60a Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 13:06:24 -0400 Subject: [PATCH 04/11] Beginning password reset implementation - Email send is not implemented yet - All of this is still untested, clearly --- Cargo.lock | 6 +- Cargo.toml | 2 +- crates/_types/tokens/src/prefixes.rs | 4 + crates/_types/tokens/src/tokens/mod.rs | 1 + .../tokens/src/tokens/password_reset.rs | 14 +++ crates/lib/email/Cargo.toml | 14 +++ crates/lib/email/src/lib.rs | 27 +++++ .../user/create_password_reset_request.rs | 31 ++++++ .../user/lookup_user_for_login_by_email.rs | 3 +- .../user/lookup_user_for_login_by_username.rs | 3 +- .../user/lookup_user_for_login_result.rs | 1 + .../src/queries/users/user/mod.rs | 1 + crates/service/plugins/users/Cargo.toml | 3 + .../plugins/users/src/endpoints/mod.rs | 1 + .../src/endpoints/password_reset_handler.rs | 101 ++++++++++++++++++ 15 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 crates/_types/tokens/src/tokens/password_reset.rs create mode 100644 crates/lib/email/Cargo.toml create mode 100644 crates/lib/email/src/lib.rs create mode 100644 crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs create mode 100644 crates/service/plugins/users/src/endpoints/password_reset_handler.rs diff --git a/Cargo.lock b/Cargo.lock index 4bc447730c..fcde783143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,7 +1178,7 @@ dependencies = [ name = "buckets" version = "0.0.1" dependencies = [ - "crockford_deprecated", + "crockford", "errors", ] @@ -1654,7 +1654,7 @@ dependencies = [ ] [[package]] -name = "crockford_deprecated" +name = "crockford" version = "0.0.1" dependencies = [ "rand 0.8.5", @@ -2187,7 +2187,7 @@ dependencies = [ "concurrency", "config", "container_common", - "crockford_deprecated", + "crockford", "dotenv", "easyenv", "enums", diff --git a/Cargo.toml b/Cargo.toml index fc4c582678..6470185b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ members = [ "crates/lib/caching/redis_caching", "crates/lib/concurrency", "crates/lib/cookies", - "crates/lib/crockford_deprecated", + "crates/lib/crockford", "crates/lib/datetimes", "crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose "crates/lib/deprecated/container_common", # TODO(bt, 2023-02-13): Pull apart and rehome pieces diff --git a/crates/_types/tokens/src/prefixes.rs b/crates/_types/tokens/src/prefixes.rs index 80c4d17ae0..ff58072deb 100644 --- a/crates/_types/tokens/src/prefixes.rs +++ b/crates/_types/tokens/src/prefixes.rs @@ -30,6 +30,8 @@ pub(crate) enum TokenPrefix { MediaFile, MediaUpload, NewsStory, // NB: aichatbot / sqlite + PasswordReset, + TtsModel, TtsRenderTask, // NB: aichatbot / sqlite User, UserSession, @@ -92,6 +94,8 @@ impl PrefixGenerator for TokenPrefix { Self::MediaFile => "m_", Self::MediaUpload => "mu_", Self::NewsStory => "news_story_", + Self::PasswordReset => "pw_reset_", + Self::TtsModel => "TM:", // NB: Old-style prefix, do not use for future tokens. Self::TtsRenderTask => "tts_task_", Self::User => "user_", // NB: Previously "U:" Self::UserSession => "session_", diff --git a/crates/_types/tokens/src/tokens/mod.rs b/crates/_types/tokens/src/tokens/mod.rs index 0bc313489f..f9ecc1b3df 100644 --- a/crates/_types/tokens/src/tokens/mod.rs +++ b/crates/_types/tokens/src/tokens/mod.rs @@ -9,6 +9,7 @@ pub mod generic_inference_jobs; pub mod media_files; pub mod media_uploads; pub mod model_categories; +pub mod password_reset; pub mod sqlite; pub mod tts_inference_jobs; pub mod tts_model_upload_jobs; diff --git a/crates/_types/tokens/src/tokens/password_reset.rs b/crates/_types/tokens/src/tokens/password_reset.rs new file mode 100644 index 0000000000..e9904c7d8e --- /dev/null +++ b/crates/_types/tokens/src/tokens/password_reset.rs @@ -0,0 +1,14 @@ +use std::fmt::Debug; + +use serde::Deserialize; +use serde::Serialize; + +use crate::prefixes::EntityType; + +/// The primary key for `password_reset`s +#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct PasswordResetToken(pub String); + +impl_string_token!(PasswordResetToken); +impl_crockford_generator!(PasswordResetToken, 32usize, EntityType::PasswordReset, CrockfordUpper); diff --git a/crates/lib/email/Cargo.toml b/crates/lib/email/Cargo.toml new file mode 100644 index 0000000000..e2268defb5 --- /dev/null +++ b/crates/lib/email/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "email" +version = "0.1.0" +edition = "2021" + + +[lib] +path = "src/lib.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +errors = { path = "../errors" } +mail-smtp = "0.3.0" \ No newline at end of file diff --git a/crates/lib/email/src/lib.rs b/crates/lib/email/src/lib.rs new file mode 100644 index 0000000000..ee03d6e488 --- /dev/null +++ b/crates/lib/email/src/lib.rs @@ -0,0 +1,27 @@ +use errors::AnyhowResult; + +pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> { + todo!(); + // use futures::Future; + // use mail_headers::{ + // headers::*, + // header_components::Domain + // }; + // use mail_core::{Mail, default_impl::simple_context}; + // use mail_smtp::{self as smtp, ConnectionConfig}; + + // let ctx = simple_context::new(Domain::from_unchecked("example.com".to_owned()), "asdkds".parse().unwrap()) + // .unwrap(); + + // let mut mail = Mail::plain_text("Some body", &ctx); + // mail.insert_headers(headers! { + // _From: ["bla@example.com"], + // _To: ["blub@example.com"], + // Subject: "Some Mail" + // }.unwrap()); + + // let con_config = ConnectionConfig::builder_local_unencrypted().build(); + + // let fut = smtp::send(mail.into(), con_config, ctx); + // let results = fut.wait(); +} \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs new file mode 100644 index 0000000000..3a8ed17a59 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs @@ -0,0 +1,31 @@ +use anyhow::Context; +use sqlx::MySqlPool; + +use errors::AnyhowResult; +use tokens::tokens::password_reset::PasswordResetToken; + +use super::lookup_user_for_login_result::UserRecordForLogin; + +// todo: ip_address_redemption nullable + +pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, ip_address: String, secret_key: String) -> AnyhowResult<()> { + let token = PasswordResetToken::generate(); + + sqlx::query!( + r#" +INSERT INTO password_resets +(token, user_token, secret_key, current_password_version, ip_address_creation, expires_at) +VALUES (?, ?, ?, ?, ?, NOW() + INTERVAL 3 hour); + "#, + + token, + user.token, + secret_key, + user.password_version, + ip_address + ) + .execute(pool) + .await + .context("inserting password reset") + .map(|_| ()) +} \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs index eb7d54402f..fcd3331b82 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs @@ -14,7 +14,8 @@ SELECT username, email_address, password_hash, - is_banned + is_banned, + password_version FROM users WHERE email_address = ? LIMIT 1 diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs index 24b474ed15..2375dc35c9 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs @@ -15,7 +15,8 @@ SELECT username, email_address, password_hash, - is_banned + is_banned, + password_version FROM users WHERE username = ? LIMIT 1 diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs index 4a998b84bd..b0cf6f8379 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs @@ -8,4 +8,5 @@ pub struct UserRecordForLogin { pub email_address: String, pub password_hash: Vec, pub is_banned: i8, + pub password_version: i32, } diff --git a/crates/lib/mysql_queries/src/queries/users/user/mod.rs b/crates/lib/mysql_queries/src/queries/users/user/mod.rs index ca6d450fbf..cad31ea615 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/mod.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/mod.rs @@ -1,4 +1,5 @@ pub mod create_account; +pub mod create_password_reset_request; pub mod get_user_token_by_username; pub mod list_users_query_builder; pub mod lookup_user_for_login_by_email; diff --git a/crates/service/plugins/users/Cargo.toml b/crates/service/plugins/users/Cargo.toml index 8e5d603ea3..fc84ffbf37 100644 --- a/crates/service/plugins/users/Cargo.toml +++ b/crates/service/plugins/users/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../../../lib/deprecated/container_common" } cookies = { path = "../../../lib/cookies" } +email = { path = "../../../lib/email" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } http_server_common = { path = "../../../lib/deprecated/http_server_common" } @@ -65,3 +66,5 @@ sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "o # TODO: Shouldn't really be a dependency, but need to add caching for the DDoS attack. # Redis Database r2d2_redis = "0.14.0" +crockford = "1.1.2" +byteorder = "1.5.0" diff --git a/crates/service/plugins/users/src/endpoints/mod.rs b/crates/service/plugins/users/src/endpoints/mod.rs index ddbd3ba54a..87c6d8c6ea 100644 --- a/crates/service/plugins/users/src/endpoints/mod.rs +++ b/crates/service/plugins/users/src/endpoints/mod.rs @@ -3,4 +3,5 @@ pub mod edit_profile_handler; pub mod get_profile_handler; pub mod login_handler; pub mod logout_handler; +pub mod password_reset_handler; pub mod session_info_handler; \ No newline at end of file diff --git a/crates/service/plugins/users/src/endpoints/password_reset_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_handler.rs new file mode 100644 index 0000000000..6ef1bf882d --- /dev/null +++ b/crates/service/plugins/users/src/endpoints/password_reset_handler.rs @@ -0,0 +1,101 @@ +/* + ~ B R A I N S T O R M ~ + + - redeem reset request + - needs a way to identify the user because only the tuple is unique (user + key) + username or email address + - provide new password + */ + +use actix_web::{HttpRequest, HttpResponse, web}; +use byteorder::ByteOrder; +use http_server_common::request::get_request_ip::get_request_ip; +use log::warn; +use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; +use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; +use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; +use rand::RngCore; +use serde::Deserialize; +use sqlx::MySqlPool; + +/// This can be reused in login requests in the future! +#[derive(Deserialize)] +pub enum UserLogin { + Email(String), + Username(String), +} + +#[derive(Deserialize)] +pub struct PasswordResetRequest { + login: UserLogin, +} + +#[derive(Serialize)] +pub struct PasswordResetResponse {} + +#[derive(Serialize)] +pub enum PasswordResetRequestError { + NoSuchUser, + Internal, +} + +#[derive(Serialize)] +pub struct PasswordResetErrorResponse { + kind: PasswordResetRequestError, +} +impl From for PasswordResetErrorResponse { + fn from(value: PasswordResetRequestError) -> Self { + Self { kind: value } + } +} +impl From for PasswordResetErrorResponse { + fn from(value: errors::AnyhowError) -> Self { + log::error!("Internal error: {value}"); + Self { kind: PasswordResetRequestError::Internal } + } +} + +/// +/// Non-authenticated! +/// +/// - create password reset request +/// - sends email? +/// - inserts record and stuff +pub async fn password_reset_request_handler( + http_request: HttpRequest, + request: web::Json, + // session_cookie_manager: web::Data, + mysql_pool: web::Data, +) -> Result { + + let ip_address = get_request_ip(&http_request); + + let user = match &request.login { + UserLogin::Email(email) => lookup_user_for_login_by_email(&email, &mysql_pool).await, + UserLogin::Username(username) => lookup_user_for_login_by_username(&username, &mysql_pool).await, + }.map_err(|e| { + warn!("Password reset user lookup: {:?}", e); + PasswordResetRequestError::NoSuchUser + //TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻 + })?; + + let mut rng = rand::thread_rng(); + let mut secret_key_bytes = [0u8;40]; + // Has to fit within (4 / 3) * string length (32 chars), + // but also be a multiple of 8 so we can read them as `u64`s + // (for Crockford) + rng.fill_bytes(&mut secret_key_bytes); + + let mut secret_key = String::new(); + secret_key_bytes.chunks_exact(8) + .map(|chunk| byteorder::LittleEndian::read_u64(chunk)) + .for_each(|chunk| crockford::encode_into::(chunk, &mut secret_key)); + + + //TODO: Handle banned users, they shouldn't be able to do this + + create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await?; + email::send_password_reset(&user.email_address, secret_key).await?; + + Ok(HttpResponse::Ok().finish()) +} \ No newline at end of file From 6bbf8a9b01d1ce43bce5bb51b1f0762d4275f937 Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 13:36:17 -0400 Subject: [PATCH 05/11] Changed e-mail library; still only partially implemented --- crates/lib/email/Cargo.toml | 2 +- crates/lib/email/src/lib.rs | 45 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/crates/lib/email/Cargo.toml b/crates/lib/email/Cargo.toml index e2268defb5..b211388c94 100644 --- a/crates/lib/email/Cargo.toml +++ b/crates/lib/email/Cargo.toml @@ -11,4 +11,4 @@ path = "src/lib.rs" [dependencies] errors = { path = "../errors" } -mail-smtp = "0.3.0" \ No newline at end of file +lettre = "0.11.0" diff --git a/crates/lib/email/src/lib.rs b/crates/lib/email/src/lib.rs index ee03d6e488..df6cd1a704 100644 --- a/crates/lib/email/src/lib.rs +++ b/crates/lib/email/src/lib.rs @@ -1,27 +1,32 @@ -use errors::AnyhowResult; +use errors::{AnyhowResult, AnyhowError, anyhow}; + +use lettre::message::header::ContentType; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; + pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> { - todo!(); - // use futures::Future; - // use mail_headers::{ - // headers::*, - // header_components::Domain - // }; - // use mail_core::{Mail, default_impl::simple_context}; - // use mail_smtp::{self as smtp, ConnectionConfig}; + let email = Message::builder() + .from("Support ".parse().unwrap()) + .to( email_address.parse().unwrap()) + .subject("Your password reset") + .header(ContentType::TEXT_PLAIN) + .body(format!("Here you go! {secret_key}")) + .unwrap(); - // let ctx = simple_context::new(Domain::from_unchecked("example.com".to_owned()), "asdkds".parse().unwrap()) - // .unwrap(); + let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned()); - // let mut mail = Mail::plain_text("Some body", &ctx); - // mail.insert_headers(headers! { - // _From: ["bla@example.com"], - // _To: ["blub@example.com"], - // Subject: "Some Mail" - // }.unwrap()); + // Open a remote connection to gmail + let mailer = SmtpTransport::relay("smtp.gmail.com") + .unwrap() + .credentials(creds) + .build(); - // let con_config = ConnectionConfig::builder_local_unencrypted().build(); + // Send the email + match mailer.send(&email) { + Ok(_) => println!("Email sent successfully!"), + Err(e) => panic!("Could not send email: {e:?}"), + } - // let fut = smtp::send(mail.into(), con_config, ctx); - // let results = fut.wait(); + Ok(()) } \ No newline at end of file From 5892fb1f1401233d3e83c4b74bc6ca3482e66a6d Mon Sep 17 00:00:00 2001 From: Jacob Conley Date: Sun, 22 Oct 2023 13:57:20 -0400 Subject: [PATCH 06/11] Fixed implementation up to the point of sending the email --- Cargo.lock | 37 +++++++++++++++---- .../user/create_password_reset_request.rs | 8 ++-- crates/service/plugins/users/Cargo.toml | 2 + .../plugins/users/src/default_routes.rs | 6 +++ .../src/endpoints/password_reset_handler.rs | 36 ++++++++++++++++-- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcde783143..088c549d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2388,7 +2388,7 @@ dependencies = [ "serde_json", "sqlx", "sqlx-core", - "strum", + "strum 0.24.1", "toml", ] @@ -2888,9 +2888,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" dependencies = [ "unicode-segmentation", ] @@ -5724,7 +5724,7 @@ checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" dependencies = [ "dotenvy", "either", - "heck 0.4.0", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -5995,22 +5995,41 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", ] +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + [[package]] name = "strum_macros" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", "syn 1.0.107", ] +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.26", +] + [[package]] name = "subprocess" version = "0.2.9" @@ -6483,7 +6502,7 @@ dependencies = [ "serde_json", "sqlx", "sqlx-core", - "strum", + "strum 0.24.1", "toml", ] @@ -7207,6 +7226,8 @@ dependencies = [ "sha2 0.9.5", "sqlx", "sqlx_mysql_helpers", + "strum 0.25.0", + "strum_macros 0.25.3", "time 0.2.26", "tokens", "toml", @@ -7477,7 +7498,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "strum", + "strum 0.24.1", "toml", ] diff --git a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs index 3a8ed17a59..fcddce58f2 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs @@ -6,16 +6,18 @@ use tokens::tokens::password_reset::PasswordResetToken; use super::lookup_user_for_login_result::UserRecordForLogin; -// todo: ip_address_redemption nullable pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, ip_address: String, secret_key: String) -> AnyhowResult<()> { let token = PasswordResetToken::generate(); + //TODO: ip_address_redemption nullable + // Definitely remove the empty string default as well! + sqlx::query!( r#" INSERT INTO password_resets -(token, user_token, secret_key, current_password_version, ip_address_creation, expires_at) -VALUES (?, ?, ?, ?, ?, NOW() + INTERVAL 3 hour); +(token, user_token, secret_key, current_password_version, ip_address_creation, ip_address_redemption, expires_at) +VALUES (?, ?, ?, ?, ?, '', NOW() + INTERVAL 3 hour); "#, token, diff --git a/crates/service/plugins/users/Cargo.toml b/crates/service/plugins/users/Cargo.toml index fc84ffbf37..8adecac992 100644 --- a/crates/service/plugins/users/Cargo.toml +++ b/crates/service/plugins/users/Cargo.toml @@ -68,3 +68,5 @@ sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "o r2d2_redis = "0.14.0" crockford = "1.1.2" byteorder = "1.5.0" +strum = "0.25.0" +strum_macros = "0.25.3" diff --git a/crates/service/plugins/users/src/default_routes.rs b/crates/service/plugins/users/src/default_routes.rs index 10adedc13d..981f3592df 100644 --- a/crates/service/plugins/users/src/default_routes.rs +++ b/crates/service/plugins/users/src/default_routes.rs @@ -7,6 +7,7 @@ use actix_web::error::Error; use crate::endpoints::create_account_handler::create_account_handler; use crate::endpoints::login_handler::login_handler; use crate::endpoints::logout_handler::logout_handler; +use crate::endpoints::password_reset_handler::password_reset_request_handler; use crate::endpoints::session_info_handler::session_info_handler; // NB: This does not include the user edit endpoints since FakeYou's API mounts them alongside other things. @@ -64,4 +65,9 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< .route(web::get().to(session_info_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::resource("/v1/request-reset") + .route(web::post().to(password_reset_request_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) } diff --git a/crates/service/plugins/users/src/endpoints/password_reset_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_handler.rs index 6ef1bf882d..323734c364 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_handler.rs @@ -7,9 +7,13 @@ - provide new password */ -use actix_web::{HttpRequest, HttpResponse, web}; +use std::fmt::Display; + +use actix_web::http::StatusCode; +use actix_web::{HttpRequest, HttpResponse, web, ResponseError}; use byteorder::ByteOrder; use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::warn; use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; @@ -17,6 +21,7 @@ use mysql_queries::queries::users::user::lookup_user_for_login_by_username::look use rand::RngCore; use serde::Deserialize; use sqlx::MySqlPool; +use strum_macros::Display; /// This can be reused in login requests in the future! #[derive(Deserialize)] @@ -33,16 +38,22 @@ pub struct PasswordResetRequest { #[derive(Serialize)] pub struct PasswordResetResponse {} -#[derive(Serialize)] +#[derive(Serialize, Debug, Display)] pub enum PasswordResetRequestError { NoSuchUser, Internal, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct PasswordResetErrorResponse { kind: PasswordResetRequestError, } +impl Display for PasswordResetErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + impl From for PasswordResetErrorResponse { fn from(value: PasswordResetRequestError) -> Self { Self { kind: value } @@ -55,6 +66,17 @@ impl From for PasswordResetErrorResponse { } } +impl ResponseError for PasswordResetErrorResponse { + //TODO: Yknow, clean this up and stuff + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } + } + /// /// Non-authenticated! /// @@ -91,10 +113,16 @@ pub async fn password_reset_request_handler( .map(|chunk| byteorder::LittleEndian::read_u64(chunk)) .for_each(|chunk| crockford::encode_into::(chunk, &mut secret_key)); + secret_key.truncate(32); + //TODO: Handle banned users, they shouldn't be able to do this - create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await?; + create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await + .map_err(|err| { + log::error!("AAAAAHHH! {:?}", err); + err + })?; email::send_password_reset(&user.email_address, secret_key).await?; Ok(HttpResponse::Ok().finish()) From 9ce771f57101b248044bf3ae2d3212769b30d607 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 24 Oct 2023 03:37:43 -0400 Subject: [PATCH 07/11] fix crockford-encoded entropy --- Cargo.lock | 130 ++++++++++++++++-- Cargo.toml | 1 + crates/_types/buckets/Cargo.toml | 2 +- .../src/public/media_files/directory.rs | 2 +- .../src/public/media_files/original_file.rs | 2 +- .../src/public/media_uploads/directory.rs | 2 +- .../src/public/media_uploads/original_file.rs | 2 +- .../voice_conversion_results/original_file.rs | 2 +- .../tokens/src/tokens/password_reset.rs | 4 +- .../Cargo.toml | 4 +- .../src/crockford_entropy.rs | 0 .../src/lib.rs | 0 crates/lib/email/src/lib.rs | 29 ++-- .../user/create_password_reset_request.rs | 10 +- .../user/lookup_user_for_login_by_email.rs | 4 +- .../user/lookup_user_for_login_by_username.rs | 4 +- .../user/lookup_user_for_login_result.rs | 2 +- crates/service/job/download_job/Cargo.toml | 2 +- .../rvc_v2/process_rvc_v2_model.rs | 2 +- crates/service/plugins/users/Cargo.toml | 2 +- .../plugins/users/src/default_routes.rs | 4 +- .../plugins/users/src/endpoints/mod.rs | 2 +- ... => password_reset_requisition_handler.rs} | 89 ++++++------ 23 files changed, 205 insertions(+), 96 deletions(-) rename crates/lib/{crockford_deprecated => crockford}/Cargo.toml (86%) rename crates/lib/{crockford_deprecated => crockford}/src/crockford_entropy.rs (100%) rename crates/lib/{crockford_deprecated => crockford}/src/lib.rs (100%) rename crates/service/plugins/users/src/endpoints/{password_reset_handler.rs => password_reset_requisition_handler.rs} (60%) diff --git a/Cargo.lock b/Cargo.lock index 088c549d0a..e2b1448bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "analytics-job" version = "0.0.1" @@ -1196,9 +1202,9 @@ checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -1314,6 +1320,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.2", + "stacker", +] + [[package]] name = "cidr-utils" version = "0.5.11" @@ -1389,7 +1405,7 @@ version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.26", @@ -2339,7 +2355,7 @@ dependencies = [ "serde_json", "sqlx", "storyteller_root", - "strum", + "strum 0.24.1", "tokens", "tokio 1.34.0", ] @@ -2358,6 +2374,30 @@ dependencies = [ "tokens", ] +[[package]] +name = "email" +version = "0.1.0" +dependencies = [ + "errors", + "lettre", +] + +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.0", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.28" @@ -2477,6 +2517,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fdeflate" version = "0.3.0" @@ -2665,7 +2711,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" dependencies = [ - "fastrand", + "fastrand 1.7.0", "futures-core", "futures-io", "memchr", @@ -2857,6 +2903,16 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + [[package]] name = "hashing" version = "0.0.1" @@ -2874,7 +2930,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -3169,7 +3225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -3450,6 +3506,31 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "lettre" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a466bc111374ccf4d90877dba636924a2185e67e5be4b35d32043199365097b2" +dependencies = [ + "base64 0.21.0", + "chumsky", + "email-encoding", + "email_address", + "fastrand 2.0.1", + "futures-util", + "hostname", + "httpdate", + "idna 0.4.0", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2 0.5.5", + "tokio 1.34.0", + "url", +] + [[package]] name = "lexical-sort" version = "0.3.1" @@ -4194,7 +4275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -4531,6 +4612,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -4587,6 +4677,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "r2d2" version = "0.8.9" @@ -5764,6 +5860,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "libc", + "psm", + "winapi 0.3.9", +] + [[package]] name = "standback" version = "0.2.17" @@ -6309,7 +6418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", - "fastrand", + "fastrand 1.7.0", "libc", "redox_syscall 0.2.13", "remove_dir_all", @@ -7200,10 +7309,13 @@ dependencies = [ "anyhow", "base64 0.21.0", "bcrypt", + "byteorder", "chrono", "container_common", "cookies", + "crockford", "dotenv", + "email", "enums", "errors", "hmac 0.10.1", diff --git a/Cargo.toml b/Cargo.toml index 6470185b75..b4208e50b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers "crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve. "crates/lib/elasticsearch_schema", + "crates/lib/email", "crates/lib/errors", "crates/lib/files/archives", "crates/lib/files/filesys", diff --git a/crates/_types/buckets/Cargo.toml b/crates/_types/buckets/Cargo.toml index f7be10c9b7..7e78465536 100644 --- a/crates/_types/buckets/Cargo.toml +++ b/crates/_types/buckets/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] # Internal -crockford_deprecated = { path = "../../lib/crockford_deprecated" } +crockford = { path = "../../lib/crockford" } errors = { path = "../../lib/errors" } # External diff --git a/crates/_types/buckets/src/public/media_files/directory.rs b/crates/_types/buckets/src/public/media_files/directory.rs index 50d7c6bcac..e0083da128 100644 --- a/crates/_types/buckets/src/public/media_files/directory.rs +++ b/crates/_types/buckets/src/public/media_files/directory.rs @@ -1,4 +1,4 @@ -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; diff --git a/crates/_types/buckets/src/public/media_files/original_file.rs b/crates/_types/buckets/src/public/media_files/original_file.rs index 8a67104bf1..efd0fd9458 100644 --- a/crates/_types/buckets/src/public/media_files/original_file.rs +++ b/crates/_types/buckets/src/public/media_files/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use crate::public::media_files::directory::MediaFileBucketDirectory; use crate::public::public_path::PublicPath; diff --git a/crates/_types/buckets/src/public/media_uploads/directory.rs b/crates/_types/buckets/src/public/media_uploads/directory.rs index c5938e8c41..aa4ffc0470 100644 --- a/crates/_types/buckets/src/public/media_uploads/directory.rs +++ b/crates/_types/buckets/src/public/media_uploads/directory.rs @@ -1,4 +1,4 @@ -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; diff --git a/crates/_types/buckets/src/public/media_uploads/original_file.rs b/crates/_types/buckets/src/public/media_uploads/original_file.rs index b7a3595361..7f0a40a6b7 100644 --- a/crates/_types/buckets/src/public/media_uploads/original_file.rs +++ b/crates/_types/buckets/src/public/media_uploads/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use crate::public::media_uploads::directory::MediaUploadDirectory; use crate::public::public_path::PublicPath; diff --git a/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs b/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs index 43493f1a01..9e256e89d2 100644 --- a/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs +++ b/crates/_types/buckets/src/public/voice_conversion_results/original_file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use crate::public::public_path::PublicPath; use crate::public::voice_conversion_results::directory::VoiceConversionResultDirectory; diff --git a/crates/_types/tokens/src/tokens/password_reset.rs b/crates/_types/tokens/src/tokens/password_reset.rs index e9904c7d8e..06a59846be 100644 --- a/crates/_types/tokens/src/tokens/password_reset.rs +++ b/crates/_types/tokens/src/tokens/password_reset.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use serde::Deserialize; use serde::Serialize; -use crate::prefixes::EntityType; +use crate::prefixes::TokenPrefix; /// The primary key for `password_reset`s #[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] @@ -11,4 +11,4 @@ use crate::prefixes::EntityType; pub struct PasswordResetToken(pub String); impl_string_token!(PasswordResetToken); -impl_crockford_generator!(PasswordResetToken, 32usize, EntityType::PasswordReset, CrockfordUpper); +impl_crockford_generator!(PasswordResetToken, 32usize, TokenPrefix::PasswordReset, CrockfordUpper); diff --git a/crates/lib/crockford_deprecated/Cargo.toml b/crates/lib/crockford/Cargo.toml similarity index 86% rename from crates/lib/crockford_deprecated/Cargo.toml rename to crates/lib/crockford/Cargo.toml index 4c0706bbb8..89cea5e711 100644 --- a/crates/lib/crockford_deprecated/Cargo.toml +++ b/crates/lib/crockford/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "crockford_deprecated" +name = "crockford" edition = "2018" version = "0.0.1" authors = [ @@ -11,7 +11,7 @@ publish = false [lib] # NB: Public "crockford" crate is non-sensically only for encoding and decoding u64s (wtf?) # Our crate is for generating entropy and crockford encoding it. -name = "crockford_deprecated" +name = "crockford" path = "src/lib.rs" [dependencies] diff --git a/crates/lib/crockford_deprecated/src/crockford_entropy.rs b/crates/lib/crockford/src/crockford_entropy.rs similarity index 100% rename from crates/lib/crockford_deprecated/src/crockford_entropy.rs rename to crates/lib/crockford/src/crockford_entropy.rs diff --git a/crates/lib/crockford_deprecated/src/lib.rs b/crates/lib/crockford/src/lib.rs similarity index 100% rename from crates/lib/crockford_deprecated/src/lib.rs rename to crates/lib/crockford/src/lib.rs diff --git a/crates/lib/email/src/lib.rs b/crates/lib/email/src/lib.rs index df6cd1a704..d8cc00a3e8 100644 --- a/crates/lib/email/src/lib.rs +++ b/crates/lib/email/src/lib.rs @@ -1,31 +1,30 @@ -use errors::{AnyhowResult, AnyhowError, anyhow}; - +use lettre::{Message, SmtpTransport, Transport}; use lettre::message::header::ContentType; use lettre::transport::smtp::authentication::Credentials; -use lettre::{Message, SmtpTransport, Transport}; +use errors::AnyhowResult; -pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> { +pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> { let email = Message::builder() - .from("Support ".parse().unwrap()) - .to( email_address.parse().unwrap()) - .subject("Your password reset") - .header(ContentType::TEXT_PLAIN) - .body(format!("Here you go! {secret_key}")) - .unwrap(); + .from("Support ".parse().unwrap()) + .to( email_address.parse().unwrap()) + .subject("Your password reset") + .header(ContentType::TEXT_PLAIN) + .body(format!("Here you go! {secret_key}")) + .unwrap(); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned()); // Open a remote connection to gmail let mailer = SmtpTransport::relay("smtp.gmail.com") - .unwrap() - .credentials(creds) - .build(); + .unwrap() + .credentials(creds) + .build(); // Send the email match mailer.send(&email) { - Ok(_) => println!("Email sent successfully!"), - Err(e) => panic!("Could not send email: {e:?}"), + Ok(_) => println!("Email sent successfully!"), + Err(e) => panic!("Could not send email: {e:?}"), } Ok(()) diff --git a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs index fcddce58f2..6e932dd0b9 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs @@ -6,18 +6,14 @@ use tokens::tokens::password_reset::PasswordResetToken; use super::lookup_user_for_login_result::UserRecordForLogin; - pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, ip_address: String, secret_key: String) -> AnyhowResult<()> { let token = PasswordResetToken::generate(); - //TODO: ip_address_redemption nullable - // Definitely remove the empty string default as well! - sqlx::query!( r#" INSERT INTO password_resets -(token, user_token, secret_key, current_password_version, ip_address_creation, ip_address_redemption, expires_at) -VALUES (?, ?, ?, ?, ?, '', NOW() + INTERVAL 3 hour); +(token, user_token, secret_key, current_password_version, ip_address_creation, expires_at) +VALUES (?, ?, ?, ?, ?, NOW() + INTERVAL 3 hour); "#, token, @@ -30,4 +26,4 @@ VALUES (?, ?, ?, ?, ?, '', NOW() + INTERVAL 3 hour); .await .context("inserting password reset") .map(|_| ()) -} \ No newline at end of file +} diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs index fcd3331b82..ad473e162a 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_email.rs @@ -14,8 +14,8 @@ SELECT username, email_address, password_hash, - is_banned, - password_version + password_version, + is_banned FROM users WHERE email_address = ? LIMIT 1 diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs index 2375dc35c9..13a1ffba20 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_by_username.rs @@ -15,8 +15,8 @@ SELECT username, email_address, password_hash, - is_banned, - password_version + password_version, + is_banned FROM users WHERE username = ? LIMIT 1 diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs index b0cf6f8379..30a19db4ee 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs @@ -7,6 +7,6 @@ pub struct UserRecordForLogin { pub username: String, pub email_address: String, pub password_hash: Vec, - pub is_banned: i8, pub password_version: i32, + pub is_banned: i8, } diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index a209dc7162..6cf50484c0 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -20,7 +20,7 @@ cloud_storage = { path = "../../../lib/api_clients/cloud_storage" } concurrency = { path = "../../../lib/concurrency" } config = { path = "../../../lib/deprecated/config" } container_common = { path = "../../../lib/deprecated/container_common" } -crockford_deprecated = { path = "../../../lib/crockford_deprecated" } +crockford = { path = "../../../lib/crockford" } easyenv = { path = "../../../lib/easyenv" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } diff --git a/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs b/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs index d33d0468c5..fc54d31f58 100644 --- a/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs +++ b/crates/service/job/download_job/src/job_types/voice_conversion/rvc_v2/process_rvc_v2_model.rs @@ -8,7 +8,7 @@ use container_common::filesystem::check_file_exists::check_file_exists; use container_common::filesystem::safe_delete_possible_temp_file::safe_delete_possible_temp_file; use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file; -use crockford_deprecated::crockford_entropy_lower; +use crockford::crockford_entropy_lower; use enums::by_table::voice_conversion_models::voice_conversion_model_type::VoiceConversionModelType; use enums::common::visibility::Visibility; use errors::AnyhowResult; diff --git a/crates/service/plugins/users/Cargo.toml b/crates/service/plugins/users/Cargo.toml index 8adecac992..c1dcd3460d 100644 --- a/crates/service/plugins/users/Cargo.toml +++ b/crates/service/plugins/users/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../../../lib/deprecated/container_common" } cookies = { path = "../../../lib/cookies" } +crockford = { path = "../../../lib/crockford" } email = { path = "../../../lib/email" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } @@ -66,7 +67,6 @@ sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "o # TODO: Shouldn't really be a dependency, but need to add caching for the DDoS attack. # Redis Database r2d2_redis = "0.14.0" -crockford = "1.1.2" byteorder = "1.5.0" strum = "0.25.0" strum_macros = "0.25.3" diff --git a/crates/service/plugins/users/src/default_routes.rs b/crates/service/plugins/users/src/default_routes.rs index 981f3592df..19c312436f 100644 --- a/crates/service/plugins/users/src/default_routes.rs +++ b/crates/service/plugins/users/src/default_routes.rs @@ -7,7 +7,7 @@ use actix_web::error::Error; use crate::endpoints::create_account_handler::create_account_handler; use crate::endpoints::login_handler::login_handler; use crate::endpoints::logout_handler::logout_handler; -use crate::endpoints::password_reset_handler::password_reset_request_handler; +use crate::endpoints::password_reset_requisition_handler::password_reset_requisition_handler; use crate::endpoints::session_info_handler::session_info_handler; // NB: This does not include the user edit endpoints since FakeYou's API mounts them alongside other things. @@ -67,7 +67,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< ) .service( web::resource("/v1/request-reset") - .route(web::post().to(password_reset_request_handler)) + .route(web::post().to(password_reset_requisition_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) } diff --git a/crates/service/plugins/users/src/endpoints/mod.rs b/crates/service/plugins/users/src/endpoints/mod.rs index 87c6d8c6ea..3523568042 100644 --- a/crates/service/plugins/users/src/endpoints/mod.rs +++ b/crates/service/plugins/users/src/endpoints/mod.rs @@ -3,5 +3,5 @@ pub mod edit_profile_handler; pub mod get_profile_handler; pub mod login_handler; pub mod logout_handler; -pub mod password_reset_handler; +pub mod password_reset_requisition_handler; pub mod session_info_handler; \ No newline at end of file diff --git a/crates/service/plugins/users/src/endpoints/password_reset_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs similarity index 60% rename from crates/service/plugins/users/src/endpoints/password_reset_handler.rs rename to crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs index 323734c364..e3ec058cc9 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs @@ -9,20 +9,20 @@ use std::fmt::Display; +use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; use actix_web::http::StatusCode; -use actix_web::{HttpRequest, HttpResponse, web, ResponseError}; -use byteorder::ByteOrder; -use http_server_common::request::get_request_ip::get_request_ip; -use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use log::warn; -use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; -use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; -use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; -use rand::RngCore; +use log::{error, warn}; use serde::Deserialize; use sqlx::MySqlPool; use strum_macros::Display; +use crockford::crockford_entropy_lower; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; +use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; +use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; + /// This can be reused in login requests in the future! #[derive(Deserialize)] pub enum UserLogin { @@ -31,42 +31,44 @@ pub enum UserLogin { } #[derive(Deserialize)] -pub struct PasswordResetRequest { +pub struct PasswordResetRequisitionRequest { login: UserLogin, } #[derive(Serialize)] -pub struct PasswordResetResponse {} +pub struct PasswordResetRequisitionResponse { + success: bool, +} #[derive(Serialize, Debug, Display)] -pub enum PasswordResetRequestError { +pub enum PasswordResetRequisitionRequestError { NoSuchUser, Internal, } #[derive(Serialize, Debug)] -pub struct PasswordResetErrorResponse { - kind: PasswordResetRequestError, +pub struct PasswordResetRequisitionErrorResponse { + kind: PasswordResetRequisitionRequestError, } -impl Display for PasswordResetErrorResponse { +impl Display for PasswordResetRequisitionErrorResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } -impl From for PasswordResetErrorResponse { - fn from(value: PasswordResetRequestError) -> Self { +impl From for PasswordResetRequisitionErrorResponse { + fn from(value: PasswordResetRequisitionRequestError) -> Self { Self { kind: value } } } -impl From for PasswordResetErrorResponse { +impl From for PasswordResetRequisitionErrorResponse { fn from(value: errors::AnyhowError) -> Self { log::error!("Internal error: {value}"); - Self { kind: PasswordResetRequestError::Internal } + Self { kind: PasswordResetRequisitionRequestError::Internal } } } -impl ResponseError for PasswordResetErrorResponse { +impl ResponseError for PasswordResetRequisitionErrorResponse { //TODO: Yknow, clean this up and stuff fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR @@ -83,12 +85,11 @@ impl ResponseError for PasswordResetErrorResponse { /// - create password reset request /// - sends email? /// - inserts record and stuff -pub async fn password_reset_request_handler( +pub async fn password_reset_requisition_handler( http_request: HttpRequest, - request: web::Json, - // session_cookie_manager: web::Data, + request: web::Json, mysql_pool: web::Data, -) -> Result { +) -> Result { let ip_address = get_request_ip(&http_request); @@ -97,33 +98,33 @@ pub async fn password_reset_request_handler( UserLogin::Username(username) => lookup_user_for_login_by_username(&username, &mysql_pool).await, }.map_err(|e| { warn!("Password reset user lookup: {:?}", e); - PasswordResetRequestError::NoSuchUser + PasswordResetRequisitionRequestError::NoSuchUser //TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻 })?; - let mut rng = rand::thread_rng(); - let mut secret_key_bytes = [0u8;40]; - // Has to fit within (4 / 3) * string length (32 chars), - // but also be a multiple of 8 so we can read them as `u64`s - // (for Crockford) - rng.fill_bytes(&mut secret_key_bytes); - - let mut secret_key = String::new(); - secret_key_bytes.chunks_exact(8) - .map(|chunk| byteorder::LittleEndian::read_u64(chunk)) - .for_each(|chunk| crockford::encode_into::(chunk, &mut secret_key)); - - secret_key.truncate(32); - + let secret_key = crockford_entropy_lower(32); //TODO: Handle banned users, they shouldn't be able to do this create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await - .map_err(|err| { - log::error!("AAAAAHHH! {:?}", err); - err - })?; + .map_err(|err| { + log::error!("Password reset error {:?}", err); + err + })?; + email::send_password_reset(&user.email_address, secret_key).await?; - Ok(HttpResponse::Ok().finish()) + let response = PasswordResetRequisitionResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| { + error!("error returning response: {:?}", e); + PasswordResetRequisitionRequestError::Internal + })?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) } \ No newline at end of file From c6a111bb4c90631c7118e6332c4b69ae63b8fc3c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 11 Nov 2023 14:14:32 -0500 Subject: [PATCH 08/11] smtp wired up --- Cargo.lock | 19 ++- Cargo.toml | 2 +- crates/lib/email/src/lib.rs | 31 ---- crates/lib/{email => email_sender}/Cargo.toml | 2 +- crates/lib/email_sender/src/lib.rs | 13 ++ .../lib/email_sender/src/smtp_email_sender.rs | 33 ++++ crates/service/plugins/users/Cargo.toml | 2 +- .../plugins/users/src/default_routes.rs | 16 +- .../plugins/users/src/endpoints/mod.rs | 3 +- ...er.rs => password_reset_redeem_handler.rs} | 6 +- .../password_reset_request_handler.rs | 158 ++++++++++++++++++ crates/service/web/storyteller_web/Cargo.toml | 1 + .../service/web/storyteller_web/src/main.rs | 9 + .../web/storyteller_web/src/server_state.rs | 3 + 14 files changed, 247 insertions(+), 51 deletions(-) delete mode 100644 crates/lib/email/src/lib.rs rename crates/lib/{email => email_sender}/Cargo.toml (91%) create mode 100644 crates/lib/email_sender/src/lib.rs create mode 100644 crates/lib/email_sender/src/smtp_email_sender.rs rename crates/service/plugins/users/src/endpoints/{password_reset_requisition_handler.rs => password_reset_redeem_handler.rs} (96%) create mode 100644 crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs diff --git a/Cargo.lock b/Cargo.lock index e2b1448bb3..7a5bf68098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2374,14 +2374,6 @@ dependencies = [ "tokens", ] -[[package]] -name = "email" -version = "0.1.0" -dependencies = [ - "errors", - "lettre", -] - [[package]] name = "email-encoding" version = "0.2.0" @@ -2398,6 +2390,14 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" +[[package]] +name = "email_sender" +version = "0.1.0" +dependencies = [ + "errors", + "lettre", +] + [[package]] name = "encoding_rs" version = "0.8.28" @@ -5972,6 +5972,7 @@ dependencies = [ "easyenv", "elasticsearch", "elasticsearch_schema", + "email_sender", "enums", "errors", "futures 0.1.31", @@ -7315,7 +7316,7 @@ dependencies = [ "cookies", "crockford", "dotenv", - "email", + "email_sender", "enums", "errors", "hmac 0.10.1", diff --git a/Cargo.toml b/Cargo.toml index b4208e50b9..d92e92afa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ "crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers "crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve. "crates/lib/elasticsearch_schema", - "crates/lib/email", + "crates/lib/email_sender", "crates/lib/errors", "crates/lib/files/archives", "crates/lib/files/filesys", diff --git a/crates/lib/email/src/lib.rs b/crates/lib/email/src/lib.rs deleted file mode 100644 index d8cc00a3e8..0000000000 --- a/crates/lib/email/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -use lettre::{Message, SmtpTransport, Transport}; -use lettre::message::header::ContentType; -use lettre::transport::smtp::authentication::Credentials; - -use errors::AnyhowResult; - -pub async fn send_password_reset(email_address: &str, secret_key: String) -> AnyhowResult<()> { - let email = Message::builder() - .from("Support ".parse().unwrap()) - .to( email_address.parse().unwrap()) - .subject("Your password reset") - .header(ContentType::TEXT_PLAIN) - .body(format!("Here you go! {secret_key}")) - .unwrap(); - - let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned()); - - // Open a remote connection to gmail - let mailer = SmtpTransport::relay("smtp.gmail.com") - .unwrap() - .credentials(creds) - .build(); - - // Send the email - match mailer.send(&email) { - Ok(_) => println!("Email sent successfully!"), - Err(e) => panic!("Could not send email: {e:?}"), - } - - Ok(()) -} \ No newline at end of file diff --git a/crates/lib/email/Cargo.toml b/crates/lib/email_sender/Cargo.toml similarity index 91% rename from crates/lib/email/Cargo.toml rename to crates/lib/email_sender/Cargo.toml index b211388c94..8f2287d989 100644 --- a/crates/lib/email/Cargo.toml +++ b/crates/lib/email_sender/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "email" +name = "email_sender" version = "0.1.0" edition = "2021" diff --git a/crates/lib/email_sender/src/lib.rs b/crates/lib/email_sender/src/lib.rs new file mode 100644 index 0000000000..a9894c3ce0 --- /dev/null +++ b/crates/lib/email_sender/src/lib.rs @@ -0,0 +1,13 @@ +//! email_sender +//! +//! Provide SMTP and other email sending capabilities to the rest of our services. +//! + +/// Public re-exports +pub mod letter_exports { + pub use lettre::Message; + pub use lettre::message::header::ContentType; + pub use lettre::message::Mailbox; +} + +pub mod smtp_email_sender; diff --git a/crates/lib/email_sender/src/smtp_email_sender.rs b/crates/lib/email_sender/src/smtp_email_sender.rs new file mode 100644 index 0000000000..f2bf2ab445 --- /dev/null +++ b/crates/lib/email_sender/src/smtp_email_sender.rs @@ -0,0 +1,33 @@ +use lettre::{Message, SmtpTransport, Transport}; +use lettre::transport::smtp::authentication::Credentials; +use lettre::transport::smtp::response::Response; + +use errors::AnyhowResult; + +#[derive(Clone)] +pub struct SmtpEmailSender { + mailer: SmtpTransport, +} + +impl SmtpEmailSender { + + // NB: To set this up for Gmail: + // + // - https://support.google.com/a/answer/2956491?hl=en&fl=1&sjid=8333642123536524043-NA + // + // "Less secure apps" setting, which must be enabled at the organization level and within the + // individual user account: + // - https://support.google.com/mail/thread/5621336/bad-credentials-using-gmail-smtp?hl=en + // + pub fn new(relay: &str, username: String, password: String) -> AnyhowResult { + Ok(Self { + mailer: SmtpTransport::relay(relay)? + .credentials(Credentials::new(username, password)) + .build() + }) + } + + pub fn send_message(&self, message: &Message) -> AnyhowResult { + Ok(self.mailer.send(message)?) + } +} diff --git a/crates/service/plugins/users/Cargo.toml b/crates/service/plugins/users/Cargo.toml index c1dcd3460d..1684c36712 100644 --- a/crates/service/plugins/users/Cargo.toml +++ b/crates/service/plugins/users/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" container_common = { path = "../../../lib/deprecated/container_common" } cookies = { path = "../../../lib/cookies" } crockford = { path = "../../../lib/crockford" } -email = { path = "../../../lib/email" } +email_sender = { path = "../../../lib/email_sender" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } http_server_common = { path = "../../../lib/deprecated/http_server_common" } diff --git a/crates/service/plugins/users/src/default_routes.rs b/crates/service/plugins/users/src/default_routes.rs index 19c312436f..fe05cf9d69 100644 --- a/crates/service/plugins/users/src/default_routes.rs +++ b/crates/service/plugins/users/src/default_routes.rs @@ -7,7 +7,8 @@ use actix_web::error::Error; use crate::endpoints::create_account_handler::create_account_handler; use crate::endpoints::login_handler::login_handler; use crate::endpoints::logout_handler::logout_handler; -use crate::endpoints::password_reset_requisition_handler::password_reset_requisition_handler; +use crate::endpoints::password_reset_redeem_handler::password_reset_redeem_handler; +use crate::endpoints::password_reset_request_handler::password_reset_request_handler; use crate::endpoints::session_info_handler::session_info_handler; // NB: This does not include the user edit endpoints since FakeYou's API mounts them alongside other things. @@ -26,6 +27,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< { app .service( + // TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal web::resource("/create_account") .route(web::post().to(create_account_handler)) .route(web::head().to(|| HttpResponse::Ok())) @@ -36,6 +38,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< .route(web::head().to(|| HttpResponse::Ok())) ) .service( + // TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal web::resource("/login") .route(web::post().to(login_handler)) .route(web::head().to(|| HttpResponse::Ok())) @@ -46,6 +49,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< .route(web::head().to(|| HttpResponse::Ok())) ) .service( + // TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal web::resource("/logout") .route(web::post().to(logout_handler)) .route(web::head().to(|| HttpResponse::Ok())) @@ -56,6 +60,7 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< .route(web::head().to(|| HttpResponse::Ok())) ) .service( + // TODO(bt,2022-11-16): non-/v1/ endpoints are deprecated and subject for future removal web::resource("/session") .route(web::get().to(session_info_handler)) .route(web::head().to(|| HttpResponse::Ok())) @@ -66,8 +71,13 @@ pub fn add_suggested_api_v1_account_creation_and_session_routes (app: App< .route(web::head().to(|| HttpResponse::Ok())) ) .service( - web::resource("/v1/request-reset") - .route(web::post().to(password_reset_requisition_handler)) + web::resource("/v1/password_reset/request") + .route(web::post().to(password_reset_request_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::resource("/v1/password_reset/redeem") + .route(web::post().to(password_reset_redeem_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) } diff --git a/crates/service/plugins/users/src/endpoints/mod.rs b/crates/service/plugins/users/src/endpoints/mod.rs index 3523568042..e92d7130d5 100644 --- a/crates/service/plugins/users/src/endpoints/mod.rs +++ b/crates/service/plugins/users/src/endpoints/mod.rs @@ -3,5 +3,6 @@ pub mod edit_profile_handler; pub mod get_profile_handler; pub mod login_handler; pub mod logout_handler; -pub mod password_reset_requisition_handler; +pub mod password_reset_redeem_handler; +pub mod password_reset_request_handler; pub mod session_info_handler; \ No newline at end of file diff --git a/crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs similarity index 96% rename from crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs rename to crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs index e3ec058cc9..6a85f433d0 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_requisition_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs @@ -85,7 +85,7 @@ impl ResponseError for PasswordResetRequisitionErrorResponse { /// - create password reset request /// - sends email? /// - inserts record and stuff -pub async fn password_reset_requisition_handler( +pub async fn password_reset_redeem_handler( http_request: HttpRequest, request: web::Json, mysql_pool: web::Data, @@ -112,8 +112,6 @@ pub async fn password_reset_requisition_handler( err })?; - email::send_password_reset(&user.email_address, secret_key).await?; - let response = PasswordResetRequisitionResponse { success: true, }; @@ -127,4 +125,4 @@ pub async fn password_reset_requisition_handler( Ok(HttpResponse::Ok() .content_type("application/json") .body(body)) -} \ No newline at end of file +} diff --git a/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs new file mode 100644 index 0000000000..4e0131f83a --- /dev/null +++ b/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs @@ -0,0 +1,158 @@ +/* + ~ B R A I N S T O R M ~ + + - redeem reset request + - needs a way to identify the user because only the tuple is unique (user + key) + username or email address + - provide new password + */ + +use std::fmt::Display; + +use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; +use actix_web::http::StatusCode; +use log::{error, warn}; +use serde::Deserialize; +use sqlx::MySqlPool; +use strum_macros::Display; + +use crockford::crockford_entropy_lower; +use email_sender::letter_exports; +use email_sender::letter_exports::Message; +use email_sender::smtp_email_sender::SmtpEmailSender; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; +use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; +use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; + +#[derive(Deserialize)] +pub struct PasswordResetRequestedRequest { + username_or_email: String, +} + +#[derive(Serialize)] +pub struct PasswordResetRequestedResponse { + success: bool, +} + +#[derive(Serialize, Debug, Display)] +pub enum PasswordResetRequestedRequestError { + NoSuchUser, + Internal, +} + +#[derive(Serialize, Debug)] +pub struct PasswordResetRequestedErrorResponse { + kind: PasswordResetRequestedRequestError, +} +impl Display for PasswordResetRequestedErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for PasswordResetRequestedErrorResponse { + fn from(value: PasswordResetRequestedRequestError) -> Self { + Self { kind: value } + } +} +impl From for PasswordResetRequestedErrorResponse { + fn from(value: errors::AnyhowError) -> Self { + log::error!("Internal error: {value}"); + Self { kind: PasswordResetRequestedRequestError::Internal } + } +} + +impl ResponseError for PasswordResetRequestedErrorResponse { + //TODO: Yknow, clean this up and stuff + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } + } + +/// +/// Non-authenticated! +/// +/// - create password reset request +/// - sends email? +/// - inserts record and stuff +pub async fn password_reset_request_handler( + http_request: HttpRequest, + request: web::Json, + mysql_pool: web::Data, + sender: web::Data, +) -> Result { + + let username_or_email = request.username_or_email.trim(); + + let user = if username_or_email.contains("@") { + lookup_user_for_login_by_email(&username_or_email, &mysql_pool).await + } else { + lookup_user_for_login_by_username(&username_or_email, &mysql_pool).await + }.map_err(|e| { + warn!("Password reset user lookup: {:?}", e); + //TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻 + PasswordResetRequestedRequestError::NoSuchUser + })?; + + let secret_key = crockford_entropy_lower(32); + + //TODO: Handle banned users, they shouldn't be able to do this + + let ip_address = get_request_ip(&http_request); + + create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await + .map_err(|err| { + log::error!("Error creating password reset: {err}"); + PasswordResetRequestedRequestError::Internal + })?; + + let from_address = "Support " + .parse() + .map_err(|err| { + log::error!("Error parsing from address: {err}"); + PasswordResetRequestedRequestError::Internal + })?; + + let to_address = user.email_address + .parse() + .map_err(|err| { + log::error!("Error parsing to address: {err}"); + PasswordResetRequestedRequestError::Internal + })?; + + let email = Message::builder() + .from(from_address) + .to(to_address) + .subject("FakeYou Password Reset") + .header(letter_exports::ContentType::TEXT_PLAIN) + .body(format!("Here's the secret key: {secret_key}")) + .map_err(|err| { + log::error!("Error constructing email: {err}"); + PasswordResetRequestedRequestError::Internal + })?; + + sender.send_message(&email).map_err(|err| { + log::error!("Error sending email: {err}"); + PasswordResetRequestedRequestError::Internal + })?; + + let response = PasswordResetRequestedResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| { + error!("error returning response: {:?}", e); + PasswordResetRequestedRequestError::Internal + })?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index daa92978ad..0346164aed 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -26,6 +26,7 @@ cookies = { path = "../../../lib/cookies" } datetimes = { path = "../../../lib/datetimes" } easyenv = { path = "../../../lib/easyenv" } elasticsearch_schema = { path = "../../../lib/elasticsearch_schema" } +email_sender= { path = "../../../lib/email_sender" } enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } google_drive_common = { path = "../../../lib/api_clients/google_drive_common" } diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index 9fa4c09ff3..19456ebbc7 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -57,6 +57,7 @@ use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; use config::shared_constants::DEFAULT_RUST_LOG; use container_common::files::read_toml_file_to_struct::read_toml_file_to_struct; +use email_sender::smtp_email_sender::SmtpEmailSender; use errors::AnyhowResult; use http_server_common::cors::{build_cors_config, build_production_cors_config}; use memory_caching::single_item_ttl_cache::SingleItemTtlCache; @@ -434,6 +435,12 @@ async fn main() -> AnyhowResult<()> { .trim() .to_string(); + // TODO(bt,2023-11-11): Password and account details should be a secret, but gotta go fast. + let email_sender = SmtpEmailSender::new( + "smtp.gmail.com", + "noreply@storyteller.ai".to_string(), + "FakeYouHanashi1".to_string())?; + let server_state = ServerState { env_config: EnvConfig { num_workers, @@ -475,6 +482,7 @@ async fn main() -> AnyhowResult<()> { audio_uploads_bucket_root, sort_key_crypto, static_api_token_set, + email_sender, caches: InMemoryCaches { durable: DurableInMemoryCaches { model_token_info: model_token_info_cache, @@ -651,6 +659,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()> .app_data(web::Data::new(server_state_arc.stripe.clone().config.clone())) .app_data(web::Data::new(server_state_arc.stripe.clone().client.clone())) .app_data(web::Data::new(server_state_arc.third_party_url_redirector.clone())) + .app_data(web::Data::new(server_state_arc.email_sender.clone())) .app_data(web::Data::new(server_environment)) .app_data(web::Data::from(product_lookup)) // NB: Data::from(Arc) for dynamic dispatch .app_data(web::Data::from(stripe_lookup)) // NB: Data::from(Arc) for dynamic dispatch diff --git a/crates/service/web/storyteller_web/src/server_state.rs b/crates/service/web/storyteller_web/src/server_state.rs index b61c443524..41ddadf1d3 100644 --- a/crates/service/web/storyteller_web/src/server_state.rs +++ b/crates/service/web/storyteller_web/src/server_state.rs @@ -6,6 +6,7 @@ use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSe use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList; use billing_component::stripe::stripe_config::StripeConfig; use cloud_storage::bucket_client::BucketClient; +use email_sender::smtp_email_sender::SmtpEmailSender; use memory_caching::single_item_ttl_cache::SingleItemTtlCache; use mysql_queries::mediators::badge_granter::BadgeGranter; use mysql_queries::mediators::firehose_publisher::FirehosePublisher; @@ -78,6 +79,8 @@ pub struct ServerState { pub sort_key_crypto: SortKeyCrypto, + pub email_sender: SmtpEmailSender, + pub ip_ban_list: IpBanList, pub cidr_ban_set: BannedCidrSet, From e068af0e1c2b982428aba713892d3ec087afc479 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 11 Nov 2023 16:23:30 -0500 Subject: [PATCH 09/11] wip redemption --- .../user_tables/password_resets.sql | 4 + .../tokens/src/tokens/password_reset.rs | 2 +- .../mysql_queries/src/queries/users/mod.rs | 1 + .../src/queries/users/user/mod.rs | 1 - .../create_password_reset_request.rs | 2 +- .../lookup_password_reset_request.rs | 41 +++++++ .../queries/users/user_password_resets/mod.rs | 2 + .../password_reset_redeem_handler.rs | 108 +++++++++--------- .../password_reset_request_handler.rs | 13 +-- 9 files changed, 106 insertions(+), 68 deletions(-) rename crates/lib/mysql_queries/src/queries/users/{user => user_password_resets}/create_password_reset_request.rs (89%) create mode 100644 crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs create mode 100644 crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs diff --git a/_sql/migrations_squashed/user_tables/password_resets.sql b/_sql/migrations_squashed/user_tables/password_resets.sql index 1be17e0f53..4516fc6c24 100644 --- a/_sql/migrations_squashed/user_tables/password_resets.sql +++ b/_sql/migrations_squashed/user_tables/password_resets.sql @@ -16,6 +16,7 @@ -- Also when creating the password hash, be sure to set `expires_at` to be "NOW() + 3 hours", or -- some other small, reasonable time frame. -- +-- TODO: Rename to 'user_password_resets' CREATE TABLE password_resets ( -- Not used for anything except replication. id BIGINT(20) NOT NULL AUTO_INCREMENT, @@ -27,10 +28,12 @@ CREATE TABLE password_resets ( -- Foreign key to user user_token VARCHAR(32) NOT NULL, + -- TODO: Change to `public_reset_token` -- Secret "key" for the password reset -- This will be shared via URL or user text input. secret_key VARCHAR(32) NOT NULL, + -- TODO: Make unsigned. Also unsigned on users. -- Copied from the user record at the time of password reset issuance. -- If the user's password version is greater than this value, then this -- reset is no longer valid. @@ -59,6 +62,7 @@ CREATE TABLE password_resets ( PRIMARY KEY (id), UNIQUE KEY (token), + -- TODO: Change to UNIQUE on `public_reset_token` UNIQUE KEY (user_token, secret_key), KEY fk_user_token (user_token), diff --git a/crates/_types/tokens/src/tokens/password_reset.rs b/crates/_types/tokens/src/tokens/password_reset.rs index 06a59846be..fb84250ec1 100644 --- a/crates/_types/tokens/src/tokens/password_reset.rs +++ b/crates/_types/tokens/src/tokens/password_reset.rs @@ -11,4 +11,4 @@ use crate::prefixes::TokenPrefix; pub struct PasswordResetToken(pub String); impl_string_token!(PasswordResetToken); -impl_crockford_generator!(PasswordResetToken, 32usize, TokenPrefix::PasswordReset, CrockfordUpper); +impl_crockford_generator!(PasswordResetToken, 32usize, TokenPrefix::PasswordReset, CrockfordMixed); diff --git a/crates/lib/mysql_queries/src/queries/users/mod.rs b/crates/lib/mysql_queries/src/queries/users/mod.rs index 57802a1b2b..1b5e04bc70 100644 --- a/crates/lib/mysql_queries/src/queries/users/mod.rs +++ b/crates/lib/mysql_queries/src/queries/users/mod.rs @@ -1,5 +1,6 @@ pub mod user; pub mod user_badges; +pub mod user_password_resets; pub mod user_profiles; pub mod user_ratings; pub mod user_roles; diff --git a/crates/lib/mysql_queries/src/queries/users/user/mod.rs b/crates/lib/mysql_queries/src/queries/users/user/mod.rs index cad31ea615..ca6d450fbf 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/mod.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/mod.rs @@ -1,5 +1,4 @@ pub mod create_account; -pub mod create_password_reset_request; pub mod get_user_token_by_username; pub mod list_users_query_builder; pub mod lookup_user_for_login_by_email; diff --git a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs similarity index 89% rename from crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs rename to crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs index 6e932dd0b9..4211a54edd 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/create_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs @@ -4,7 +4,7 @@ use sqlx::MySqlPool; use errors::AnyhowResult; use tokens::tokens::password_reset::PasswordResetToken; -use super::lookup_user_for_login_result::UserRecordForLogin; +use crate::queries::users::user::lookup_user_for_login_result::UserRecordForLogin; pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, ip_address: String, secret_key: String) -> AnyhowResult<()> { let token = PasswordResetToken::generate(); diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs new file mode 100644 index 0000000000..8096f85eaa --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs @@ -0,0 +1,41 @@ +use anyhow::anyhow; +use sqlx::{Error, MySqlPool}; + +use errors::AnyhowResult; +use tokens::tokens::users::UserToken; + +pub struct PasswordResetLookupInfo { + pub user_token: UserToken, +} + +// TODO: SELECT FOR UPDATE in a transaction. +pub async fn lookup_password_reset_request(password_reset_token: &str, pool: &MySqlPool) -> AnyhowResult> { + // NB: Lookup failure is Err(RowNotFound). + let result = sqlx::query_as!( + PasswordResetLookupInfo, + r#" +SELECT + pw.user_token as `user_token: tokens::tokens::users::UserToken` +FROM password_resets AS pw +JOIN users AS u + ON pw.user_token = u.token +WHERE secret_key = ? + AND pw.expires_at < NOW() + AND pw.current_password_version = u.password_version + AND NOT pw.is_redeemed + AND NOT u.is_banned +LIMIT 1 + "#, + password_reset_token, + ) + .fetch_one(pool) + .await; + + match result { + Ok(record) => Ok(Some(record)), + Err(err) => match err { + Error::RowNotFound => Ok(None), + _ => Err(anyhow!(err)), + }, + } +} diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs new file mode 100644 index 0000000000..b51f198c2e --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs @@ -0,0 +1,2 @@ +pub mod create_password_reset_request; +pub mod lookup_password_reset_request; diff --git a/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs index 6a85f433d0..fa58b75afe 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs @@ -1,12 +1,3 @@ -/* - ~ B R A I N S T O R M ~ - - - redeem reset request - - needs a way to identify the user because only the tuple is unique (user + key) - username or email address - - provide new password - */ - use std::fmt::Display; use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; @@ -16,59 +7,56 @@ use serde::Deserialize; use sqlx::MySqlPool; use strum_macros::Display; -use crockford::crockford_entropy_lower; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; - -/// This can be reused in login requests in the future! -#[derive(Deserialize)] -pub enum UserLogin { - Email(String), - Username(String), -} +use mysql_queries::queries::users::user_password_resets::lookup_password_reset_request::lookup_password_reset_request; #[derive(Deserialize)] -pub struct PasswordResetRequisitionRequest { - login: UserLogin, +pub struct PasswordResetRedemptionRequest { + username_or_email: String, + reset_token: String, + new_password: String, + new_password_validation: String, } #[derive(Serialize)] -pub struct PasswordResetRequisitionResponse { +pub struct PasswordResetRedemptionResponse { success: bool, } #[derive(Serialize, Debug, Display)] -pub enum PasswordResetRequisitionRequestError { - NoSuchUser, +pub enum PasswordResetRedemptionError { + /// Account does not exist or reset token is wrong. + InvalidRedemption, + PasswordsDoNotMatch, Internal, } #[derive(Serialize, Debug)] -pub struct PasswordResetRequisitionErrorResponse { - kind: PasswordResetRequisitionRequestError, +pub struct PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError, } -impl Display for PasswordResetRequisitionErrorResponse { +impl Display for PasswordResetRedemptionErrorResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } -impl From for PasswordResetRequisitionErrorResponse { - fn from(value: PasswordResetRequisitionRequestError) -> Self { +impl From for PasswordResetRedemptionErrorResponse { + fn from(value: PasswordResetRedemptionError) -> Self { Self { kind: value } } } -impl From for PasswordResetRequisitionErrorResponse { +impl From for PasswordResetRedemptionErrorResponse { fn from(value: errors::AnyhowError) -> Self { log::error!("Internal error: {value}"); - Self { kind: PasswordResetRequisitionRequestError::Internal } + Self { kind: PasswordResetRedemptionError::Internal } } } -impl ResponseError for PasswordResetRequisitionErrorResponse { +impl ResponseError for PasswordResetRedemptionErrorResponse { //TODO: Yknow, clean this up and stuff fn status_code(&self) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR @@ -79,47 +67,59 @@ impl ResponseError for PasswordResetRequisitionErrorResponse { } } -/// -/// Non-authenticated! -/// -/// - create password reset request -/// - sends email? -/// - inserts record and stuff pub async fn password_reset_redeem_handler( http_request: HttpRequest, - request: web::Json, + request: web::Json, mysql_pool: web::Data, -) -> Result { +) -> Result { + let username_or_email = request.username_or_email.trim(); + + //let user = if username_or_email.contains("@") { + // lookup_user_for_login_by_email(&username_or_email, &mysql_pool).await + //} else { + // lookup_user_for_login_by_username(&username_or_email, &mysql_pool).await + //}.map_err(|e| { + // warn!("Password reset redemption user lookup error: {:?}", e); + // PasswordResetRedemptionError::InvalidRedemption + //})?; let ip_address = get_request_ip(&http_request); - let user = match &request.login { - UserLogin::Email(email) => lookup_user_for_login_by_email(&email, &mysql_pool).await, - UserLogin::Username(username) => lookup_user_for_login_by_username(&username, &mysql_pool).await, - }.map_err(|e| { - warn!("Password reset user lookup: {:?}", e); - PasswordResetRequisitionRequestError::NoSuchUser - //TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻 - })?; - - let secret_key = crockford_entropy_lower(32); - //TODO: Handle banned users, they shouldn't be able to do this - create_password_reset(&mysql_pool, &user, ip_address, secret_key.clone()).await + let result = lookup_password_reset_request(&request.reset_token, &mysql_pool).await .map_err(|err| { log::error!("Password reset error {:?}", err); err - })?; + }); - let response = PasswordResetRequisitionResponse { + let reset_state = match result { + Ok(Some(reset_state)) => reset_state, + Ok(None) => { + warn!("No such reset request."); + return Err(PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError::InvalidRedemption, + }); + } + Err(err) => { + error!("lookup error: {err}"); + return Err(PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError::InvalidRedemption, + }); + } + }; + + + println!("Reset state: {}", reset_state.user_token); + + let response = PasswordResetRedemptionResponse { success: true, }; let body = serde_json::to_string(&response) .map_err(|e| { error!("error returning response: {:?}", e); - PasswordResetRequisitionRequestError::Internal + PasswordResetRedemptionError::Internal })?; Ok(HttpResponse::Ok() diff --git a/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs index 4e0131f83a..7560c127f5 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_request_handler.rs @@ -1,12 +1,3 @@ -/* - ~ B R A I N S T O R M ~ - - - redeem reset request - - needs a way to identify the user because only the tuple is unique (user + key) - username or email address - - provide new password - */ - use std::fmt::Display; use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; @@ -22,9 +13,9 @@ use email_sender::letter_exports::Message; use email_sender::smtp_email_sender::SmtpEmailSender; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use mysql_queries::queries::users::user::create_password_reset_request::create_password_reset; use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; +use mysql_queries::queries::users::user_password_resets::create_password_reset_request::create_password_reset; #[derive(Deserialize)] pub struct PasswordResetRequestedRequest { @@ -95,7 +86,7 @@ pub async fn password_reset_request_handler( } else { lookup_user_for_login_by_username(&username_or_email, &mysql_pool).await }.map_err(|e| { - warn!("Password reset user lookup: {:?}", e); + warn!("Password reset user lookup error: {:?}", e); //TODO: This could be anything, not necessarily a lookup. The name is misleading 🤷🏻 PasswordResetRequestedRequestError::NoSuchUser })?; From a69d0f7b9a16820ab1b16694e220a8fc78c3d7ac Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 11 Nov 2023 19:46:53 -0500 Subject: [PATCH 10/11] redemption works --- Cargo.lock | 10 ++- Cargo.toml | 1 + .../change_password_from_password_reset.rs | 70 +++++++++++++++++++ .../lookup_password_reset_request.rs | 28 ++++++-- .../queries/users/user_password_resets/mod.rs | 1 + crates/lib/password/Cargo.toml | 27 +++++++ .../password/src/bcrypt_confirm_password.rs | 28 ++++++++ .../lib/password/src/bcrypt_hash_password.rs | 19 +++++ crates/lib/password/src/lib.rs | 24 +++++++ crates/service/plugins/users/Cargo.toml | 2 +- .../src/endpoints/create_account_handler.rs | 3 +- .../users/src/endpoints/login_handler.rs | 5 +- .../password_reset_redeem_handler.rs | 50 +++++++++---- 13 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs create mode 100644 crates/lib/password/Cargo.toml create mode 100644 crates/lib/password/src/bcrypt_confirm_password.rs create mode 100644 crates/lib/password/src/bcrypt_hash_password.rs create mode 100644 crates/lib/password/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 7a5bf68098..bab7f29ad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4358,6 +4358,14 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "password" +version = "0.0.1" +dependencies = [ + "bcrypt", + "errors", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -7309,7 +7317,6 @@ dependencies = [ "actix-web 4.0.1", "anyhow", "base64 0.21.0", - "bcrypt", "byteorder", "chrono", "container_common", @@ -7327,6 +7334,7 @@ dependencies = [ "md-5", "mysql_queries", "once_cell", + "password", "r2d2_redis", "rand 0.8.5", "redis_caching", diff --git a/Cargo.toml b/Cargo.toml index d92e92afa8..de75c6d0d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "crates/lib/jobs_common", "crates/lib/logging", "crates/lib/mysql_queries", + "crates/lib/password", "crates/lib/redis_common", "crates/lib/server_environment", "crates/lib/sqlx_mysql_helpers", diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs new file mode 100644 index 0000000000..c9fb61c8b5 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs @@ -0,0 +1,70 @@ +use anyhow::anyhow; +use sqlx::{MySql, Transaction}; + +use errors::AnyhowResult; +use tokens::tokens::password_reset::PasswordResetToken; +use tokens::tokens::users::UserToken; + +pub struct ChangePasswordFromPasswordResetArgs<'a, 'b> { + pub password_reset_token: &'a PasswordResetToken, + pub user_token: &'a UserToken, + pub new_password_hash: &'a str, + pub ip_address: &'a str, + pub mysql_transaction: Transaction<'b, MySql>, +} + +pub async fn change_password_from_password_reset<'a, 'b>( + mut args: ChangePasswordFromPasswordResetArgs<'a, 'b> +) -> AnyhowResult<()> { + + let query = sqlx::query!( + r#" +UPDATE users +SET + password_hash = ?, + ip_address_last_update = ?, + password_version = password_version + 1, + version = version + 1 +WHERE + token = ? +LIMIT 1 + "#, + args.new_password_hash, + args.ip_address, + args.user_token, + ); + + let query_result = query.execute(&mut args.mysql_transaction).await; + + if let Err(err) = query_result { + let _r = args.mysql_transaction.rollback().await.ok(); + return Err(anyhow!("Error updating user password from password reset request: {err}")); + } + + let query = sqlx::query!( + r#" +UPDATE password_resets +SET + is_redeemed = true, + ip_address_redemption = ?, + version = version + 1 +WHERE + token = ? +LIMIT 1 + "#, + args.ip_address, + args.password_reset_token, + ); + + let query_result = query.execute(&mut args.mysql_transaction).await; + + if let Err(err) = query_result { + let _r = args.mysql_transaction.rollback().await.ok(); + return Err(anyhow!("Error updating user password from password reset request: {err}")); + } + + match args.mysql_transaction.commit().await { + Ok(_) => Ok(()), + Err(err) => Err(anyhow!("Error committing password reset changes: {err}")), + } +} diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs index 8096f85eaa..b7f7021a15 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs @@ -1,38 +1,52 @@ use anyhow::anyhow; -use sqlx::{Error, MySqlPool}; +use sqlx::{Error, MySql, MySqlPool, Transaction}; use errors::AnyhowResult; +use tokens::tokens::password_reset::PasswordResetToken; use tokens::tokens::users::UserToken; +pub struct PasswordResetStateAndTransaction <'a> { + pub transaction: Transaction<'a, MySql>, + pub reset_state: PasswordResetLookupInfo, +} + pub struct PasswordResetLookupInfo { + pub password_reset_token: PasswordResetToken, pub user_token: UserToken, } -// TODO: SELECT FOR UPDATE in a transaction. -pub async fn lookup_password_reset_request(password_reset_token: &str, pool: &MySqlPool) -> AnyhowResult> { - // NB: Lookup failure is Err(RowNotFound). +/// Begin a lookup transaction. +/// `select for update` ensures the record won't change, blocking race conditions. +pub async fn lookup_password_reset_request<'a>(password_reset_token: &str, pool: &'a MySqlPool) -> AnyhowResult>> { + let mut transaction = pool.begin().await?; + let result = sqlx::query_as!( PasswordResetLookupInfo, r#" SELECT + pw.token as `password_reset_token: tokens::tokens::password_reset::PasswordResetToken`, pw.user_token as `user_token: tokens::tokens::users::UserToken` FROM password_resets AS pw JOIN users AS u ON pw.user_token = u.token WHERE secret_key = ? - AND pw.expires_at < NOW() + AND pw.expires_at > NOW() AND pw.current_password_version = u.password_version AND NOT pw.is_redeemed AND NOT u.is_banned LIMIT 1 +FOR UPDATE "#, password_reset_token, ) - .fetch_one(pool) + .fetch_one(&mut transaction) .await; match result { - Ok(record) => Ok(Some(record)), + Ok(record) => Ok(Some(PasswordResetStateAndTransaction { + transaction, + reset_state: record, + })), Err(err) => match err { Error::RowNotFound => Ok(None), _ => Err(anyhow!(err)), diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs index b51f198c2e..4deb8ed56f 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/mod.rs @@ -1,2 +1,3 @@ +pub mod change_password_from_password_reset; pub mod create_password_reset_request; pub mod lookup_password_reset_request; diff --git a/crates/lib/password/Cargo.toml b/crates/lib/password/Cargo.toml new file mode 100644 index 0000000000..6838578472 --- /dev/null +++ b/crates/lib/password/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "password" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +# NB: Public "password" crate is squatted for over eight years +name = "password" +path = "src/lib.rs" + +[dependencies] +# Internal +errors = { path = "../errors" } + +# External +bcrypt = "0.8" # password hashes +#hmac = "0.10.0" # jwt signing +#jwt = "0.13.0" +#sha2 = "0.9.5" # hmac signing + +[dev-dependencies] +# None yet diff --git a/crates/lib/password/src/bcrypt_confirm_password.rs b/crates/lib/password/src/bcrypt_confirm_password.rs new file mode 100644 index 0000000000..1319e0f4fc --- /dev/null +++ b/crates/lib/password/src/bcrypt_confirm_password.rs @@ -0,0 +1,28 @@ +use errors::AnyhowResult; + +pub fn bcrypt_confirm_password(password: String, bcrypt_hash: &str) -> AnyhowResult { + let verified = bcrypt::verify(&password, &bcrypt_hash)?; + Ok(verified) +} + +#[cfg(test)] +mod tests { + use crate::bcrypt_confirm_password::bcrypt_confirm_password; + + #[test] + fn test_confirmation() { + // NB: This is to make sure we don't migrate the algorithm to a state where it won't work anymore. + // NB: Be mindful that these are expensive to compute and don't add too many test cases. + let hash_to_passwords = [ + ("$2b$12$ieU6.dygzbiZjtED5Qz8Vu8fSjDjE9R4AykzpGI5hL666AjV753Iu", "password"), + ("$2b$12$nGHAaxDCXj0oC/C5vbnhAOQhZHprKmJ8eTnX.uVKXvQNFildSAIAa", "hunter2"), + ("$2b$12$USIc/tDDOobgW96sBdw13u2Ts9bmddMHDrrixBdwASxur2h4oy8iC", "hunter2"), // hunter2 again + ("$2b$12$9418BXjnbbJwfu1YFe4r3OOsePo6kflnqypfOFnA1QsID06mqn3X.", "&*KA@jh2l9*2!!2"), + ]; + + for (hash, password) in hash_to_passwords { + let valid = bcrypt_confirm_password(password.to_string(), hash).unwrap(); + assert!(valid); + } + } +} \ No newline at end of file diff --git a/crates/lib/password/src/bcrypt_hash_password.rs b/crates/lib/password/src/bcrypt_hash_password.rs new file mode 100644 index 0000000000..9e0c11a051 --- /dev/null +++ b/crates/lib/password/src/bcrypt_hash_password.rs @@ -0,0 +1,19 @@ +use errors::AnyhowResult; + +pub fn bcrypt_hash_password(password: String) -> AnyhowResult { + let hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST)?; + Ok(hash) +} + +#[cfg(test)] +mod tests { + use crate::bcrypt_hash_password::bcrypt_hash_password; + + #[test] + fn test_password_hash_round_trip() { + let password = "password"; + let hash = bcrypt_hash_password(password.to_string()).unwrap(); + let valid = bcrypt::verify(password, &hash).unwrap(); + assert!(valid); + } +} diff --git a/crates/lib/password/src/lib.rs b/crates/lib/password/src/lib.rs new file mode 100644 index 0000000000..045836a245 --- /dev/null +++ b/crates/lib/password/src/lib.rs @@ -0,0 +1,24 @@ +//! password +//! +//! Password-related functions +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +// TODO(bt,2023-11-11): Start using a more modern / safer password hashing algorithm such as Argon2 or scrypt +// https://medium.com/@mpreziuso/password-hashing-pbkdf2-scrypt-bcrypt-and-argon2-e25aaf41598e +// https://stytch.com/blog/argon2-vs-bcrypt-vs-scrypt/ +pub mod bcrypt_hash_password; +pub mod bcrypt_confirm_password; diff --git a/crates/service/plugins/users/Cargo.toml b/crates/service/plugins/users/Cargo.toml index 1684c36712..840148a663 100644 --- a/crates/service/plugins/users/Cargo.toml +++ b/crates/service/plugins/users/Cargo.toml @@ -23,6 +23,7 @@ enums = { path = "../../../_types/enums" } errors = { path = "../../../lib/errors" } http_server_common = { path = "../../../lib/deprecated/http_server_common" } mysql_queries = { path = "../../../lib/mysql_queries" } +password = { path = "../../../lib/password" } redis_caching = { path = "../../../lib/caching/redis_caching" } redis_common = { path = "../../../lib/redis_common" } reusable_types = { path = "../../../_types/reusable_types" } @@ -47,7 +48,6 @@ actix-web = "4.0.0-beta.10" jwt = "0.13.0" # Crypto -bcrypt = "0.8" # password hashes hmac = "0.10.0" # cookies md-5 = "0.9.1" # gravatar sha2 = "0.9.5" # file hashes, inference text hashes, etc. diff --git a/crates/service/plugins/users/src/endpoints/create_account_handler.rs b/crates/service/plugins/users/src/endpoints/create_account_handler.rs index bf9f1d9a24..a0ea2a2ccf 100644 --- a/crates/service/plugins/users/src/endpoints/create_account_handler.rs +++ b/crates/service/plugins/users/src/endpoints/create_account_handler.rs @@ -18,6 +18,7 @@ use http_server_common::response::serialize_as_json_error::serialize_as_json_err use mysql_queries::mediators::firehose_publisher::FirehosePublisher; use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError}; use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session; +use password::bcrypt_hash_password::bcrypt_hash_password; use user_input_common::check_for_slurs::contains_slurs; use crate::cookies::session::session_cookie_manager::SessionCookieManager; @@ -132,7 +133,7 @@ pub async fn create_account_handler( }); } - let password_hash = match bcrypt::hash(&request.password, bcrypt::DEFAULT_COST) { + let password_hash = match bcrypt_hash_password(request.password.clone()) { Ok(hash) => hash, Err(err) => { warn!("Bcrypt error: {:?}", err); diff --git a/crates/service/plugins/users/src/endpoints/login_handler.rs b/crates/service/plugins/users/src/endpoints/login_handler.rs index 5953d61db5..b353769e5a 100644 --- a/crates/service/plugins/users/src/endpoints/login_handler.rs +++ b/crates/service/plugins/users/src/endpoints/login_handler.rs @@ -18,6 +18,7 @@ use mysql_queries::helpers::boolean_converters::i8_to_bool; use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session; +use password::bcrypt_confirm_password::bcrypt_confirm_password; use crate::cookies::session::session_cookie_manager::SessionCookieManager; @@ -123,7 +124,7 @@ pub async fn login_handler( } }; - match bcrypt::verify(&request.password, &actual_hash) { + match bcrypt_confirm_password(request.password.clone(), &actual_hash) { Err(e) => { warn!("Login hash comparison error: {:?}", e); return Err(LoginErrorResponse::server_error()); @@ -135,7 +136,7 @@ pub async fn login_handler( } // Good to go...! }, - }; + } let ip_address = get_request_ip(&http_request); diff --git a/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs index fa58b75afe..9afd3daa73 100644 --- a/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs +++ b/crates/service/plugins/users/src/endpoints/password_reset_redeem_handler.rs @@ -9,9 +9,9 @@ use strum_macros::Display; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use mysql_queries::queries::users::user::lookup_user_for_login_by_email::lookup_user_for_login_by_email; -use mysql_queries::queries::users::user::lookup_user_for_login_by_username::lookup_user_for_login_by_username; +use mysql_queries::queries::users::user_password_resets::change_password_from_password_reset::{change_password_from_password_reset, ChangePasswordFromPasswordResetArgs}; use mysql_queries::queries::users::user_password_resets::lookup_password_reset_request::lookup_password_reset_request; +use password::bcrypt_hash_password::bcrypt_hash_password; #[derive(Deserialize)] pub struct PasswordResetRedemptionRequest { @@ -72,18 +72,14 @@ pub async fn password_reset_redeem_handler( request: web::Json, mysql_pool: web::Data, ) -> Result { - let username_or_email = request.username_or_email.trim(); - //let user = if username_or_email.contains("@") { - // lookup_user_for_login_by_email(&username_or_email, &mysql_pool).await - //} else { - // lookup_user_for_login_by_username(&username_or_email, &mysql_pool).await - //}.map_err(|e| { - // warn!("Password reset redemption user lookup error: {:?}", e); - // PasswordResetRedemptionError::InvalidRedemption - //})?; + let new_password = request.new_password.trim(); - let ip_address = get_request_ip(&http_request); + if new_password != request.new_password_validation.trim() { + return Err(PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError::PasswordsDoNotMatch, + }); + } //TODO: Handle banned users, they shouldn't be able to do this @@ -93,7 +89,7 @@ pub async fn password_reset_redeem_handler( err }); - let reset_state = match result { + let transaction_and_state = match result { Ok(Some(reset_state)) => reset_state, Ok(None) => { warn!("No such reset request."); @@ -109,8 +105,34 @@ pub async fn password_reset_redeem_handler( } }; + println!("Reset state: {}", transaction_and_state.reset_state.user_token); - println!("Reset state: {}", reset_state.user_token); + let ip_address = get_request_ip(&http_request); + + let password_hash = match bcrypt_hash_password(new_password.to_string()) { + Ok(hash) => hash, + Err(err) => { + error!("password hash error: {err}"); + return Err(PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError::Internal, + }); + } + }; + + let result = change_password_from_password_reset(ChangePasswordFromPasswordResetArgs { + password_reset_token: &transaction_and_state.reset_state.password_reset_token, + user_token: &transaction_and_state.reset_state.user_token, + new_password_hash: &password_hash, + ip_address: &ip_address, + mysql_transaction: transaction_and_state.transaction, + }).await; + + if let Err(err) = result { + error!("password reset error: {err}"); + return Err(PasswordResetRedemptionErrorResponse { + kind: PasswordResetRedemptionError::Internal, + }); + } let response = PasswordResetRedemptionResponse { success: true, From 34c90becbb8023f1b86372651c11101e3a6c5f92 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 12 Nov 2023 02:01:50 -0500 Subject: [PATCH 11/11] schema migrations for password reset --- .../down.sql | 5 +++++ .../up.sql | 5 +++++ .../down.sql | 11 +++++++++++ .../up.sql | 9 +++++++++ .../down.sql | 11 +++++++++++ .../up.sql | 10 ++++++++++ .../down.sql | 6 ++++++ .../up.sql | 6 ++++++ .../down.sql | 12 ++++++++++++ .../up.sql | 12 ++++++++++++ ...ssword_resets.sql => user_password_resets.sql} | 15 +++++---------- _sql/migrations_squashed/user_tables/users.sql | 4 ++-- crates/_types/tokens/src/prefixes.rs | 4 +--- .../users/user/lookup_user_for_login_result.rs | 2 +- .../change_password_from_password_reset.rs | 2 +- .../create_password_reset_request.rs | 4 ++-- .../lookup_password_reset_request.rs | 4 ++-- 17 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 _sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/down.sql create mode 100644 _sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/up.sql create mode 100644 _sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/down.sql create mode 100644 _sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/up.sql create mode 100644 _sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/down.sql create mode 100644 _sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/up.sql create mode 100644 _sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/down.sql create mode 100644 _sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/up.sql create mode 100644 _sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/down.sql create mode 100644 _sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/up.sql rename _sql/migrations_squashed/user_tables/{password_resets.sql => user_password_resets.sql} (84%) diff --git a/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/down.sql b/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/down.sql new file mode 100644 index 0000000000..a8950d53e0 --- /dev/null +++ b/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +RENAME TABLE user_password_resets TO password_resets; diff --git a/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/up.sql b/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/up.sql new file mode 100644 index 0000000000..ef0a5a370f --- /dev/null +++ b/_sql/migrations/2023-11-12-005723_alter_password_resets_rename_table/up.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +RENAME TABLE password_resets TO user_password_resets; \ No newline at end of file diff --git a/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/down.sql b/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/down.sql new file mode 100644 index 0000000000..6faef36f66 --- /dev/null +++ b/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/down.sql @@ -0,0 +1,11 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- revert +ALTER TABLE user_password_resets + MODIFY COLUMN version INT NOT NULL DEFAULT 0; + +-- revert +ALTER TABLE user_password_resets + MODIFY COLUMN current_password_version INT NOT NULL DEFAULT 0; diff --git a/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/up.sql b/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/up.sql new file mode 100644 index 0000000000..3eb243cb9b --- /dev/null +++ b/_sql/migrations/2023-11-12-005759_alter_password_resets_to_unsigned_ints/up.sql @@ -0,0 +1,9 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE user_password_resets + MODIFY COLUMN current_password_version INT UNSIGNED NOT NULL DEFAULT 0; + +ALTER TABLE user_password_resets + MODIFY COLUMN version INT UNSIGNED NOT NULL DEFAULT 0; diff --git a/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/down.sql b/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/down.sql new file mode 100644 index 0000000000..7d46ba9b47 --- /dev/null +++ b/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/down.sql @@ -0,0 +1,11 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- revert +ALTER TABLE users + MODIFY COLUMN password_version INT NOT NULL DEFAULT 0; + +-- revert +ALTER TABLE users + MODIFY COLUMN version INT NOT NULL DEFAULT 0; diff --git a/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/up.sql b/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/up.sql new file mode 100644 index 0000000000..9e3521bf2d --- /dev/null +++ b/_sql/migrations/2023-11-12-005851_alter_users_to_unsigned_ints/up.sql @@ -0,0 +1,10 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE users + MODIFY COLUMN password_version INT UNSIGNED NOT NULL DEFAULT 0; + +ALTER TABLE users + MODIFY COLUMN version INT UNSIGNED NOT NULL DEFAULT 0; + diff --git a/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/down.sql b/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/down.sql new file mode 100644 index 0000000000..67b5870d98 --- /dev/null +++ b/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/down.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE user_password_resets + RENAME COLUMN public_reset_token TO secret_key; diff --git a/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/up.sql b/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/up.sql new file mode 100644 index 0000000000..35ddb56db2 --- /dev/null +++ b/_sql/migrations/2023-11-12-010940_alter_password_resets_secret_field_name/up.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE user_password_resets + RENAME COLUMN secret_key TO public_reset_token; diff --git a/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/down.sql b/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/down.sql new file mode 100644 index 0000000000..f0978a6636 --- /dev/null +++ b/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/down.sql @@ -0,0 +1,12 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE user_password_resets + DROP INDEX public_reset_token; + +ALTER TABLE user_password_resets + ADD UNIQUE INDEX unique_user_token_and_secret_key (user_token, public_reset_token); + +ALTER TABLE user_password_resets + ADD INDEX index_secret_key (public_reset_token); diff --git a/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/up.sql b/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/up.sql new file mode 100644 index 0000000000..17dd2f91a0 --- /dev/null +++ b/_sql/migrations/2023-11-12-010954_alter_password_resets_change_composite_key/up.sql @@ -0,0 +1,12 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE user_password_resets + DROP INDEX unique_user_token_and_secret_key; + +ALTER TABLE user_password_resets + ADD UNIQUE INDEX public_reset_token (public_reset_token); + +ALTER TABLE user_password_resets + DROP INDEX index_secret_key; diff --git a/_sql/migrations_squashed/user_tables/password_resets.sql b/_sql/migrations_squashed/user_tables/user_password_resets.sql similarity index 84% rename from _sql/migrations_squashed/user_tables/password_resets.sql rename to _sql/migrations_squashed/user_tables/user_password_resets.sql index 4516fc6c24..37ac3ea572 100644 --- a/_sql/migrations_squashed/user_tables/password_resets.sql +++ b/_sql/migrations_squashed/user_tables/user_password_resets.sql @@ -16,8 +16,7 @@ -- Also when creating the password hash, be sure to set `expires_at` to be "NOW() + 3 hours", or -- some other small, reasonable time frame. -- --- TODO: Rename to 'user_password_resets' -CREATE TABLE password_resets ( +CREATE TABLE user_password_resets ( -- Not used for anything except replication. id BIGINT(20) NOT NULL AUTO_INCREMENT, @@ -28,16 +27,14 @@ CREATE TABLE password_resets ( -- Foreign key to user user_token VARCHAR(32) NOT NULL, - -- TODO: Change to `public_reset_token` -- Secret "key" for the password reset -- This will be shared via URL or user text input. - secret_key VARCHAR(32) NOT NULL, + public_reset_token VARCHAR(32) NOT NULL, - -- TODO: Make unsigned. Also unsigned on users. -- Copied from the user record at the time of password reset issuance. -- If the user's password version is greater than this value, then this -- reset is no longer valid. - current_password_version INT NOT NULL DEFAULT 0, + current_password_version INT UNSIGNED NOT NULL DEFAULT 0, -- Whether the password reset has been consumed. is_redeemed BOOLEAN NOT NULL DEFAULT false, @@ -48,7 +45,7 @@ CREATE TABLE password_resets ( ip_address_redemption VARCHAR(40) DEFAULT NULL, -- Incremented with every update. - version INT NOT NULL DEFAULT 0, + version INT UNSIGNED NOT NULL DEFAULT 0, -- ========== TIMESTAMPS ========== @@ -61,9 +58,7 @@ CREATE TABLE password_resets ( -- INDICES -- PRIMARY KEY (id), UNIQUE KEY (token), - - -- TODO: Change to UNIQUE on `public_reset_token` - UNIQUE KEY (user_token, secret_key), + UNIQUE KEY (public_reset_token), KEY fk_user_token (user_token), KEY index_secret_key (secret_key), diff --git a/_sql/migrations_squashed/user_tables/users.sql b/_sql/migrations_squashed/user_tables/users.sql index 124a5b8b43..43b6805adb 100644 --- a/_sql/migrations_squashed/user_tables/users.sql +++ b/_sql/migrations_squashed/user_tables/users.sql @@ -41,7 +41,7 @@ CREATE TABLE users ( password_hash BINARY(60) NOT NULL, -- Incremented with every update to the password. - password_version INT NOT NULL DEFAULT 0, + password_version INT UNSIGNED NOT NULL DEFAULT 0, -- ========== ABUSE TRACKING ========== @@ -145,7 +145,7 @@ CREATE TABLE users ( -- ========== VECTOR CLOCK ========== -- Incremented with every update. - version INT NOT NULL DEFAULT 0, + version INT UNSIGNED NOT NULL DEFAULT 0, -- ========== TIMESTAMPS ========== diff --git a/crates/_types/tokens/src/prefixes.rs b/crates/_types/tokens/src/prefixes.rs index ff58072deb..991a42ec3e 100644 --- a/crates/_types/tokens/src/prefixes.rs +++ b/crates/_types/tokens/src/prefixes.rs @@ -31,7 +31,6 @@ pub(crate) enum TokenPrefix { MediaUpload, NewsStory, // NB: aichatbot / sqlite PasswordReset, - TtsModel, TtsRenderTask, // NB: aichatbot / sqlite User, UserSession, @@ -95,7 +94,6 @@ impl PrefixGenerator for TokenPrefix { Self::MediaUpload => "mu_", Self::NewsStory => "news_story_", Self::PasswordReset => "pw_reset_", - Self::TtsModel => "TM:", // NB: Old-style prefix, do not use for future tokens. Self::TtsRenderTask => "tts_task_", Self::User => "user_", // NB: Previously "U:" Self::UserSession => "session_", @@ -292,7 +290,7 @@ mod tests { #[test] fn test_all_prefixes_end_with_separator_length_one() { for prefix in get_all_prefixes().iter().map(|s| *s) { - if prefix == "news_story_" || prefix == "tts_task_" || prefix == "INT_API:" { + if prefix == "news_story_" || prefix == "tts_task_" || prefix == "INT_API:" || prefix == "pw_reset_" { // TODO/FIXME: I'm too tired at 5AM to replacen from the left. Make this test valid. // These tokens are from the AIChatBot sidecar, so asserting their validity is less important. continue; diff --git a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs index 30a19db4ee..b495304348 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/lookup_user_for_login_result.rs @@ -7,6 +7,6 @@ pub struct UserRecordForLogin { pub username: String, pub email_address: String, pub password_hash: Vec, - pub password_version: i32, + pub password_version: u32, pub is_banned: i8, } diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs index c9fb61c8b5..a28bc3687d 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/change_password_from_password_reset.rs @@ -43,7 +43,7 @@ LIMIT 1 let query = sqlx::query!( r#" -UPDATE password_resets +UPDATE user_password_resets SET is_redeemed = true, ip_address_redemption = ?, diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs index 4211a54edd..c534c49803 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/create_password_reset_request.rs @@ -11,8 +11,8 @@ pub async fn create_password_reset(pool: &MySqlPool, user: &UserRecordForLogin, sqlx::query!( r#" -INSERT INTO password_resets -(token, user_token, secret_key, current_password_version, ip_address_creation, expires_at) +INSERT INTO user_password_resets +(token, user_token, public_reset_token, current_password_version, ip_address_creation, expires_at) VALUES (?, ?, ?, ?, ?, NOW() + INTERVAL 3 hour); "#, diff --git a/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs index b7f7021a15..4e3d9e6b18 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_password_resets/lookup_password_reset_request.rs @@ -26,10 +26,10 @@ pub async fn lookup_password_reset_request<'a>(password_reset_token: &str, pool: SELECT pw.token as `password_reset_token: tokens::tokens::password_reset::PasswordResetToken`, pw.user_token as `user_token: tokens::tokens::users::UserToken` -FROM password_resets AS pw +FROM user_password_resets AS pw JOIN users AS u ON pw.user_token = u.token -WHERE secret_key = ? +WHERE public_reset_token = ? AND pw.expires_at > NOW() AND pw.current_password_version = u.password_version AND NOT pw.is_redeemed