factoriauth/src/auth/mod.rs

229 lines
6.6 KiB
Rust

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<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<Box<dyn Database + Send>>>,
backends: Vec<AuthenticationBackend>,
}
impl UserAuthenticator {
const TOKEN_LEN: usize = 30;
pub fn new(
db: Arc<Mutex<Box<dyn Database + Send>>>,
backends: Vec<AuthenticationBackend>,
) -> 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<Sha256> = 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<UserAuthenticator>,
padlock_generator: Arc<ServerPadlockGenerator>,
}
impl UserServerKeyGenerator {
pub fn new(
user_authenticator: Arc<UserAuthenticator>,
padlock_generator: Arc<ServerPadlockGenerator>,
) -> 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<Md5> = 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))
}
}