diff --git a/.env.template b/.env.template index 61cd046b..0189cc7a 100644 --- a/.env.template +++ b/.env.template @@ -444,6 +444,11 @@ ## ## Maximum attempts before an email token is reset and a new email will need to be sent. # EMAIL_ATTEMPTS_LIMIT=3 +## +## Setup email 2FA regardless of any organization policy +# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false +## Automatically setup email 2FA as fallback provider when needed +# EMAIL_2FA_AUTO_FALLBACK=false ## Other MFA/2FA settings ## Disable 2FA remember diff --git a/src/api/admin.rs b/src/api/admin.rs index dfd3e28f..4c8d5604 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -510,7 +510,11 @@ async fn update_user_org_type(data: Json, token: AdminToken, mu match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot modify this user to this type because it has no two-step login method activated"); + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?; + } else { + err!("You cannot modify this user to this type because they have not setup 2FA"); + } } Err(OrgPolicyErr::SingleOrgEnforced) => { err!("You cannot modify this user to this type because it is a member of an organization which forbids it"); diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index b8a837a0..a97c4c31 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -5,8 +5,9 @@ use serde_json::Value; use crate::{ api::{ - core::log_user_event, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, - JsonUpcase, Notify, PasswordOrOtpData, UpdateType, + core::{log_user_event, two_factor::email}, + register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, JsonUpcase, Notify, + PasswordOrOtpData, UpdateType, }, auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, crypto, @@ -104,6 +105,19 @@ fn enforce_password_hint_setting(password_hint: &Option) -> EmptyResult } Ok(()) } +async fn is_email_2fa_required(org_user_uuid: Option, conn: &mut DbConn) -> bool { + if !CONFIG._enable_email_2fa() { + return false; + } + if CONFIG.email_2fa_enforce_on_verified_invite() { + return true; + } + if org_user_uuid.is_some() { + return OrgPolicy::is_enabled_by_org(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn) + .await; + } + false +} #[post("/accounts/register", data = "")] async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { @@ -208,6 +222,10 @@ pub async fn _register(data: JsonUpcase, mut conn: DbConn) -> Json } else if let Err(e) = mail::send_welcome(&user.email).await { error!("Error sending welcome email: {:#?}", e); } + + if verified_by_invite && is_email_2fa_required(data.OrganizationUserId, &mut conn).await { + let _ = email::activate_email_2fa(&user, &mut conn).await; + } } user.save(&mut conn).await?; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 7041d3cb..1d841cda 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1079,7 +1079,7 @@ async fn accept_invite( let claims = decode_invite(&data.Token)?; match User::find_by_mail(&claims.email, &mut conn).await { - Some(_) => { + Some(user) => { Invitation::take(&claims.email, &mut conn).await; if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) { @@ -1103,7 +1103,11 @@ async fn accept_invite( match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot join this organization until you enable two-step login on your user account"); + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::activate_email_2fa(&user, &mut conn).await?; + } else { + err!("You cannot join this organization until you enable two-step login on your user account"); + } } Err(OrgPolicyErr::SingleOrgEnforced) => { err!("You cannot join this organization because you are a member of an organization which forbids it"); @@ -1228,10 +1232,14 @@ async fn _confirm_invite( match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot confirm this user because it has no two-step login method activated"); + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::find_and_activate_email_2fa(&user_to_confirm.user_uuid, conn).await?; + } else { + err!("You cannot confirm this user because they have not setup 2FA"); + } } Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot confirm this user because it is a member of an organization which forbids it"); + err!("You cannot confirm this user because they are a member of an organization which forbids it"); } } } @@ -1359,10 +1367,14 @@ async fn edit_user( match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, org_id, true, &mut conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot modify this user to this type because it has no two-step login method activated"); + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?; + } else { + err!("You cannot modify this user to this type because they have not setup 2FA"); + } } Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot modify this user to this type because it is a member of an organization which forbids it"); + err!("You cannot modify this user to this type because they are a member of an organization which forbids it"); } } } @@ -2159,10 +2171,14 @@ async fn _restore_organization_user( match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await { Ok(_) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot restore this user because it has no two-step login method activated"); + if CONFIG.email_2fa_auto_fallback() { + two_factor::email::find_and_activate_email_2fa(&user_org.user_uuid, conn).await?; + } else { + err!("You cannot restore this user because they have not setup 2FA"); + } } Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot restore this user because it is a member of an organization which forbids it"); + err!("You cannot restore this user because they are a member of an organization which forbids it"); } } } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index e1ee847f..582265b1 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -10,7 +10,7 @@ use crate::{ auth::Headers, crypto, db::{ - models::{EventType, TwoFactor, TwoFactorType}, + models::{EventType, TwoFactor, TwoFactorType, User}, DbConn, }, error::{Error, MapResult}, @@ -297,6 +297,15 @@ impl EmailTokenData { } } +pub async fn activate_email_2fa(user: &User, conn: &mut DbConn) -> EmptyResult { + if user.verified_at.is_none() { + err!("Auto-enabling of email 2FA failed because the users email address has not been verified!"); + } + let twofactor_data = EmailTokenData::new(user.email.clone(), String::new()); + let twofactor = TwoFactor::new(user.uuid.clone(), TwoFactorType::Email, twofactor_data.to_json()); + twofactor.save(conn).await +} + /// Takes an email address and obscures it by replacing it with asterisks except two characters. pub fn obscure_email(email: &str) -> String { let split: Vec<&str> = email.rsplitn(2, '@').collect(); @@ -318,6 +327,14 @@ pub fn obscure_email(email: &str) -> String { format!("{}@{}", new_name, &domain) } +pub async fn find_and_activate_email_2fa(user_uuid: &str, conn: &mut DbConn) -> EmptyResult { + if let Some(user) = User::find_by_uuid(user_uuid, conn).await { + activate_email_2fa(&user, conn).await + } else { + err!("User not found!"); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/config.rs b/src/config.rs index 01f387ec..489a229d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -686,6 +686,10 @@ make_config! { email_expiration_time: u64, true, def, 600; /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent email_attempts_limit: u64, true, def, 3; + /// Automatically enforce at login |> Setup email 2FA provider regardless of any organization policy + email_2fa_enforce_on_verified_invite: bool, true, def, false; + /// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed + email_2fa_auto_fallback: bool, true, def, false; }, } @@ -888,6 +892,13 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { err!("To enable email 2FA, a mail transport must be configured") } + if !cfg._enable_email_2fa && cfg.email_2fa_enforce_on_verified_invite { + err!("To enforce email 2FA on verified invitations, email 2fa has to be enabled!"); + } + if !cfg._enable_email_2fa && cfg.email_2fa_auto_fallback { + err!("To use email 2FA as automatic fallback, email 2fa has to be enabled!"); + } + // Check if the icon blacklist regex is valid if let Some(ref r) = cfg.icon_blacklist_regex { let validate_regex = regex::Regex::new(r); diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 8b3f1271..18bbdcd3 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -340,4 +340,11 @@ impl OrgPolicy { } false } + + pub async fn is_enabled_by_org(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool { + if let Some(policy) = OrgPolicy::find_by_org_and_type(org_uuid, policy_type, conn).await { + return policy.enabled; + } + false + } }