feat: Centralize axum-login authentication logic in infra and introduce configurable database and session settings.

This commit is contained in:
2026-01-02 23:02:50 +01:00
parent 650baf0d0f
commit 0fe682c737
10 changed files with 212 additions and 122 deletions

View File

@@ -5,15 +5,26 @@ edition = "2024"
[features]
default = ["sqlite", "broker-nats"]
sqlite = ["sqlx/sqlite", "k-core/sqlite", "tower-sessions-sqlx-store", "k-core/sessions-db"]
postgres = ["sqlx/postgres", "k-core/postgres", "tower-sessions-sqlx-store", "k-core/sessions-db"]
sqlite = [
"sqlx/sqlite",
"k-core/sqlite",
"tower-sessions-sqlx-store",
"k-core/sessions-db",
]
postgres = [
"sqlx/postgres",
"k-core/postgres",
"tower-sessions-sqlx-store",
"k-core/sessions-db",
]
broker-nats = ["dep:futures-util", "k-core/broker-nats"]
auth-axum-login = ["dep:axum-login", "dep:password-auth"]
[dependencies]
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
"logging",
"logging",
"db-sqlx",
"sessions-db"
"sessions-db",
] }
domain = { path = "../domain" }
@@ -21,12 +32,18 @@ async-trait = "0.1.89"
chrono = { version = "0.4.42", features = ["serde"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "chrono", "migrate"] }
thiserror = "2.0.17"
anyhow = "1.0"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1"
uuid = { version = "1.19.0", features = ["v4", "serde"] }
tower-sessions-sqlx-store = { version = "0.15.0", optional = true}
tower-sessions-sqlx-store = { version = "0.15.0", optional = true }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
async-nats = { version = "0.45", optional = true }
futures-util = { version = "0.3", optional = true }
futures-core = "0.3"
tower-sessions = "0.14"
# Auth dependencies (optional)
axum-login = { version = "0.18", optional = true }
password-auth = { version = "1.0", optional = true }

117
infra/src/auth/mod.rs Normal file
View File

@@ -0,0 +1,117 @@
//! Authentication infrastructure
//!
//! This module contains the concrete implementation of authentication mechanisms.
#[cfg(feature = "auth-axum-login")]
pub mod backend {
use std::sync::Arc;
use axum_login::{AuthnBackend, UserId};
use password_auth::verify_password;
use serde::{Deserialize, Serialize};
use tower_sessions::SessionManagerLayer;
use uuid::Uuid;
use domain::{User, UserRepository};
// We use the same session store as defined in infra
use crate::session_store::InfraSessionStore;
/// Wrapper around domain User to implement AuthUser
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthUser(pub User);
impl axum_login::AuthUser for AuthUser {
type Id = Uuid;
fn id(&self) -> Self::Id {
self.0.id
}
fn session_auth_hash(&self) -> &[u8] {
// Use password hash to invalidate sessions if password changes
self.0
.password_hash
.as_ref()
.map(|s| s.as_bytes())
.unwrap_or(&[])
}
}
#[derive(Clone)]
pub struct AuthBackend {
pub user_repo: Arc<dyn UserRepository>,
}
impl AuthBackend {
pub fn new(user_repo: Arc<dyn UserRepository>) -> Self {
Self { user_repo }
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Credentials {
pub email: String,
pub password: String,
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}
impl AuthnBackend for AuthBackend {
type User = AuthUser;
type Credentials = Credentials;
type Error = AuthError;
async fn authenticate(
&self,
creds: Self::Credentials,
) -> Result<Option<Self::User>, Self::Error> {
let user = self
.user_repo
.find_by_email(&creds.email)
.await
.map_err(|e| AuthError::Anyhow(anyhow::anyhow!(e)))?;
if let Some(user) = user {
if let Some(hash) = &user.password_hash {
// Verify password
if verify_password(&creds.password, hash).is_ok() {
return Ok(Some(AuthUser(user)));
}
}
}
Ok(None)
}
async fn get_user(
&self,
user_id: &UserId<Self>,
) -> Result<Option<Self::User>, Self::Error> {
let user = self
.user_repo
.find_by_id(*user_id)
.await
.map_err(|e| AuthError::Anyhow(anyhow::anyhow!(e)))?;
Ok(user.map(AuthUser))
}
}
pub type AuthSession = axum_login::AuthSession<AuthBackend>;
pub type AuthManagerLayer = axum_login::AuthManagerLayer<AuthBackend, InfraSessionStore>;
pub async fn setup_auth_layer(
session_layer: SessionManagerLayer<InfraSessionStore>,
user_repo: Arc<dyn UserRepository>,
) -> Result<AuthManagerLayer, AuthError> {
let backend = AuthBackend::new(user_repo);
let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build();
Ok(auth_layer)
}
}

View File

@@ -14,6 +14,7 @@
//! - [`db::create_pool`] - Create a database connection pool
//! - [`db::run_migrations`] - Run database migrations
pub mod auth;
pub mod db;
pub mod factory;
pub mod session_store;

View File

@@ -1 +1,2 @@
pub use k_core::session::store::InfraSessionStore;
pub use tower_sessions::{Expiry, SessionManagerLayer};