mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 13:51:14 +00:00 
			
		
		
		
	automatically use email address as 2fa provider (#4317)
This commit is contained in:
		
					parent
					
						
							
								7c3cad197c
							
						
					
				
			
			
				commit
				
					
						79ce5b49bc
					
				
			
		
					 7 changed files with 90 additions and 12 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|             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"); | ||||
|  |  | |||
|  | @ -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<String>) -> EmptyResult | |||
|     } | ||||
|     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>")] | ||||
| 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 { | ||||
|             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?; | ||||
|  |  | |||
|  | @ -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"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
|  | @ -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::*; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue