From 4f26f725861107c0a16874c6a7fd5d273f083ffd Mon Sep 17 00:00:00 2001 From: Arunabha-Mukhopadhyay Date: Mon, 10 Aug 2026 14:11:21 +0530 Subject: [PATCH 1/2] Allow email-only /api/two-factor/send-email-login for mobile clients Mobile clients (iOS) may call /api/two-factor/send-email-login with only the user's email and without a MasterPasswordHash or an AuthRequest. Permit email-only requests so the server will send the email 2FA token in that flow.\n\nModified: src/api/core/two_factor/email.rs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api/core/two_factor/email.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 44ba2e7f..d38bd873 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -86,7 +86,11 @@ async fn send_email_login(data: Json, client_headers: Client err!("AuthRequest doesn't exist", "Invalid device, IP or code") } } else { - err!("No password hash has been submitted.") + // Allow email-only requests to trigger sending an email 2FA token. + // Mobile clients (e.g. iOS) may call this endpoint with only the email when + // the token endpoint indicated 2FA is required. In that flow the client + // doesn't submit the master password hash or an auth request id, so + // permit sending the email token based solely on the user's email. } user From 47fa7f52c805e0e5bd85b21396365867c3b51aff Mon Sep 17 00:00:00 2001 From: Arunabha-Mukhopadhyay Date: Mon, 17 Aug 2026 13:51:19 +0530 Subject: [PATCH 2/2] fix(email-2fa): use device-identifier fallback when no password hash submitted iOS clients call /api/two-factor/send-email-login with an email and DeviceIdentifier but without masterPasswordHash or authRequestId after receiving a 2FA-required response from the token endpoint. The previous empty else-block allowed any caller to trigger a 2FA email for any account knowing only the email address. Replace the empty block with a device-identifier-based fallback: - Look up the most-recently-active device via find_by_device_for_email2fa. - Verify the device's associated user email matches the submitted email. - Log (debug) when the fallback path is exercised for operator visibility. - Reject with the original error when no device identifier is provided or when the device maps to a different account. Fixes #7568 --- src/api/core/two_factor/email.rs | 40 ++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index d38bd873..78a80cb5 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -86,11 +86,41 @@ async fn send_email_login(data: Json, client_headers: Client err!("AuthRequest doesn't exist", "Invalid device, IP or code") } } else { - // Allow email-only requests to trigger sending an email 2FA token. - // Mobile clients (e.g. iOS) may call this endpoint with only the email when - // the token endpoint indicated 2FA is required. In that flow the client - // doesn't submit the master password hash or an auth request id, so - // permit sending the email token based solely on the user's email. + // Fallback for clients (e.g. iOS) that call this endpoint with an email but + // without a masterPasswordHash or authRequestId. This can happen when the + // client receives a 2FA-required response from the token endpoint and then + // immediately calls send-email-login without re-submitting credentials. + // + // If the client provided a device identifier, use it to look up the most + // recently active device for the account and verify it matches the submitted + // email. This preserves a meaningful security check while remaining + // compatible with these clients. + // + // If no device identifier is present either, reject the request to prevent + // unauthenticated actors from triggering emails for arbitrary accounts. + if let Some(device_identifier) = &data.device_identifier { + match User::find_by_device_for_email2fa(device_identifier, &conn).await { + Some(device_user) if device_user.email.to_lowercase() == email.to_lowercase() => { + // Device matches the requested email – allow the token to be sent. + // Log so operators can monitor how often this fallback path is used. + debug!( + "Email 2FA send-email-login: using device-identifier fallback for user '{}' \ + (device: {}). No masterPasswordHash or authRequestId was provided.", + email, device_identifier + ); + } + Some(_) => { + // Device exists but belongs to a different account – reject. + err!("Username or password is incorrect. Try again.") + } + None => { + // No device record found – cannot verify the caller. + err!("No password hash has been submitted.") + } + } + } else { + err!("No password hash has been submitted.") + } } user