From f446d67cf451c6c326d274f9057f135fcb3e0abb Mon Sep 17 00:00:00 2001 From: Xiretza Date: Sat, 10 Feb 2024 21:44:43 +0000 Subject: [PATCH] Implement LDAP authentication backend --- src/auth.rs | 64 ++++++++++++++-- src/auth/backends.rs | 176 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 3 + src/main.rs | 11 ++- src/server.rs | 2 +- 5 files changed, 247 insertions(+), 9 deletions(-) create mode 100644 src/auth/backends.rs diff --git a/src/auth.rs b/src/auth.rs index 870e19a..5c78083 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -15,12 +15,17 @@ use tokio::sync::Mutex; use tracing::{event, instrument, Level}; use crate::{ + config::AuthBackendConfig, db::{/* Database, */ Database, SqliteDatabase, UserTokenEntry}, secrets::{ PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken, }, }; +use self::backends::{LdapBackend, ValidateLogin}; + +pub mod backends; + #[derive(Debug, Error)] pub enum AuthenticationError { #[error("Invalid username or password")] @@ -31,18 +36,48 @@ pub enum AuthenticationError { InvalidServerHash, #[error("Database error")] Database(#[from] sqlx::Error), + #[error("Authentication backend error")] + Backend(#[from] ldap3::LdapError), + #[error("No authentication backends available")] + NoBackends, +} + +#[derive(Debug)] +pub enum AuthenticationBackend { + Ldap(Arc), +} + +impl AuthenticationBackend { + pub async fn new(config: AuthBackendConfig) -> Result { + match config { + AuthBackendConfig::Ldap(c) => Ok(Self::Ldap(LdapBackend::new(c).await?)), + } + } +} + +impl ValidateLogin for AuthenticationBackend { + async fn validate_login( + &self, + username: &str, + password: &Password, + ) -> Result { + match self { + AuthenticationBackend::Ldap(b) => b.validate_login(username, password).await, + } + } } #[derive(Debug)] pub struct UserAuthenticator { db: Arc>, + backends: Vec, } impl UserAuthenticator { const TOKEN_LEN: usize = 30; - pub fn new(db: Arc>) -> Self { - Self { db } + pub fn new(db: Arc>, backends: Vec) -> Self { + Self { db, backends } } #[instrument] @@ -50,16 +85,33 @@ impl UserAuthenticator { &self, username: &str, password: &Password, - ) -> Result { - // TODO: validate password + ) -> Result<(String, UserToken), AuthenticationError> { + let mut auth_result = Err(AuthenticationError::NoBackends); + + for backend in &self.backends { + auth_result = backend.validate_login(username, password).await; + match &auth_result { + Ok(_) => { + break; + } + Err(err) => { + event!(Level::WARN, ?err, "Authentication with backend failed"); + } + } + } + + let Ok(username) = auth_result else { + event!(Level::WARN, "Authentication failed with all backends"); + return Err(AuthenticationError::InvalidUserOrPassword); + }; let new_token = UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN)); let mut db = self.db.lock().await; - db.save_token(username, &new_token).await?; + db.save_token(&username, &new_token).await?; - Ok(new_token) + Ok((username, new_token)) } #[instrument] diff --git a/src/auth/backends.rs b/src/auth/backends.rs new file mode 100644 index 0000000..4c1b3b6 --- /dev/null +++ b/src/auth/backends.rs @@ -0,0 +1,176 @@ +use std::{cmp::min, convert::Infallible, sync::Arc, time::Duration}; + +use ldap3::{drive, ldap_escape, Ldap, LdapConnAsync, LdapError, Scope, SearchEntry}; +use secrecy::ExposeSecret; +use tokio::{sync::Mutex, time::sleep}; +use tracing::{event, instrument, Level}; + +use crate::{config::LdapBackendConfig, secrets::Password}; + +use super::AuthenticationError; + +pub trait ValidateLogin { + /// Validates that the given username and password combination is correct, and returns the + /// (normalized) user ID. + async fn validate_login( + &self, + username: &str, + password: &Password, + ) -> Result; +} + +#[instrument] +async fn start_ldap_connection( + config: &LdapBackendConfig, +) -> Result<(LdapConnAsync, Ldap), LdapError> { + event!(Level::INFO, "Opening LDAP connection"); + + LdapConnAsync::new(&config.server_address).await +} + +#[derive(Debug)] +pub struct LdapBackend { + ldap: Arc>, + config: LdapBackendConfig, +} + +impl LdapBackend { + #[instrument] + pub async fn new(config: LdapBackendConfig) -> Result, LdapError> { + let (conn, ldap) = start_ldap_connection(&config).await?; + let ldap = Arc::new(Mutex::new(ldap)); + + let this = Arc::new(Self { ldap, config }); + + { + let this = Arc::clone(&this); + tokio::spawn(async move { this.drive_and_restart_connection(conn).await }); + } + + Ok(this) + } + + #[instrument(skip(conn))] + async fn drive_and_restart_connection(&self, conn: LdapConnAsync) -> Infallible { + const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(60); + + let mut conn = conn; + + loop { + let ret = conn.drive().await; + match ret { + Ok(()) => event!( + Level::ERROR, + "LDAP connection exited without specific error" + ), + Err(err) => event!(Level::ERROR, ?err, "LDAP connection exited"), + } + + // try to get a new connection + let mut delay = Duration::from_secs(1); + let (new_conn, ldap) = loop { + match start_ldap_connection(&self.config).await { + Ok(ret) => { + event!(Level::INFO, "Successfully reconnected"); + break ret; + } + Err(err) => { + event!( + Level::WARN, + ?err, + "Reconnecting to LDAP failed, reconnecting in {}s", + delay.as_secs() + ); + sleep(delay).await; + delay = min(delay * 2, MAX_RECONNECT_DELAY); + } + } + }; + + conn = new_conn; + *self.ldap.lock().await = ldap; + } + } +} + +impl ValidateLogin for LdapBackend { + #[instrument] + async fn validate_login( + &self, + username: &str, + password: &Password, + ) -> Result { + let filter = self + .config + .user_filter + .replace("%s", &ldap_escape(username)); + + event!(Level::DEBUG, filter, "Validating LDAP user"); + + let search_results = self + .ldap + .lock() + .await + .search( + &self.config.search_base, + Scope::Subtree, + &filter, + ["dn", "uid"], + ) + .await? + .success()? + .0; + + event!(Level::TRACE, ?search_results, "Got raw search results"); + + let search_entry = match search_results.len() { + 1 => SearchEntry::construct(search_results.into_iter().next().unwrap()), + 0 => { + event!(Level::WARN, "No matching LDAP user found"); + return Err(AuthenticationError::InvalidUserOrPassword); + } + _ => { + event!(Level::ERROR, "Multiple LDAP users matched"); + return Err(AuthenticationError::InvalidUserOrPassword); + } + }; + + let uid = { + let uids = search_entry.attrs.get("uid").ok_or_else(|| { + event!( + Level::ERROR, + "LDAP entry doesn't contain a uid attr, rejecting" + ); + AuthenticationError::InvalidUserOrPassword + })?; + + if uids.len() != 1 { + event!( + Level::ERROR, + ?uids, + "LDAP entry contains multiple uid attrs" + ); + return Err(AuthenticationError::InvalidUserOrPassword); + } + + uids[0].clone() + }; + + event!( + Level::TRACE, + dn = search_entry.dn, + uid, + "Found LDAP user, attempting to bind" + ); + + // try to bind as the found user + { + let (conn, mut ldap) = start_ldap_connection(&self.config).await?; + drive!(conn); + ldap.simple_bind(&search_entry.dn, password.0.expose_secret()) + .await?; + } + + Ok(uid) + } +} diff --git a/src/config.rs b/src/config.rs index 086bec9..2ae370d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,4 +31,7 @@ pub enum AuthBackendConfig { #[serde(rename_all = "kebab-case")] pub struct LdapBackendConfig { pub server_address: String, + pub search_base: String, + /// User filter template. All occurences of `%s` will be replaced with the username. + pub user_filter: String, } diff --git a/src/main.rs b/src/main.rs index c84cb0e..517f82c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,9 @@ mod server; use std::sync::Arc; -use auth::{ServerPadlockGenerator, UserAuthenticator, UserServerKeyGenerator}; +use auth::{ + AuthenticationBackend, ServerPadlockGenerator, UserAuthenticator, UserServerKeyGenerator, +}; use clap::Parser; use color_eyre::Result; use config::Config; @@ -84,7 +86,12 @@ async fn main() -> Result<()> { SqliteDatabase::open(&config.database.connection_string).await, )); - let user_authenticator = Arc::new(UserAuthenticator::new(database)); + let mut auth_backends = vec![]; + for c in config.auth_backends { + auth_backends.push(AuthenticationBackend::new(c).await?); + } + + let user_authenticator = Arc::new(UserAuthenticator::new(database, auth_backends)); let padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock_secret)); let user_server_key_generator = Arc::new(UserServerKeyGenerator::new( Arc::clone(&user_authenticator), diff --git a/src/server.rs b/src/server.rs index 5379686..b650c57 100644 --- a/src/server.rs +++ b/src/server.rs @@ -109,7 +109,7 @@ async fn api_login( ) -> ApiResult> { event!(Level::INFO, "Generating user key"); - let user_token = state + let (username, user_token) = state .user_authenticator .create_user_token(&username, &password) .await?;