From bd1e8be32811609fe6df452767ef2e4d542d4508 Mon Sep 17 00:00:00 2001 From: tomuta Date: Sun, 24 Nov 2019 22:28:49 -0700 Subject: [PATCH] Implement change-email, email-verification, account-recovery, and welcome notifications --- .env.template | 18 +- .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 5 + .../down.sql | 1 + .../up.sql | 5 + src/api/core/accounts.rs | 175 +++++++++++++++++- src/api/core/two_factor/email.rs | 20 +- src/api/identity.rs | 29 +++ src/auth.rs | 58 ++++++ src/config.rs | 11 ++ src/crypto.rs | 16 ++ src/db/models/device.rs | 3 +- src/db/models/user.rs | 12 +- src/db/schemas/mysql/schema.rs | 5 + src/db/schemas/postgresql/schema.rs | 7 +- src/db/schemas/sqlite/schema.rs | 5 + src/mail.rs | 81 +++++++- src/static/templates/email/change_email.hbs | 6 + .../templates/email/change_email.html.hbs | 129 +++++++++++++ src/static/templates/email/delete_account.hbs | 12 ++ .../templates/email/delete_account.html.hbs | 137 ++++++++++++++ src/static/templates/email/pw_hint_none.hbs | 8 +- .../templates/email/pw_hint_none.html.hbs | 5 + src/static/templates/email/pw_hint_some.hbs | 2 + .../templates/email/pw_hint_some.html.hbs | 5 + src/static/templates/email/verify_email.hbs | 12 ++ .../templates/email/verify_email.html.hbs | 137 ++++++++++++++ src/static/templates/email/welcome.hbs | 8 + src/static/templates/email/welcome.html.hbs | 129 +++++++++++++ .../templates/email/welcome_must_verify.hbs | 12 ++ .../email/welcome_must_verify.html.hbs | 137 ++++++++++++++ 33 files changed, 1164 insertions(+), 33 deletions(-) create mode 100644 migrations/mysql/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/mysql/2019-11-17-011009_add_email_verification/up.sql create mode 100644 migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql create mode 100644 migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql create mode 100644 migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql create mode 100644 src/static/templates/email/change_email.hbs create mode 100644 src/static/templates/email/change_email.html.hbs create mode 100644 src/static/templates/email/delete_account.hbs create mode 100644 src/static/templates/email/delete_account.html.hbs create mode 100644 src/static/templates/email/verify_email.hbs create mode 100644 src/static/templates/email/verify_email.html.hbs create mode 100644 src/static/templates/email/welcome.hbs create mode 100644 src/static/templates/email/welcome.html.hbs create mode 100644 src/static/templates/email/welcome_must_verify.hbs create mode 100644 src/static/templates/email/welcome_must_verify.html.hbs diff --git a/.env.template b/.env.template index 7c0200d0..3ccdbdbd 100644 --- a/.env.template +++ b/.env.template @@ -95,12 +95,22 @@ ## Controls if new users can register # SIGNUPS_ALLOWED=true +## Controls if new users need to verify their email address upon registration +## Note that setting this option to true prevents logins until the email address has been verified! +## The welcome email will include a verification link, and login attempts will periodically +## trigger another verification email to be sent. +# SIGNUPS_VERIFY=false + +## If SIGNUPS_VERIFY is set to true, this limits how many seconds after the last time +## an email verification link has been sent another verification email will be sent +# SIGNUPS_VERIFY_RESEND_TIME=3600 + +## If SIGNUPS_VERIFY is set to true, this limits how many times an email verification +## email will be re-sent upon an attempted login. +# SIGNUPS_VERIFY_RESEND_LIMIT=6 + ## Controls if new users from a list of comma-separated domains can register ## even if SIGNUPS_ALLOWED is set to false -## -## WARNING: There is currently no validation that prevents anyone from -## signing up with any made-up email address from one of these -## whitelisted domains! # SIGNUPS_DOMAINS_WHITELIST=example.com,example.net,example.org ## Token for the admin interface, preferably use a long random string diff --git a/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql b/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/migrations/mysql/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql b/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 00000000..01957762 --- /dev/null +++ b/migrations/mysql/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL; diff --git a/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql b/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/migrations/postgresql/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql b/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 00000000..1a1c55da --- /dev/null +++ b/migrations/postgresql/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at TIMESTAMP DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at TIMESTAMP DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new VARCHAR(255) DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token VARCHAR(16) DEFAULT NULL; diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql @@ -0,0 +1 @@ + diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql new file mode 100644 index 00000000..aa3b6753 --- /dev/null +++ b/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL; +ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL; +ALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index b91e724d..489854dc 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1,11 +1,13 @@ use rocket_contrib::json::Json; +use chrono::Utc; use crate::db::models::*; use crate::db::DbConn; use crate::api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType}; -use crate::auth::{decode_invite, Headers}; +use crate::auth::{decode_invite, decode_delete, decode_verify_email, Headers}; use crate::mail; +use crate::crypto; use crate::CONFIG; @@ -25,6 +27,10 @@ pub fn routes() -> Vec { post_sstamp, post_email_token, post_email, + post_verify_email, + post_verify_email_token, + post_delete_recover, + post_delete_recover_token, delete_account, post_delete_account, revision_date, @@ -126,6 +132,20 @@ fn register(data: JsonUpcase, conn: DbConn) -> EmptyResult { user.public_key = Some(keys.PublicKey); } + if CONFIG.mail_enabled() { + if CONFIG.signups_verify() { + if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid) { + error!("Error sending welcome email: {:#?}", e); + } + + user.last_verifying_at = Some(user.created_at); + } else { + if let Err(e) = mail::send_welcome(&user.email) { + error!("Error sending welcome email: {:#?}", e); + } + } + } + user.save(&conn) } @@ -341,8 +361,9 @@ struct EmailTokenData { #[post("/accounts/email-token", data = "")] fn post_email_token(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { let data: EmailTokenData = data.into_inner().data; + let mut user = headers.user; - if !headers.user.check_valid_password(&data.MasterPasswordHash) { + if !user.check_valid_password(&data.MasterPasswordHash) { err!("Invalid password") } @@ -350,7 +371,21 @@ fn post_email_token(data: JsonUpcase, headers: Headers, conn: Db err!("Email already in use"); } - Ok(()) + if !CONFIG.signups_allowed() && !CONFIG.can_signup_user(&data.NewEmail) { + err!("Email cannot be changed to this address"); + } + + let token = crypto::generate_token(6)?; + + if CONFIG.mail_enabled() { + if let Err(e) = mail::send_change_email(&data.NewEmail, &token) { + error!("Error sending change-email email: {:#?}", e); + } + } + + user.email_new = Some(data.NewEmail); + user.email_new_token = Some(token); + user.save(&conn) } #[derive(Deserialize)] @@ -361,8 +396,7 @@ struct ChangeEmailData { Key: String, NewMasterPasswordHash: String, - #[serde(rename = "Token")] - _Token: NumberOrString, + Token: NumberOrString, } #[post("/accounts/email", data = "")] @@ -378,7 +412,32 @@ fn post_email(data: JsonUpcase, headers: Headers, conn: DbConn) err!("Email already in use"); } + match user.email_new { + Some(ref val) => { + if *val != data.NewEmail.to_string() { + err!("Email change mismatch"); + } + }, + None => err!("No email change pending"), + } + + if CONFIG.mail_enabled() { + // Only check the token if we sent out an email... + match user.email_new_token { + Some(ref val) => + if *val != data.Token.into_string() { + err!("Token mismatch"); + } + None => err!("No email change pending"), + } + user.verified_at = Some(Utc::now().naive_utc()); + } else { + user.verified_at = None; + } + user.email = data.NewEmail; + user.email_new = None; + user.email_new_token = None; user.set_password(&data.NewMasterPasswordHash); user.akey = data.Key; @@ -386,6 +445,112 @@ fn post_email(data: JsonUpcase, headers: Headers, conn: DbConn) user.save(&conn) } +#[post("/accounts/verify-email")] +fn post_verify_email(headers: Headers, _conn: DbConn) -> EmptyResult { + let user = headers.user; + + if !CONFIG.mail_enabled() { + err!("Cannot verify email address"); + } + + if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) { + error!("Error sending delete account email: {:#?}", e); + } + + Ok(()) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct VerifyEmailTokenData { + UserId: String, + Token: String, +} + +#[post("/accounts/verify-email-token", data = "")] +fn post_verify_email_token(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: VerifyEmailTokenData = data.into_inner().data; + + let mut user = match User::find_by_uuid(&data.UserId, &conn) { + Some(user) => user, + None => err!("User doesn't exist"), + }; + + let claims = match decode_verify_email(&data.Token) { + Ok(claims) => claims, + Err(_) => err!("Invalid claim"), + }; + + if claims.sub != user.uuid { + err!("Invalid claim"); + } + + user.verified_at = Some(Utc::now().naive_utc()); + user.last_verifying_at = None; + user.login_verify_count = 0; + if let Err(e) = user.save(&conn) { + error!("Error saving email verification: {:#?}", e); + } + + Ok(()) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct DeleteRecoverData { + Email: String, +} + +#[post("/accounts/delete-recover", data="")] +fn post_delete_recover(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: DeleteRecoverData = data.into_inner().data; + + let user = User::find_by_mail(&data.Email, &conn); + + if CONFIG.mail_enabled() { + if let Some(user) = user { + if let Err(e) = mail::send_delete_account(&user.email, &user.uuid) { + error!("Error sending delete account email: {:#?}", e); + } + } + Ok(()) + } else { + // We don't support sending emails, but we shouldn't allow anybody + // to delete accounts without at least logging in... And if the user + // cannot remember their password then they will need to contact + // the administrator to delete it... + err!("Please contact the administrator to delete your account"); + } +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct DeleteRecoverTokenData { + UserId: String, + Token: String, +} + +#[post("/accounts/delete-recover-token", data="")] +fn post_delete_recover_token(data: JsonUpcase, conn: DbConn) -> EmptyResult { + let data: DeleteRecoverTokenData = data.into_inner().data; + + let user = match User::find_by_uuid(&data.UserId, &conn) { + Some(user) => user, + None => err!("User doesn't exist"), + }; + + let claims = match decode_delete(&data.Token) { + Ok(claims) => claims, + Err(_) => err!("Invalid claim"), + }; + + if claims.sub != user.uuid { + err!("Invalid claim"); + } + + user.delete(&conn) +} + #[post("/accounts/delete", data = "")] fn post_delete_account(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { delete_account(data, headers, conn) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 654d239f..0a78ca2c 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -66,7 +66,7 @@ pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult { let type_ = TwoFactorType::Email as i32; let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?; - let generated_token = generate_token(CONFIG.email_token_size())?; + let generated_token = crypto::generate_token(CONFIG.email_token_size())?; let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?; twofactor_data.set_token(generated_token); @@ -109,22 +109,6 @@ struct SendEmailData { MasterPasswordHash: String, } - -fn generate_token(token_size: u32) -> Result { - if token_size > 19 { - err!("Generating token failed") - } - - // 8 bytes to create an u64 for up to 19 token digits - let bytes = crypto::get_random(vec![0; 8]); - let mut bytes_array = [0u8; 8]; - bytes_array.copy_from_slice(&bytes); - - let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size); - let token = format!("{:0size$}", number, size = token_size as usize); - Ok(token) -} - /// Send a verification email to the specified email address to check whether it exists/belongs to user. #[post("/two-factor/send-email", data = "")] fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) -> EmptyResult { @@ -145,7 +129,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - tf.delete(&conn)?; } - let generated_token = generate_token(CONFIG.email_token_size())?; + let generated_token = crypto::generate_token(CONFIG.email_token_size())?; let twofactor_data = EmailTokenData::new(data.Email, generated_token); // Uses EmailVerificationChallenge as type to show that it's not verified yet. diff --git a/src/api/identity.rs b/src/api/identity.rs index 7a2ef137..ad475e76 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -3,6 +3,7 @@ use rocket::request::{Form, FormItems, FromForm}; use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; +use chrono::Utc; use crate::api::core::two_factor::email::EmailTokenData; use crate::api::core::two_factor::{duo, email, yubikey}; @@ -96,6 +97,34 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult ) } + if !user.verified_at.is_some() && CONFIG.mail_enabled() && CONFIG.signups_verify() { + let now = Utc::now().naive_utc(); + if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() > CONFIG.signups_verify_resend_time() as i64 { + let resend_limit = CONFIG.signups_verify_resend_limit() as i32; + if resend_limit == 0 || user.login_verify_count < resend_limit { + // We want to send another email verification if we require signups to verify + // their email address, and we haven't sent them a reminder in a while... + let mut user = user; + user.last_verifying_at = Some(now); + user.login_verify_count = user.login_verify_count + 1; + + if let Err(e) = user.save(&conn) { + error!("Error updating user: {:#?}", e); + } + + if let Err(e) = mail::send_verify_email(&user.email, &user.uuid) { + error!("Error auto-sending email verification email: {:#?}", e); + } + } + } + + // We still want the login to fail until they actually verified the email address + err!( + "Please verify your email before trying again.", + format!("IP: {}. Username: {}.", ip.ip, username) + ) + } + let (mut device, new_device) = get_device(&data, &conn, &user); let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; diff --git a/src/auth.rs b/src/auth.rs index 8a55ba54..cd19e97b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -18,6 +18,8 @@ lazy_static! { static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM); pub static ref JWT_LOGIN_ISSUER: String = format!("{}|login", CONFIG.domain()); pub static ref JWT_INVITE_ISSUER: String = format!("{}|invite", CONFIG.domain()); + pub static ref JWT_DELETE_ISSUER: String = format!("{}|delete", CONFIG.domain()); + pub static ref JWT_VERIFYEMAIL_ISSUER: String = format!("{}|verifyemail", CONFIG.domain()); pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain()); static ref PRIVATE_RSA_KEY: Vec = match read_file(&CONFIG.private_rsa_key()) { Ok(key) => key, @@ -62,6 +64,14 @@ pub fn decode_invite(token: &str) -> Result { decode_jwt(token, JWT_INVITE_ISSUER.to_string()) } +pub fn decode_delete(token: &str) -> Result { + decode_jwt(token, JWT_DELETE_ISSUER.to_string()) +} + +pub fn decode_verify_email(token: &str) -> Result { + decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string()) +} + pub fn decode_admin(token: &str) -> Result { decode_jwt(token, JWT_ADMIN_ISSUER.to_string()) } @@ -134,6 +144,54 @@ pub fn generate_invite_claims( } } +#[derive(Debug, Serialize, Deserialize)] +pub struct DeleteJWTClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, +} + +pub fn generate_delete_claims( + uuid: String, +) -> DeleteJWTClaims { + let time_now = Utc::now().naive_utc(); + DeleteJWTClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::days(5)).timestamp(), + iss: JWT_DELETE_ISSUER.to_string(), + sub: uuid, + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerifyEmailJWTClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, +} + +pub fn generate_verify_email_claims( + uuid: String, +) -> DeleteJWTClaims { + let time_now = Utc::now().naive_utc(); + DeleteJWTClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::days(5)).timestamp(), + iss: JWT_VERIFYEMAIL_ISSUER.to_string(), + sub: uuid, + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct AdminJWTClaims { // Not before diff --git a/src/config.rs b/src/config.rs index f75b9e64..4cccbb10 100644 --- a/src/config.rs +++ b/src/config.rs @@ -243,6 +243,12 @@ make_config! { disable_icon_download: bool, true, def, false; /// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited signups_allowed: bool, true, def, true; + /// Require email verification on signups. This will prevent logins from succeeding until the address has been verified + signups_verify: bool, true, def, false; + /// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds) + signups_verify_resend_time: u64, true, def, 3_600; + /// If signups require email verification, limit how many emails are automatically sent when login is attempted (0 means no limit) + signups_verify_resend_limit: u32, true, def, 6; /// Allow signups only from this list of comma-separated domains signups_domains_whitelist: String, true, def, "".to_string(); /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled @@ -595,6 +601,8 @@ fn load_templates(path: &str) -> Handlebars { } // First register default templates here + reg!("email/change_email", ".html"); + reg!("email/delete_account", ".html"); reg!("email/invite_accepted", ".html"); reg!("email/invite_confirmed", ".html"); reg!("email/new_device_logged_in", ".html"); @@ -602,6 +610,9 @@ fn load_templates(path: &str) -> Handlebars { reg!("email/pw_hint_some", ".html"); reg!("email/send_org_invite", ".html"); reg!("email/twofactor_email", ".html"); + reg!("email/verify_email", ".html"); + reg!("email/welcome", ".html"); + reg!("email/welcome_must_verify", ".html"); reg!("admin/base"); reg!("admin/login"); diff --git a/src/crypto.rs b/src/crypto.rs index d8cf4461..dd3ed31a 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -4,6 +4,7 @@ use ring::{digest, hmac, pbkdf2}; use std::num::NonZeroU32; +use crate::error::Error; static DIGEST_ALG: &digest::Algorithm = &digest::SHA256; const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN; @@ -52,6 +53,21 @@ pub fn get_random(mut array: Vec) -> Vec { array } +pub fn generate_token(token_size: u32) -> Result { + if token_size > 19 { + err!("Generating token failed") + } + + // 8 bytes to create an u64 for up to 19 token digits + let bytes = get_random(vec![0; 8]); + let mut bytes_array = [0u8; 8]; + bytes_array.copy_from_slice(&bytes); + + let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size); + let token = format!("{:0size$}", number, size = token_size as usize); + Ok(token) +} + // // Constant time compare // diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 9f8506e1..4fa91fa2 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,6 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use super::User; +use crate::CONFIG; #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "devices"] @@ -87,7 +88,7 @@ impl Device { premium: true, name: user.name.to_string(), email: user.email.to_string(), - email_verified: true, + email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(), orgowner, orgadmin, diff --git a/src/db/models/user.rs b/src/db/models/user.rs index f35739e1..9646ae58 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -11,8 +11,13 @@ pub struct User { pub uuid: String, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub verified_at: Option, + pub last_verifying_at: Option, + pub login_verify_count: i32, pub email: String, + pub email_new: Option, + pub email_new_token: Option, pub name: String, pub password_hash: Vec, @@ -56,9 +61,14 @@ impl User { uuid: crate::util::get_uuid(), created_at: now, updated_at: now, + verified_at: None, + last_verifying_at: None, + login_verify_count: 0, name: email.clone(), email, akey: String::new(), + email_new: None, + email_new_token: None, password_hash: Vec::new(), salt: crypto::get_random_64(), @@ -135,7 +145,7 @@ impl User { "Id": self.uuid, "Name": self.name, "Email": self.email, - "EmailVerified": true, + "EmailVerified": !CONFIG.mail_enabled() || self.verified_at.is_some(), "Premium": true, "MasterPasswordHint": self.password_hint, "Culture": "en-US", diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 2e705112..36165b6b 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Varchar, created_at -> Datetime, updated_at -> Datetime, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Varchar, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Blob, salt -> Blob, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 0112c59a..a683212c 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Text, created_at -> Timestamp, updated_at -> Timestamp, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Text, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Binary, salt -> Binary, @@ -170,4 +175,4 @@ allow_tables_to_appear_in_same_query!( users, users_collections, users_organizations, -); \ No newline at end of file +); diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 43abe5c4..a683212c 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -101,7 +101,12 @@ table! { uuid -> Text, created_at -> Timestamp, updated_at -> Timestamp, + verified_at -> Nullable, + last_verifying_at -> Nullable, + login_verify_count -> Integer, email -> Text, + email_new -> Nullable, + email_new_token -> Nullable, name -> Text, password_hash -> Binary, salt -> Binary, diff --git a/src/mail.rs b/src/mail.rs index 80a4f467..711ac5bb 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -8,7 +8,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use quoted_printable::encode_to_str; use crate::api::EmptyResult; -use crate::auth::{encode_jwt, generate_invite_claims}; +use crate::auth::{encode_jwt, generate_invite_claims, generate_delete_claims, generate_verify_email_claims}; use crate::error::Error; use crate::CONFIG; use chrono::NaiveDateTime; @@ -95,6 +95,73 @@ pub fn send_password_hint(address: &str, hint: Option) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } +pub fn send_delete_account(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_delete_claims( + uuid.to_string(), + ); + let delete_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/delete_account", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), + "token": delete_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_verify_email(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_verify_email_claims( + uuid.to_string(), + ); + let verify_email_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/verify_email", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(), + "token": verify_email_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_welcome(address: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/welcome", + json!({ + "url": CONFIG.domain(), + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + +pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { + let claims = generate_verify_email_claims( + uuid.to_string(), + ); + let verify_email_token = encode_jwt(&claims); + + let (subject, body_html, body_text) = get_text( + "email/welcome_must_verify", + json!({ + "url": CONFIG.domain(), + "user_id": uuid, + "token": verify_email_token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + pub fn send_invite( address: &str, uuid: &str, @@ -183,6 +250,18 @@ pub fn send_token(address: &str, token: &str) -> EmptyResult { send_email(&address, &subject, &body_html, &body_text) } +pub fn send_change_email(address: &str, token: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/change_email", + json!({ + "url": CONFIG.domain(), + "token": token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult { let html = PartBuilder::new() .body(encode_to_str(body_html)) diff --git a/src/static/templates/email/change_email.hbs b/src/static/templates/email/change_email.hbs new file mode 100644 index 00000000..30008822 --- /dev/null +++ b/src/static/templates/email/change_email.hbs @@ -0,0 +1,6 @@ +Your Email Change + + +

To finalize changing your email address enter the following code in web vault: {{token}}

+

If you did not try to change an email address, you can safely ignore this email.

+ diff --git a/src/static/templates/email/change_email.html.hbs b/src/static/templates/email/change_email.html.hbs new file mode 100644 index 00000000..afca96d6 --- /dev/null +++ b/src/static/templates/email/change_email.html.hbs @@ -0,0 +1,129 @@ +Your Email Change + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + +
+ To finalize changing your email address enter the following code in web vault: {{token}} +
+ If you did not try to change an email address, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/delete_account.hbs b/src/static/templates/email/delete_account.hbs new file mode 100644 index 00000000..22425343 --- /dev/null +++ b/src/static/templates/email/delete_account.hbs @@ -0,0 +1,12 @@ +Delete Your Account + + +

+click the link below to delete your account. +
+
+ +Delete Your Account +

+

If you did not request this email to delete your account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/delete_account.html.hbs b/src/static/templates/email/delete_account.html.hbs new file mode 100644 index 00000000..70a92cd9 --- /dev/null +++ b/src/static/templates/email/delete_account.html.hbs @@ -0,0 +1,137 @@ +Delete Your Account + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ click the link below to delete your account. +
+ + Delete Your Account + +
+ If you did not request this email to delete your account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/pw_hint_none.hbs b/src/static/templates/email/pw_hint_none.hbs index c73e3b3a..67a7a093 100644 --- a/src/static/templates/email/pw_hint_none.hbs +++ b/src/static/templates/email/pw_hint_none.hbs @@ -1,3 +1,7 @@ -Sorry, you have no password hint... +Your master password hint -Sorry, you have not specified any password hint... +You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. + +If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + +If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_none.html.hbs b/src/static/templates/email/pw_hint_none.html.hbs index 12729bf9..bf3162c8 100644 --- a/src/static/templates/email/pw_hint_none.html.hbs +++ b/src/static/templates/email/pw_hint_none.html.hbs @@ -99,6 +99,11 @@ Sorry, you have no password hint... You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint.
+ + + If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_some.hbs b/src/static/templates/email/pw_hint_some.hbs index f16dc2b1..07c3c0a7 100644 --- a/src/static/templates/email/pw_hint_some.hbs +++ b/src/static/templates/email/pw_hint_some.hbs @@ -5,4 +5,6 @@ You (or someone) recently requested your master password hint. Your hint is: "{{hint}}" Log in: Web Vault +If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/pw_hint_some.html.hbs b/src/static/templates/email/pw_hint_some.html.hbs index 0b17bdd9..f1e987aa 100644 --- a/src/static/templates/email/pw_hint_some.html.hbs +++ b/src/static/templates/email/pw_hint_some.html.hbs @@ -105,6 +105,11 @@ Your master password hint Log in: Web Vault + + + If you cannot remember your master password, there is no way to recover your data. The only option to gain access to your account again is to delete the account so that you can register again and start over. All data associated with your account will be deleted. + + If you did not request your master password hint you can safely ignore this email. diff --git a/src/static/templates/email/verify_email.hbs b/src/static/templates/email/verify_email.hbs new file mode 100644 index 00000000..06cf2c8d --- /dev/null +++ b/src/static/templates/email/verify_email.hbs @@ -0,0 +1,12 @@ +Verify Your Email + + +

+Verify this email address for your account by clicking the link below. +
+
+ +Verify Email Address Now +

+

If you did not request to verify your account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/verify_email.html.hbs b/src/static/templates/email/verify_email.html.hbs new file mode 100644 index 00000000..c950c7f2 --- /dev/null +++ b/src/static/templates/email/verify_email.html.hbs @@ -0,0 +1,137 @@ +Verify Your Email + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ Verify this email address for your account by clicking the link below. +
+ + Verify Email Address Now + +
+ If you did not request to verify your account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/welcome.hbs b/src/static/templates/email/welcome.hbs new file mode 100644 index 00000000..be4f530e --- /dev/null +++ b/src/static/templates/email/welcome.hbs @@ -0,0 +1,8 @@ +Welcome + + +

+Thank you for creating an account at {{url}}. You may now log in with your new account. +

+

If you did not request to create an account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/welcome.html.hbs b/src/static/templates/email/welcome.html.hbs new file mode 100644 index 00000000..5f782f31 --- /dev/null +++ b/src/static/templates/email/welcome.html.hbs @@ -0,0 +1,129 @@ +Welcome + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + +
+ Thank you for creating an account at {{url}}. You may now log in with your new account. +
+ If you did not request to create an account, you can safely ignore this email. +
+
+ + + + + +
+
+ + diff --git a/src/static/templates/email/welcome_must_verify.hbs b/src/static/templates/email/welcome_must_verify.hbs new file mode 100644 index 00000000..2a7f86e5 --- /dev/null +++ b/src/static/templates/email/welcome_must_verify.hbs @@ -0,0 +1,12 @@ +Welcome + + +

+Thank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below. +
+
+ +Verify Email Address Now +

+

If you did not request to create an account, you can safely ignore this email.

+ diff --git a/src/static/templates/email/welcome_must_verify.html.hbs b/src/static/templates/email/welcome_must_verify.html.hbs new file mode 100644 index 00000000..d6de9738 --- /dev/null +++ b/src/static/templates/email/welcome_must_verify.html.hbs @@ -0,0 +1,137 @@ +Welcome + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + +
+ Thank you for creating an account at {{url}}. Before you can login with your new account, you must verify this email address by clicking the link below. +
+ + Verify Email Address Now + +
+ If you did not request to create an account, you can safely ignore this email. +
+
+ + + + + +
+
+ +