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,10 @@ edition = "2024"
default-run = "api"
[features]
default = ["sqlite"]
sqlite = [
"infra/sqlite",
"tower-sessions-sqlx-store/sqlite",
]
postgres = [
"infra/postgres",
"tower-sessions-sqlx-store/postgres",
]
default = ["sqlite", "auth-axum-login"]
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
auth-axum-login = ["infra/auth-axum-login"]
[dependencies]
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
@@ -21,12 +16,11 @@ k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features
"db-sqlx",
"sqlite",
"http",
"auth","sessions-db"
"auth",
"sessions-db",
] }
domain = { path = "../domain" }
infra = { path = "../infra", default-features = false, features = [
"sqlite",
] }
infra = { path = "../infra", default-features = false, features = ["sqlite"] }
#Web framework
axum = { version = "0.8.8", features = ["macros"] }
@@ -34,10 +28,9 @@ tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["cors", "trace"] }
# Authentication
axum-login = "0.18"
tower-sessions = "0.14"
# Moved to infra
tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] }
password-auth = "1.0"
# password-auth removed
time = "0.3"
async-trait = "0.1.89"
@@ -65,4 +58,3 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
dotenvy = "0.15.7"
config = "0.15.19"

View File

@@ -1,101 +1,23 @@
//! Authentication logic using axum-login
//! Authentication logic
//!
//! Proxies to infra implementation if enabled.
use std::sync::Arc;
use axum_login::{AuthnBackend, UserId};
use infra::session_store::InfraSessionStore;
use password_auth::verify_password;
use serde::{Deserialize, Serialize};
use tower_sessions::SessionManagerLayer;
use uuid::Uuid;
use domain::UserRepository;
use infra::session_store::{InfraSessionStore, SessionManagerLayer};
use crate::error::ApiError;
use domain::{User, UserRepository};
/// 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,
}
impl AuthnBackend for AuthBackend {
type User = AuthUser;
type Credentials = Credentials;
type Error = ApiError;
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| ApiError::internal(e.to_string()))?;
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| ApiError::internal(e.to_string()))?;
Ok(user.map(AuthUser))
}
}
pub type AuthSession = axum_login::AuthSession<AuthBackend>;
#[cfg(feature = "auth-axum-login")]
pub use infra::auth::backend::{AuthManagerLayer, AuthSession, AuthUser, Credentials};
#[cfg(feature = "auth-axum-login")]
pub async fn setup_auth_layer(
session_layer: SessionManagerLayer<InfraSessionStore>,
user_repo: Arc<dyn UserRepository>,
) -> Result<axum_login::AuthManagerLayer<AuthBackend, InfraSessionStore>, ApiError> {
let backend = AuthBackend::new(user_repo);
let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build();
Ok(auth_layer)
) -> Result<AuthManagerLayer, ApiError> {
infra::auth::backend::setup_auth_layer(session_layer, user_repo)
.await
.map_err(|e| ApiError::Internal(e.to_string()))
}

View File

@@ -17,6 +17,27 @@ pub struct Config {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_secure_cookie")]
pub secure_cookie: bool,
#[serde(default = "default_db_max_connections")]
pub db_max_connections: u32,
#[serde(default = "default_db_min_connections")]
pub db_min_connections: u32,
}
fn default_secure_cookie() -> bool {
false
}
fn default_db_max_connections() -> u32 {
5
}
fn default_db_min_connections() -> u32 {
1
}
fn default_port() -> u16 {
@@ -62,12 +83,30 @@ impl Config {
.filter(|s| !s.is_empty())
.collect();
let secure_cookie = env::var("SECURE_COOKIE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(false);
let db_max_connections = env::var("DB_MAX_CONNECTIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(5);
let db_min_connections = env::var("DB_MIN_CONNECTIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1);
Self {
host,
port,
database_url,
session_secret,
cors_allowed_origins,
secure_cookie,
db_max_connections,
db_min_connections,
}
}
}

View File

@@ -6,12 +6,12 @@ use domain::UserService;
use infra::factory::build_session_store;
use infra::factory::build_user_repository;
use infra::run_migrations;
use infra::session_store::{Expiry, SessionManagerLayer};
use k_core::http::server::ServerConfig;
use k_core::http::server::apply_standard_middleware;
use k_core::logging;
use time::Duration;
use tokio::net::TcpListener;
use tower_sessions::{Expiry, SessionManagerLayer};
use tracing::info;
mod auth;
@@ -37,8 +37,8 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Connecting to database: {}", config.database_url);
let db_config = k_core::db::DatabaseConfig {
url: config.database_url.clone(),
max_connections: 5,
min_connections: 1,
max_connections: config.db_max_connections,
min_connections: config.db_min_connections,
acquire_timeout: StdDuration::from_secs(30),
};
@@ -60,7 +60,7 @@ async fn main() -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!(e))?;
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false) // Set to true in prod
.with_secure(config.secure_cookie)
.with_expiry(Expiry::OnInactivity(Duration::days(7)));
let auth_layer = setup_auth_layer(session_layer, user_repo).await?;

View File

@@ -31,10 +31,10 @@ async fn login(
password: payload.password,
})
.await
.map_err(|e| ApiError::Internal(e.to_string()))?
{
Ok(Some(user)) => user,
Ok(None) => return Err(ApiError::Validation("Invalid credentials".to_string())),
Err(_) => return Err(ApiError::Internal("Authentication failed".to_string())),
Some(user) => user,
None => return Err(ApiError::Validation("Invalid credentials".to_string())),
};
auth_session