use std::sync::Arc; use base64::{prelude::BASE64_STANDARD, Engine}; use hmac::{Hmac, Mac}; use md5::Md5; use rand::{ distributions::{Alphanumeric, DistString}, thread_rng, }; use secrecy::ExposeSecret; use sha2::Sha256; use thiserror::Error; use time::{macros::format_description, OffsetDateTime}; use tokio::sync::Mutex; use tracing::{event, instrument, Level}; use crate::{ config::AuthBackendConfig, db::{Database, 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")] InvalidUserOrPassword, #[error("Invalid token")] InvalidToken, #[error("Invalid server hash")] 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>>, backends: Vec, ) -> Self { Self { db, backends } } /// Attempt to verify credentials against all backends and return the user's most recent token if they match. /// /// If a valid token already exists, return it; if not, create a new one. #[instrument] pub async fn get_user_token( &self, username: &str, password: &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 mut db = self.db.lock().await; let token = if let Some(UserTokenEntry::Valid(old_token, _, _)) = db.get_user_token(&username).await? { old_token } else { let new_token = UserToken::from(Alphanumeric.sample_string(&mut thread_rng(), Self::TOKEN_LEN)); db.save_user_token(&username, &new_token).await?; new_token }; Ok((username, token)) } /// Check if the passed token matches the user's currently active one. /// /// Additionally, update the token's `last_used` value if verification is successful. #[instrument] pub async fn verify_user_token( &self, username: &str, token: &UserToken, ) -> Result<(), AuthenticationError> { let mut db = self.db.lock().await; if let Some(UserTokenEntry::Valid(user_token, ..)) = &db.get_user_token(username).await? { if token == user_token { db.update_user_token_last_used(username, token).await?; return Ok(()); } } Err(AuthenticationError::InvalidToken) } } #[derive(Debug)] pub struct ServerPadlockGenerator { secret: PadlockGenerationSecret, } impl ServerPadlockGenerator { const HASH_LEN: usize = 32; pub fn new(secret: PadlockGenerationSecret) -> Self { Self { secret } } #[instrument] pub fn generate_hash() -> ServerHash { ServerHash(Alphanumeric.sample_string(&mut thread_rng(), Self::HASH_LEN)) } #[instrument] pub fn generate_padlock(&self, server_hash: &ServerHash) -> ServerPadlock { #[allow(clippy::expect_used)] let mut hmac: Hmac = Hmac::new_from_slice(self.secret.0.expose_secret()) .expect("HMAC should accept key of any length"); hmac.update(server_hash.0.as_bytes()); BASE64_STANDARD.encode(hmac.finalize().into_bytes()).into() } } #[derive(Debug)] pub struct UserServerKeyGenerator { user_authenticator: Arc, padlock_generator: Arc, } impl UserServerKeyGenerator { pub fn new( user_authenticator: Arc, padlock_generator: Arc, ) -> Self { Self { user_authenticator, padlock_generator, } } #[instrument] pub async fn generate_user_server_key( &self, username: &str, token: &UserToken, server_hash: &ServerHash, ) -> Result<(UserServerKey, String), AuthenticationError> { self.user_authenticator .verify_user_token(username, token) .await?; let padlock = self.padlock_generator.generate_padlock(server_hash); #[allow(clippy::expect_used)] let timestamp = OffsetDateTime::now_utc() .format(format_description!( "[year repr:last_two][month][day][hour repr:24][minute][second]" )) .expect("timestamp format should be validated at compile-time"); event!(Level::DEBUG, timestamp, "Generating user_server_key"); let mut mac: Hmac = Hmac::new_from_slice(padlock.0.expose_secret().as_bytes()) .map_err(|_e| AuthenticationError::InvalidServerHash)?; mac.update(format!("{}_{}_{}", username, padlock.0.expose_secret(), timestamp).as_bytes()); let user_server_key = UserServerKey(BASE64_STANDARD.encode(mac.finalize().into_bytes()).into()); Ok((user_server_key, timestamp)) } }