From b953411dd51bac05aba8821c1037bde0039b7e47 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Wed, 26 Nov 2025 01:35:02 +0100 Subject: [PATCH] fix email as 2fa for sso --- src/api/core/two_factor/email.rs | 45 ++++++++++++++++++++------------ src/api/identity.rs | 7 +++++ src/db/models/user.rs | 16 ++++++++++-- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index cc6909af..930611c7 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, User, UserId}, + models::{DeviceId, EventType, TwoFactor, TwoFactorType, User, UserId}, DbConn, }, error::{Error, MapResult}, @@ -24,10 +24,12 @@ pub fn routes() -> Vec { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SendEmailLoginData { + #[serde(alias = "DeviceIdentifier")] + device_identifier: DeviceId, #[serde(alias = "Email")] - email: String, + email: Option, #[serde(alias = "MasterPasswordHash")] - master_password_hash: String, + master_password_hash: Option, } /// User is trying to login and wants to use email 2FA. @@ -36,25 +38,36 @@ struct SendEmailLoginData { async fn send_email_login(data: Json, conn: DbConn) -> EmptyResult { let data: SendEmailLoginData = data.into_inner(); - use crate::db::models::User; - - // Get the user - let Some(user) = User::find_by_mail(&data.email, &conn).await else { - err!("Username or password is incorrect. Try again.") - }; - if !CONFIG._enable_email_2fa() { err!("Email 2FA is disabled") } - // Check password - if !user.check_valid_password(&data.master_password_hash) { - err!("Username or password is incorrect. Try again.") - } + // Get the user + let user = if let Some(email) = &data.email { + let Some(master_password_hash) = &data.master_password_hash else { + err!("No password hash has been submitted.") + }; - send_token(&user.uuid, &conn).await?; + let Some(user) = User::find_by_mail(email, &conn).await else { + err!("Username or password is incorrect. Try again.") + }; - Ok(()) + // Check password + if !user.check_valid_password(master_password_hash) { + err!("Username or password is incorrect. Try again.") + } + + user + } else { + // SSO login only sends device id, so we get the user by the most recently used device + let Some(user) = User::find_by_device(&data.device_identifier, &conn).await else { + err!("Username or password is incorrect. Try again.") + }; + + user + }; + + send_token(&user.uuid, &conn).await } /// Generate the token, save the data for later verification and send email to user diff --git a/src/api/identity.rs b/src/api/identity.rs index 92b6c1e4..6c1c8bb1 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -267,6 +267,10 @@ async fn _sso_login( } Some((mut user, sso_user)) => { let mut device = get_device(&data, conn, &user).await?; + + // Save to update `device.updated_at` to track usage and toggle new status + device.save(conn).await?; + let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; if user.private_key.is_none() { @@ -431,6 +435,9 @@ async fn _password_login( let mut device = get_device(&data, conn, &user).await?; + // Save to update `device.updated_at` to track usage and toggle new status + device.save(conn).await?; + let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index c7f4e1bc..d2de7a21 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,4 +1,4 @@ -use crate::db::schema::{invitations, sso_users, users}; +use crate::db::schema::{devices, invitations, sso_users, users}; use chrono::{NaiveDateTime, TimeDelta, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; @@ -10,7 +10,7 @@ use super::{ use crate::{ api::EmptyResult, crypto, - db::DbConn, + db::{models::DeviceId, DbConn}, error::MapResult, sso::OIDCIdentifier, util::{format_date, get_uuid, retry}, @@ -386,6 +386,18 @@ impl User { }} } + pub async fn find_by_device(device_uuid: &DeviceId, conn: &DbConn) -> Option { + db_run! { conn: { + users::table + .inner_join(devices::table.on(devices::user_uuid.eq(users::uuid))) + .filter(devices::uuid.eq(device_uuid)) + .select(users::all_columns) + .order_by(devices::updated_at.desc()) + .first::(conn) + .ok() + }} + } + pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option)> { db_run! { conn: { users::table