//! Authentication middleware — session-lock based auth for Axum. //! //! Validates `X-Session-Id` header against the `SessionRepository` before //! forwarding the request to the inner service. use std::future::Future; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use axum::body::Body; use axum::http::{Request, Response, StatusCode}; use axum::response::IntoResponse; use serde::{Deserialize, Serialize}; use tower::{Layer, Service}; use zesdex_domain::auth::{SessionId, SessionRepository}; /// Identity extracted from a validated session. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionIdentity { pub session_id: String, pub user_agent: String, pub connected_at: i64, } impl SessionIdentity { pub fn new(session_id: String, user_agent: String) -> Self { let connected_at = chrono::Utc::now().timestamp(); Self { session_id, user_agent, connected_at, } } } /// Tower Layer that produces SessionAuthMiddleware services. /// /// Holds a reference to the `SessionRepository` and the base directory /// needed to validate session IDs. #[derive(Debug, Clone)] pub struct SessionAuthLayer { base_dir: PathBuf, repo: Arc, } impl SessionAuthLayer { pub fn new(base_dir: PathBuf, repo: Arc) -> Self { Self { base_dir, repo } } } impl Layer for SessionAuthLayer where R: SessionRepository + Send + Sync + 'static, { type Service = SessionAuthMiddleware; fn layer(&self, inner: S) -> Self::Service { SessionAuthMiddleware { inner, base_dir: self.base_dir.clone(), repo: self.repo.clone(), } } } /// Tower Service that validates X-Session-Id before forwarding. #[derive(Debug, Clone)] pub struct SessionAuthMiddleware { inner: S, base_dir: PathBuf, repo: Arc, } impl Service> for SessionAuthMiddleware where S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, ReqBody: Send + 'static, R: SessionRepository + Send + Sync + 'static, { type Response = S::Response; type Error = S::Error; type Future = Pin> + Send + 'static>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { let session_id = req .headers() .get("X-Session-Id") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); // Validate the session against the repository. match session_id { Some(sid) => match SessionId::new(&sid) { Ok(id) => match self.repo.load_session(&self.base_dir, &id) { Ok(_session) => { // Session is valid — forward the request. let fut = self.inner.call(req); return Box::pin(fut); } Err(_) => { /* fall through to 401 */ } }, Err(_) => { /* fall through to 401 */ } }, None => { /* fall through to 401 */ } } Box::pin(async move { Ok((StatusCode::UNAUTHORIZED, "missing or invalid X-Session-Id header").into_response()) }) } }