automatically use email address as 2fa provider (#4317)

This commit is contained in:
Stefan Melmuk 2024-03-17 22:35:02 +01:00 committed by GitHub
parent 7c3cad197c
commit 79ce5b49bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 90 additions and 12 deletions

View file

@ -444,6 +444,11 @@
## ##
## Maximum attempts before an email token is reset and a new email will need to be sent. ## Maximum attempts before an email token is reset and a new email will need to be sent.
# EMAIL_ATTEMPTS_LIMIT=3 # 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 ## Other MFA/2FA settings
## Disable 2FA remember ## Disable 2FA remember

View file

@ -510,7 +510,11 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await { match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
Ok(_) => {} Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => { 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(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 it is a member of an organization which forbids it");

View file

@ -5,8 +5,9 @@ use serde_json::Value;
use crate::{ use crate::{
api::{ api::{
core::log_user_event, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, core::{log_user_event, two_factor::email},
JsonUpcase, Notify, PasswordOrOtpData, UpdateType, register_push_device, unregister_push_device, AnonymousNotify, EmptyResult, JsonResult, JsonUpcase, Notify,
PasswordOrOtpData, UpdateType,
}, },
auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers},
crypto, crypto,
@ -104,6 +105,19 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
} }
Ok(()) Ok(())
} }
async fn is_email_2fa_required(org_user_uuid: Option<String>, 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 = "<data>")] #[post("/accounts/register", data = "<data>")]
async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult { async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
@ -208,6 +222,10 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
} else if let Err(e) = mail::send_welcome(&user.email).await { } else if let Err(e) = mail::send_welcome(&user.email).await {
error!("Error sending welcome email: {:#?}", e); 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?; user.save(&mut conn).await?;

View file

@ -1079,7 +1079,7 @@ async fn accept_invite(
let claims = decode_invite(&data.Token)?; let claims = decode_invite(&data.Token)?;
match User::find_by_mail(&claims.email, &mut conn).await { match User::find_by_mail(&claims.email, &mut conn).await {
Some(_) => { Some(user) => {
Invitation::take(&claims.email, &mut conn).await; Invitation::take(&claims.email, &mut conn).await;
if let (Some(user_org), Some(org)) = (&claims.user_org_id, &claims.org_id) { 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 { match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, &mut conn).await {
Ok(_) => {} Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => { 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(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot join this organization because you are a member of an organization which forbids it"); 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 { match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await {
Ok(_) => {} Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => { 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(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 { match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, org_id, true, &mut conn).await {
Ok(_) => {} Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => { 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(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 { match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await {
Ok(_) => {} Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => { 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(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");
} }
} }
} }

View file

@ -10,7 +10,7 @@ use crate::{
auth::Headers, auth::Headers,
crypto, crypto,
db::{ db::{
models::{EventType, TwoFactor, TwoFactorType}, models::{EventType, TwoFactor, TwoFactorType, User},
DbConn, DbConn,
}, },
error::{Error, MapResult}, 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. /// Takes an email address and obscures it by replacing it with asterisks except two characters.
pub fn obscure_email(email: &str) -> String { pub fn obscure_email(email: &str) -> String {
let split: Vec<&str> = email.rsplitn(2, '@').collect(); let split: Vec<&str> = email.rsplitn(2, '@').collect();
@ -318,6 +327,14 @@ pub fn obscure_email(email: &str) -> String {
format!("{}@{}", new_name, &domain) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -686,6 +686,10 @@ make_config! {
email_expiration_time: u64, true, def, 600; 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 /// 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; 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") 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 // Check if the icon blacklist regex is valid
if let Some(ref r) = cfg.icon_blacklist_regex { if let Some(ref r) = cfg.icon_blacklist_regex {
let validate_regex = regex::Regex::new(r); let validate_regex = regex::Regex::new(r);

View file

@ -340,4 +340,11 @@ impl OrgPolicy {
} }
false 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
}
} }