diff --git a/src/api/identity.rs b/src/api/identity.rs index 59e585f0..90d356ee 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -17,7 +17,7 @@ use crate::{ push::register_push_device, ApiResult, EmptyResult, JsonResult, }, - auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, + auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, ClientVersion}, db::{models::*, DbConn}, error::MapResult, mail, util, CONFIG, @@ -28,7 +28,12 @@ pub fn routes() -> Vec { } #[post("/connect/token", data = "")] -async fn login(data: Form, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult { +async fn login( + data: Form, + client_header: ClientHeaders, + client_version: Option, + mut conn: DbConn, +) -> JsonResult { let data: ConnectData = data.into_inner(); let mut user_id: Option = None; @@ -48,7 +53,7 @@ async fn login(data: Form, client_header: ClientHeaders, mut conn: _check_is_some(&data.device_name, "device_name cannot be blank")?; _check_is_some(&data.device_type, "device_type cannot be blank")?; - _password_login(data, &mut user_id, &mut conn, &client_header.ip).await + _password_login(data, &mut user_id, &mut conn, &client_header.ip, &client_version).await } "client_credentials" => { _check_is_some(&data.client_id, "client_id cannot be blank")?; @@ -144,6 +149,7 @@ async fn _password_login( user_id: &mut Option, conn: &mut DbConn, ip: &ClientIp, + client_version: &Option, ) -> JsonResult { // Validate scope let scope = data.scope.as_ref().unwrap(); @@ -262,7 +268,7 @@ async fn _password_login( let (mut device, new_device) = get_device(&data, conn, &user).await; - let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?; + let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, client_version, conn).await?; if CONFIG.mail_enabled() && new_device { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await { @@ -520,6 +526,7 @@ async fn twofactor_auth( data: &ConnectData, device: &mut Device, ip: &ClientIp, + client_version: &Option, conn: &mut DbConn, ) -> ApiResult> { let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await; @@ -538,7 +545,10 @@ async fn twofactor_auth( let twofactor_code = match data.two_factor_token { Some(ref code) => code, None => { - err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, "2FA token not provided") + err_json!( + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, + "2FA token not provided" + ) } }; @@ -585,7 +595,7 @@ async fn twofactor_auth( } _ => { err_json!( - _json_err_twofactor(&twofactor_ids, &user.uuid, data, conn).await?, + _json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?, "2FA Remember token not provided" ) } @@ -617,6 +627,7 @@ async fn _json_err_twofactor( providers: &[i32], user_id: &UserId, data: &ConnectData, + client_version: &Option, conn: &mut DbConn, ) -> ApiResult { let mut result = json!({ @@ -689,8 +700,16 @@ async fn _json_err_twofactor( err!("No twofactor email registered") }; - // Send email immediately if email is the only 2FA option - if providers.len() == 1 { + // Starting with version 2025.5.0 the client will call `/api/two-factor/send-email-login`. + let disabled_send = if let Some(cv) = client_version { + let ver_match = semver::VersionReq::parse(">=2025.5.0").unwrap(); + ver_match.matches(&cv.0) + } else { + false + }; + + // Send email immediately if email is the only 2FA option. + if providers.len() == 1 && !disabled_send { email::send_token(user_id, conn).await? }