Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a82533498c Fix LDAP bind to not support email login - only allow email login for web UI
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 20:55:24 +00:00
copilot-swe-agent[bot]
2d899d5672 Implement email login functionality - allow login with email address
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-09-04 20:36:29 +00:00
copilot-swe-agent[bot]
6032004f84 Initial plan 2025-09-04 20:11:24 +00:00
3 changed files with 90 additions and 17 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};
use lldap_domain_handlers::handler::{BindRequest, LoginHandler, UserRequestFilter, UserListerBackendHandler};
use lldap_domain_model::{
error::{DomainError, Result},
model::{self, UserColumn},
@@ -60,6 +60,26 @@ 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]
@@ -101,14 +121,33 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self,
request: login::ClientLoginStartRequest,
) -> Result<login::ServerLoginStartResponse> {
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?
// 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
.map(|bytes| {
opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| {
DomainError::InternalError(format!("Corrupted password file for {}", &user_id))
DomainError::InternalError(format!("Corrupted password file for {}", &actual_user_id))
})
})
.transpose()?;
@@ -120,11 +159,11 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self.opaque_setup,
maybe_password_file,
request.login_start_request,
&user_id,
&actual_user_id,
)?;
let secret_key = self.get_orion_secret_key()?;
let server_data = login::ServerData {
username: user_id,
username: actual_user_id,
server_login: start_response.state,
};
let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
@@ -306,6 +345,7 @@ 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"),
@@ -313,6 +353,8 @@ mod tests {
})
.await
.unwrap();
// Test login with non-existent user
handler
.bind(BindRequest {
name: UserId::new("andrew"),
@@ -320,6 +362,8 @@ mod tests {
})
.await
.unwrap_err();
// Test login with wrong password
handler
.bind(BindRequest {
name: UserId::new("bob"),
@@ -327,6 +371,39 @@ 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,8 +186,9 @@ where
Some(token) => token,
};
if let Err(e) = super::mail::send_password_reset_email(
user.display_name.as_deref(),
user.user_id.as_str(),
user.display_name
.as_deref()
.unwrap_or_else(|| user.user_id.as_str()),
user.email.as_str(),
&token,
&data.server_url,

View File

@@ -80,7 +80,6 @@ async fn send_email(
}
pub async fn send_password_reset_email(
display_name: Option<&str>,
username: &str,
to: &str,
token: &str,
@@ -93,16 +92,12 @@ 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!(
"{greeting}
"Hello {username},
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."