Compare commits

..

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c0e23f2251 Use unwrap_or logic for greeting instead of match statement
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 23:06:18 +00:00
copilot-swe-agent[bot]
f1d0c400aa Add display_name argument for greeting in password reset emails
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:50:32 +00:00
copilot-swe-agent[bot]
c400fac481 Revert display name changes, keep username line
Reverted all changes except adding the username line as requested. The function signature is back to the original (no display_name parameter), greeting uses username like before, but the "Your username is: {username}" line is preserved for recovery purposes.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:29:51 +00:00
copilot-swe-agent[bot]
ccb389f2c2 Include both display name and username in password reset emails
As requested by @nitnelave, the password reset email now includes both the display name for personalization and the username for recovery. The email uses the display name in the greeting when available, but always shows the username explicitly for recovery purposes.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 22:11:42 +00:00
copilot-swe-agent[bot]
19799351db Implement username recovery in password reset emails
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 20:37:24 +00:00
copilot-swe-agent[bot]
ad41c04092 Initial plan 2025-09-04 20:22:29 +00:00
3 changed files with 17 additions and 90 deletions

View File

@@ -3,7 +3,7 @@ use async_trait::async_trait;
use base64::Engine;
use lldap_auth::opaque;
use lldap_domain::types::UserId;
use lldap_domain_handlers::handler::{BindRequest, LoginHandler, UserRequestFilter, UserListerBackendHandler};
use lldap_domain_handlers::handler::{BindRequest, LoginHandler};
use lldap_domain_model::{
error::{DomainError, Result},
model::{self, UserColumn},
@@ -60,26 +60,6 @@ impl SqlBackendHandler {
.await?
.and_then(|u| u.0))
}
#[instrument(skip(self), level = "debug", err)]
async fn find_user_id_by_email(&self, email: &str) -> Result<Option<UserId>> {
// Find user ID by email address
let users = self
.list_users(
Some(UserRequestFilter::Equality(UserColumn::Email, email.to_owned())),
false,
)
.await?;
if users.len() > 1 {
warn!("Multiple users found with email '{}', login ambiguous", email);
return Ok(None);
}
Ok(users.first().map(|user_and_groups| user_and_groups.user.user_id.clone()))
}
}
#[async_trait]
@@ -121,33 +101,14 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self,
request: login::ClientLoginStartRequest,
) -> Result<login::ServerLoginStartResponse> {
// First try to authenticate with the provided name as a user ID
let mut actual_user_id = request.username.clone();
let mut maybe_password_file = self
.get_password_file_for_user(request.username.clone())
.await?;
// If no user found by user ID, try to find by email for web UI login
if maybe_password_file.is_none() {
debug!(r#"User "{}" not found by user ID, trying email lookup for web login"#, &request.username);
if let Some(user_id_by_email) = self
.find_user_id_by_email(request.username.as_str())
.await?
{
debug!(r#"Found user by email: "{}""#, &user_id_by_email);
actual_user_id = user_id_by_email;
maybe_password_file = self
.get_password_file_for_user(actual_user_id.clone())
.await?;
}
}
info!(r#"OPAQUE login attempt for "{}" (input: "{}")"#, &actual_user_id, &request.username);
let maybe_password_file = maybe_password_file
let user_id = request.username;
info!(r#"OPAQUE login attempt for "{}""#, &user_id);
let maybe_password_file = self
.get_password_file_for_user(user_id.clone())
.await?
.map(|bytes| {
opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| {
DomainError::InternalError(format!("Corrupted password file for {}", &actual_user_id))
DomainError::InternalError(format!("Corrupted password file for {}", &user_id))
})
})
.transpose()?;
@@ -159,11 +120,11 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self.opaque_setup,
maybe_password_file,
request.login_start_request,
&actual_user_id,
&user_id,
)?;
let secret_key = self.get_orion_secret_key()?;
let server_data = login::ServerData {
username: actual_user_id,
username: user_id,
server_login: start_response.state,
};
let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
@@ -345,7 +306,6 @@ mod tests {
let handler = SqlOpaqueHandler::new(generate_random_private_key(), sql_pool.clone());
insert_user(&handler, "bob", "bob00").await;
// Test login with username (should work)
handler
.bind(BindRequest {
name: UserId::new("bob"),
@@ -353,8 +313,6 @@ mod tests {
})
.await
.unwrap();
// Test login with non-existent user
handler
.bind(BindRequest {
name: UserId::new("andrew"),
@@ -362,8 +320,6 @@ mod tests {
})
.await
.unwrap_err();
// Test login with wrong password
handler
.bind(BindRequest {
name: UserId::new("bob"),
@@ -371,39 +327,6 @@ mod tests {
})
.await
.unwrap_err();
// Test that email login is NOT supported for LDAP bind
handler
.bind(BindRequest {
name: UserId::new("bob@bob.bob"),
password: "bob00".to_string(),
})
.await
.unwrap_err();
}
#[tokio::test]
async fn test_opaque_login_with_email() {
let sql_pool = get_initialized_db().await;
crate::logging::init_for_tests();
let backend_handler = SqlBackendHandler::new(generate_random_private_key(), sql_pool);
insert_user(&backend_handler, "bob", "bob00").await;
// Test OPAQUE login with username (should work as before)
attempt_login(&backend_handler, "bob", "bob00").await.unwrap();
// Test OPAQUE login with email (new functionality)
attempt_login(&backend_handler, "bob@bob.bob", "bob00").await.unwrap();
// Test OPAQUE login with non-existent email
attempt_login(&backend_handler, "nonexistent@bob.bob", "bob00")
.await
.unwrap_err();
// Test OPAQUE login with wrong password using email
attempt_login(&backend_handler, "bob@bob.bob", "wrong_password")
.await
.unwrap_err();
}
#[tokio::test]

View File

@@ -186,9 +186,8 @@ where
Some(token) => token,
};
if let Err(e) = super::mail::send_password_reset_email(
user.display_name
.as_deref()
.unwrap_or_else(|| user.user_id.as_str()),
user.display_name.as_deref(),
user.user_id.as_str(),
user.email.as_str(),
&token,
&data.server_url,

View File

@@ -80,6 +80,7 @@ async fn send_email(
}
pub async fn send_password_reset_email(
display_name: Option<&str>,
username: &str,
to: &str,
token: &str,
@@ -92,12 +93,16 @@ pub async fn send_password_reset_email(
.path_segments_mut()
.unwrap()
.extend(["reset-password", "step2", token]);
let greeting = format!("Hello {},", display_name.unwrap_or(username));
let body = format!(
"Hello {username},
"{greeting}
This email has been sent to you in order to validate your identity.
If you did not initiate the process your credentials might have been
compromised. You should reset your password and contact an administrator.
Your username is: {username}
To reset your password please visit the following URL: {reset_url}
Please contact an administrator if you did not initiate the process."