use anyhow::Result; use axum::{ extract::{FromRef, FromRequestParts}, http::request::Parts, response::Response, }; use axum_extra::extract::PrivateCookieJar; use tracing::{debug, trace}; use crate::{ config::Config, http::context::WebContext, http::cookies::SessionCookie, storage::identity_profile::model::IdentityProfile, }; use super::errors::middleware_errors::MiddlewareAuthError; /// Cookie name for session storage. /// Updated version to force re-authentication when cookie format changes. pub(crate) const AUTH_COOKIE_NAME: &str = "session20260122"; #[derive(Clone)] pub(crate) enum Auth { Unauthenticated, Authenticated { profile: IdentityProfile, session: SessionCookie, }, } impl Auth { /// Get the profile if authenticated, None otherwise pub(crate) fn profile(&self) -> Option<&IdentityProfile> { match self { Auth::Authenticated { profile, .. } => Some(profile), Auth::Unauthenticated => None, } } /// Get the session if authenticated, None otherwise pub(crate) fn session(&self) -> Option<&SessionCookie> { match self { Auth::Authenticated { session, .. } => Some(session), Auth::Unauthenticated => None, } } /// Requires authentication and redirects to login with a signed token containing the original destination /// /// This creates a redirect URL with a signed token containing the destination, /// which the login handler can verify and redirect back to after successful authentication. pub(crate) fn require(&self, location: &str) -> Result { match self { Auth::Authenticated { profile, .. } => { trace!(did = %profile.did, "User authenticated"); return Ok(profile.clone()); } Auth::Unauthenticated => {} } Err(MiddlewareAuthError::AccessDenied(location.to_string())) } /// Simpler authentication check that just redirects to root path /// /// Use this when you don't need to return to the original page after login pub(crate) fn require_flat(&self) -> Result { match self { Auth::Authenticated { profile, .. } => { trace!(did = %profile.did, "User authenticated"); return Ok(profile.clone()); } Auth::Unauthenticated => {} } debug!("Authentication required, redirecting to root"); Err(MiddlewareAuthError::AccessDenied("/".to_string())) } /// Requires admin authentication /// /// Returns NotFound error instead of redirecting to login for security reasons pub(crate) fn require_admin( &self, config: &Config, ) -> Result { match self { Auth::Authenticated { profile, .. } => { if config.is_admin(&profile.did) { debug!(did = %profile.did, "Admin authenticated"); return Ok(profile.clone()); } debug!(did = %profile.did, "User not an admin"); } Auth::Unauthenticated => { debug!("No authentication found for admin check"); } } // Return NotFound instead of redirect for security reasons Err(MiddlewareAuthError::NotFound) } } impl FromRequestParts for Auth where S: Send + Sync, WebContext: FromRef, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, context: &S) -> Result { trace!("Extracting Auth from request"); let web_context = WebContext::from_ref(context); let cookie_jar = PrivateCookieJar::from_headers( &parts.headers, web_context.config.http_cookie_key.as_ref().clone(), ); // Try to get session from cookie let session = cookie_jar .get(AUTH_COOKIE_NAME) .map(|user_cookie| user_cookie.value().to_owned()) .and_then(|inner_value| SessionCookie::try_from(inner_value).ok()); if let Some(session_cookie) = session { trace!(did = %session_cookie.did, "Found session cookie"); // Look up the user's profile from the database match crate::storage::identity_profile::handle_for_did( &web_context.pool, &session_cookie.did, ) .await { Ok(profile) => { debug!(did = %session_cookie.did, "Session validated"); return Ok(Auth::Authenticated { profile, session: session_cookie, }); } Err(err) => { debug!(did = %session_cookie.did, ?err, "Failed to look up profile"); return Ok(Auth::Unauthenticated); } }; } trace!("No session cookie found"); Ok(Auth::Unauthenticated) } }