Implement LDAP authentication backend

This commit is contained in:
Xiretza 2024-02-10 21:44:43 +00:00
parent 8dd283d773
commit f446d67cf4
5 changed files with 247 additions and 9 deletions

View file

@ -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<LdapBackend>),
}
impl AuthenticationBackend {
pub async fn new(config: AuthBackendConfig) -> Result<Self, AuthenticationError> {
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<String, AuthenticationError> {
match self {
AuthenticationBackend::Ldap(b) => b.validate_login(username, password).await,
}
}
}
#[derive(Debug)]
pub struct UserAuthenticator {
db: Arc<Mutex<SqliteDatabase>>,
backends: Vec<AuthenticationBackend>,
}
impl UserAuthenticator {
const TOKEN_LEN: usize = 30;
pub fn new(db: Arc<Mutex<SqliteDatabase>>) -> Self {
Self { db }
pub fn new(db: Arc<Mutex<SqliteDatabase>>, backends: Vec<AuthenticationBackend>) -> Self {
Self { db, backends }
}
#[instrument]
@ -50,16 +85,33 @@ impl UserAuthenticator {
&self,
username: &str,
password: &Password,
) -> Result<UserToken, AuthenticationError> {
// 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]

176
src/auth/backends.rs Normal file
View file

@ -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<String, AuthenticationError>;
}
#[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<Mutex<Ldap>>,
config: LdapBackendConfig,
}
impl LdapBackend {
#[instrument]
pub async fn new(config: LdapBackendConfig) -> Result<Arc<Self>, 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<String, AuthenticationError> {
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)
}
}

View file

@ -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,
}

View file

@ -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),

View file

@ -109,7 +109,7 @@ async fn api_login(
) -> ApiResult<Json<LoginResponse>> {
event!(Level::INFO, "Generating user key");
let user_token = state
let (username, user_token) = state
.user_authenticator
.create_user_token(&username, &password)
.await?;