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 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
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")]
|
||||
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,
|
||||
}
|
||||
|
|
11
src/main.rs
11
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),
|
||||
|
|
|
@ -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?;
|
||||
|
|
Loading…
Reference in a new issue