Skip to content

Commit

Permalink
automatically use email address as 2fa provider
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan0xC committed Feb 4, 2024
1 parent 897bdf8 commit 958bc76
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,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 provider regardless of any organization policy
# EMAIL_2FA_AUTO_ENFORCE=false
## Automatically setup email 2FA as provider where possible
# EMAIL_2FA_AUTO_FALLBACK=false

## Other MFA/2FA settings
## Disable 2FA remember
Expand Down
25 changes: 23 additions & 2 deletions src/api/core/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -128,6 +129,7 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
enforce_password_hint_setting(&password_hint)?;

let mut verified_by_invite = false;
let mut enable_email_2fa = false;

let mut user = match User::find_by_mail(&email, &mut conn).await {
Some(mut user) => {
Expand All @@ -141,6 +143,13 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
// Verify the email address when signing up via a valid invite token
verified_by_invite = true;
user.verified_at = Some(Utc::now().naive_utc());
enable_email_2fa = if CONFIG.email_2fa_auto_enforce() || CONFIG.auto_fallback_email_2fa() {
true
} else if CONFIG._enable_email_2fa() && CONFIG.mail_enabled() && data.OrganizationUserId.is_some() {
check_2fa_required(data.OrganizationUserId.unwrap(), &mut conn).await
} else {
false
};
user
} else {
err!("Registration email does not match invite email")
Expand Down Expand Up @@ -211,6 +220,9 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
}

user.save(&mut conn).await?;
if enable_email_2fa {
let _ = email::activate_email_2fa(&user, &mut conn).await;
}
Ok(Json(json!({
"Object": "register",
"CaptchaBypassToken": "",
Expand Down Expand Up @@ -1214,3 +1226,12 @@ pub async fn purge_auth_requests(pool: DbPool) {
error!("Failed to get DB connection while purging trashed ciphers")
}
}

pub async fn check_2fa_required(org_uuid: String, conn: &mut DbConn) -> bool {
if let Some(two_factor_required) =
OrgPolicy::find_by_org_and_type(&org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await
{
return two_factor_required.enabled;
}
false
}
38 changes: 33 additions & 5 deletions src/api/core/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,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) {
Expand All @@ -1095,7 +1095,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.auto_fallback_email_2fa() {
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");
Expand Down Expand Up @@ -1220,7 +1224,15 @@ 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.auto_fallback_email_2fa() {
if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await {
two_factor::email::activate_email_2fa(&user, conn).await?;
} else {
err!("User not found!");
}
} else {
err!("You cannot confirm this user because it has no two-step login method activated");
}
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot confirm this user because it is a member of an organization which forbids it");
Expand Down Expand Up @@ -1351,7 +1363,15 @@ 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.auto_fallback_email_2fa() {
if let Some(user) = User::find_by_uuid(&user_to_edit.user_uuid, &mut conn).await {
two_factor::email::activate_email_2fa(&user, &mut conn).await?;
} else {
err!("User not found!");
}
} else {
err!("You cannot modify this user to this type because it has no two-step login method activated");
}
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot modify this user to this type because it is a member of an organization which forbids it");
Expand Down Expand Up @@ -2151,7 +2171,15 @@ 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.auto_fallback_email_2fa() {
if let Some(user) = User::find_by_uuid(&user_org.user_uuid, conn).await {
two_factor::email::activate_email_2fa(&user, conn).await?;
} else {
err!("User not found");
}
} else {
err!("You cannot restore this user because it has no two-step login method activated");
}
}
Err(OrgPolicyErr::SingleOrgEnforced) => {
err!("You cannot restore this user because it is a member of an organization which forbids it");
Expand Down
11 changes: 10 additions & 1 deletion src/api/core/two_factor/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
auth::Headers,
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType},
models::{EventType, TwoFactor, TwoFactorType, User},
DbConn,
},
error::{Error, MapResult},
Expand Down Expand Up @@ -297,6 +297,15 @@ impl EmailTokenData {
}
}

pub async fn activate_email_2fa(user: &User, conn: &mut DbConn) -> EmptyResult {
if user.verified_at.is_none() {
return Ok(());
}
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();
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,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_auto_enforce: bool, true, def, false;
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA provider where possible
email_2fa_auto_fallback: bool, true, def, false;
},
}

Expand Down Expand Up @@ -1196,6 +1200,9 @@ impl Config {
let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
}
pub fn auto_fallback_email_2fa(&self) -> bool {
CONFIG.email_2fa_auto_fallback() && CONFIG._enable_email_2fa() && CONFIG.mail_enabled()
}

pub fn get_duo_akey(&self) -> String {
if let Some(akey) = self._duo_akey() {
Expand Down

0 comments on commit 958bc76

Please sign in to comment.