Implement LDAP authentication backend
This commit is contained in:
parent
8dd283d773
commit
f446d67cf4
5 changed files with 247 additions and 9 deletions
64
src/auth.rs
64
src/auth.rs
|
@ -15,12 +15,17 @@ use tokio::sync::Mutex;
|
||||||
use tracing::{event, instrument, Level};
|
use tracing::{event, instrument, Level};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::AuthBackendConfig,
|
||||||
db::{/* Database, */ Database, SqliteDatabase, UserTokenEntry},
|
db::{/* Database, */ Database, SqliteDatabase, UserTokenEntry},
|
||||||
secrets::{
|
secrets::{
|
||||||
PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken,
|
PadlockGenerationSecret, Password, ServerHash, ServerPadlock, UserServerKey, UserToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::backends::{LdapBackend, ValidateLogin};
|
||||||
|
|
||||||
|
pub mod backends;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum AuthenticationError {
|
pub enum AuthenticationError {
|
||||||
#[error("Invalid username or password")]
|
#[error("Invalid username or password")]
|
||||||
|
@ -31,18 +36,48 @@ pub enum AuthenticationError {
|
||||||
InvalidServerHash,
|
InvalidServerHash,
|
||||||
#[error("Database error")]
|
#[error("Database error")]
|
||||||
Database(#[from] sqlx::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)]
|
#[derive(Debug)]
|
||||||
pub struct UserAuthenticator {
|
pub struct UserAuthenticator {
|
||||||
db: Arc<Mutex<SqliteDatabase>>,
|
db: Arc<Mutex<SqliteDatabase>>,
|
||||||
|
backends: Vec<AuthenticationBackend>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserAuthenticator {
|
impl UserAuthenticator {
|
||||||
const TOKEN_LEN: usize = 30;
|
const TOKEN_LEN: usize = 30;
|
||||||
|
|
||||||
pub fn new(db: Arc<Mutex<SqliteDatabase>>) -> Self {
|
pub fn new(db: Arc<Mutex<SqliteDatabase>>, backends: Vec<AuthenticationBackend>) -> Self {
|
||||||
Self { db }
|
Self { db, backends }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
@ -50,16 +85,33 @@ impl UserAuthenticator {
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &Password,
|
password: &Password,
|
||||||
) -> Result<UserToken, AuthenticationError> {
|
) -> Result<(String, UserToken), AuthenticationError> {
|
||||||
// TODO: validate password
|
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 =
|
let new_token =
|
||||||
UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN));
|
UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN));
|
||||||
|
|
||||||
let mut db = self.db.lock().await;
|
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]
|
#[instrument]
|
||||||
|
|
176
src/auth/backends.rs
Normal file
176
src/auth/backends.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,4 +31,7 @@ pub enum AuthBackendConfig {
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct LdapBackendConfig {
|
pub struct LdapBackendConfig {
|
||||||
pub server_address: String,
|
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,
|
||||||
}
|
}
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -27,7 +27,9 @@ mod server;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use auth::{ServerPadlockGenerator, UserAuthenticator, UserServerKeyGenerator};
|
use auth::{
|
||||||
|
AuthenticationBackend, ServerPadlockGenerator, UserAuthenticator, UserServerKeyGenerator,
|
||||||
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
@ -84,7 +86,12 @@ async fn main() -> Result<()> {
|
||||||
SqliteDatabase::open(&config.database.connection_string).await,
|
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 padlock_generator = Arc::new(ServerPadlockGenerator::new(config.padlock_secret));
|
||||||
let user_server_key_generator = Arc::new(UserServerKeyGenerator::new(
|
let user_server_key_generator = Arc::new(UserServerKeyGenerator::new(
|
||||||
Arc::clone(&user_authenticator),
|
Arc::clone(&user_authenticator),
|
||||||
|
|
|
@ -109,7 +109,7 @@ async fn api_login(
|
||||||
) -> ApiResult<Json<LoginResponse>> {
|
) -> ApiResult<Json<LoginResponse>> {
|
||||||
event!(Level::INFO, "Generating user key");
|
event!(Level::INFO, "Generating user key");
|
||||||
|
|
||||||
let user_token = state
|
let (username, user_token) = state
|
||||||
.user_authenticator
|
.user_authenticator
|
||||||
.create_user_token(&username, &password)
|
.create_user_token(&username, &password)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
Loading…
Reference in a new issue