diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 536564d4..b9c47507 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1409,7 +1409,7 @@ async fn put_device_token(device_id: DeviceId, data: Json, headers: H } device.push_token = Some(token); - if let Err(e) = device.save(&conn).await { + if let Err(e) = device.save(true, &conn).await { err!(format!("An error occurred while trying to save the device push token: {e}")); } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index cc6909af..b8724cf1 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,40 @@ struct SendEmailLoginData { async fn send_email_login(data: Json, conn: DbConn) -> EmptyResult { let data: SendEmailLoginData = data.into_inner(); - use crate::db::models::User; + if !CONFIG._enable_email_2fa() { + err!("Email 2FA is disabled") + } // Get the user - let Some(user) = User::find_by_mail(&data.email, &conn).await else { - err!("Username or password is incorrect. Try again.") + let email = match &data.email { + Some(email) if !email.is_empty() => Some(email), + _ => None, }; + let user = if let Some(email) = email { + let Some(master_password_hash) = &data.master_password_hash else { + err!("No password hash has been submitted.") + }; - if !CONFIG._enable_email_2fa() { - err!("Email 2FA is disabled") - } + let Some(user) = User::find_by_mail(email, &conn).await else { + err!("Username or password is incorrect. Try again.") + }; - // Check password - if !user.check_valid_password(&data.master_password_hash) { - err!("Username or password is incorrect. Try again.") - } + // 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_for_email2fa(&data.device_identifier, &conn).await else { + err!("Username or password is incorrect. Try again.") + }; - send_token(&user.uuid, &conn).await?; + user + }; - Ok(()) + 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..5f5ba327 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1,4 +1,4 @@ -use chrono::{NaiveDateTime, Utc}; +use chrono::Utc; use num_traits::FromPrimitive; use rocket::{ form::{Form, FromForm}, @@ -147,7 +147,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn, ip: &ClientIp) -> Json } Ok((mut device, auth_tokens)) => { // Save to update `device.updated_at` to track usage and toggle new status - device.save(conn).await?; + device.save(true, conn).await?; let result = json!({ "refresh_token": auth_tokens.refresh_token(), @@ -267,6 +267,7 @@ async fn _sso_login( } Some((mut user, sso_user)) => { let mut device = get_device(&data, conn, &user).await?; + let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; if user.private_key.is_none() { @@ -313,7 +314,7 @@ async fn _sso_login( auth_user.expires_in, )?; - authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await + authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await } async fn _password_login( @@ -435,7 +436,7 @@ async fn _password_login( let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); - authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await + authenticated_response(&user, &mut device, auth_tokens, twofactor_token, conn, ip).await } async fn authenticated_response( @@ -443,12 +444,12 @@ async fn authenticated_response( device: &mut Device, auth_tokens: auth::AuthTokens, twofactor_token: Option, - now: &NaiveDateTime, conn: &DbConn, ip: &ClientIp, ) -> JsonResult { if CONFIG.mail_enabled() && device.is_new() { - if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), now, device).await { + let now = Utc::now().naive_utc(); + if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, device).await { error!("Error sending new device email: {e:#?}"); if CONFIG.require_device_email() { @@ -468,7 +469,7 @@ async fn authenticated_response( } // Save to update `device.updated_at` to track usage and toggle new status - device.save(conn).await?; + device.save(true, conn).await?; let master_password_policy = master_password_policy(user, conn).await; @@ -585,7 +586,7 @@ async fn _user_api_key_login( let access_claims = auth::LoginJwtClaims::default(&device, &user, &AuthMethod::UserApiKey, data.client_id); // Save to update `device.updated_at` to track usage and toggle new status - device.save(conn).await?; + device.save(true, conn).await?; info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); @@ -648,7 +649,12 @@ async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> ApiResult // Find device or create new match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await { Some(device) => Ok(device), - None => Device::new(device_id, user.uuid.clone(), device_name, device_type, conn).await, + None => { + let mut device = Device::new(device_id, user.uuid.clone(), device_name, device_type); + // save device without updating `device.updated_at` + device.save(false, conn).await?; + Ok(device) + } } } diff --git a/src/api/push.rs b/src/api/push.rs index 4394e7d2..a7e88455 100644 --- a/src/api/push.rs +++ b/src/api/push.rs @@ -128,7 +128,7 @@ pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyRe err!(format!("An error occurred while proceeding registration of a device: {e}")); } - if let Err(e) = device.save(conn).await { + if let Err(e) = device.save(true, conn).await { err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}")); } diff --git a/src/auth.rs b/src/auth.rs index e10de615..6360aaf6 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1223,7 +1223,7 @@ pub async fn refresh_tokens( }; // Save to update `updated_at`. - device.save(conn).await?; + device.save(true, conn).await?; let user = match User::find_by_uuid(&device.user_uuid, conn).await { None => err!("Impossible to find user"), diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 0d86870f..4e3d0197 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -35,6 +35,25 @@ pub struct Device { /// Local methods impl Device { + pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self { + let now = Utc::now().naive_utc(); + + Self { + uuid, + created_at: now, + updated_at: now, + + user_uuid, + name, + atype, + + push_uuid: Some(PushId(get_uuid())), + push_token: None, + refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL), + twofactor_remember: None, + } + } + pub fn to_json(&self) -> Value { json!({ "id": self.uuid, @@ -110,38 +129,21 @@ impl DeviceWithAuthRequest { } use crate::db::DbConn; -use crate::api::{ApiResult, EmptyResult}; +use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Device { - pub async fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32, conn: &DbConn) -> ApiResult { - let now = Utc::now().naive_utc(); - - let device = Self { - uuid, - created_at: now, - updated_at: now, - - user_uuid, - name, - atype, - - push_uuid: Some(PushId(get_uuid())), - push_token: None, - refresh_token: crypto::encode_random_bytes::<64>(&BASE64URL), - twofactor_remember: None, - }; - - device.inner_save(conn).await.map(|()| device) - } + pub async fn save(&mut self, update_time: bool, conn: &DbConn) -> EmptyResult { + if update_time { + self.updated_at = Utc::now().naive_utc(); + } - async fn inner_save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: sqlite, mysql { crate::util::retry(|| diesel::replace_into(devices::table) - .values(self) + .values(&*self) .execute(conn), 10, ).map_res("Error saving device") @@ -149,10 +151,10 @@ impl Device { postgresql { crate::util::retry(|| diesel::insert_into(devices::table) - .values(self) + .values(&*self) .on_conflict((devices::uuid, devices::user_uuid)) .do_update() - .set(self) + .set(&*self) .execute(conn), 10, ).map_res("Error saving device") @@ -160,12 +162,6 @@ impl Device { } } - // Should only be called after user has passed authentication - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { - self.updated_at = Utc::now().naive_utc(); - self.inner_save(conn).await - } - pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid))) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index c7f4e1bc..c96e0fe7 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::{invitations, sso_users, twofactor_incomplete, 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,20 @@ impl User { }} } + pub async fn find_by_device_for_email2fa(device_uuid: &DeviceId, conn: &DbConn) -> Option { + if let Some(user_uuid) = db_run! ( conn: { + twofactor_incomplete::table + .filter(twofactor_incomplete::device_uuid.eq(device_uuid)) + .order_by(twofactor_incomplete::login_time.desc()) + .select(twofactor_incomplete::user_uuid) + .first::(conn) + .ok() + }) { + return Self::find_by_uuid(&user_uuid, conn).await; + } + None + } + pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option)> { db_run! { conn: { users::table