The smokesignal.events web application
at main 153 lines 5.2 kB view raw
1use anyhow::Result; 2use axum::{ 3 extract::{FromRef, FromRequestParts}, 4 http::request::Parts, 5 response::Response, 6}; 7use axum_extra::extract::PrivateCookieJar; 8use tracing::{debug, trace}; 9 10use crate::{ 11 config::Config, http::context::WebContext, http::cookies::SessionCookie, 12 storage::identity_profile::model::IdentityProfile, 13}; 14 15use super::errors::middleware_errors::MiddlewareAuthError; 16 17/// Cookie name for session storage. 18/// Updated version to force re-authentication when cookie format changes. 19pub(crate) const AUTH_COOKIE_NAME: &str = "session20260122"; 20 21#[derive(Clone)] 22pub(crate) enum Auth { 23 Unauthenticated, 24 Authenticated { 25 profile: IdentityProfile, 26 session: SessionCookie, 27 }, 28} 29 30impl Auth { 31 /// Get the profile if authenticated, None otherwise 32 pub(crate) fn profile(&self) -> Option<&IdentityProfile> { 33 match self { 34 Auth::Authenticated { profile, .. } => Some(profile), 35 Auth::Unauthenticated => None, 36 } 37 } 38 39 /// Get the session if authenticated, None otherwise 40 pub(crate) fn session(&self) -> Option<&SessionCookie> { 41 match self { 42 Auth::Authenticated { session, .. } => Some(session), 43 Auth::Unauthenticated => None, 44 } 45 } 46 47 /// Requires authentication and redirects to login with a signed token containing the original destination 48 /// 49 /// This creates a redirect URL with a signed token containing the destination, 50 /// which the login handler can verify and redirect back to after successful authentication. 51 pub(crate) fn require(&self, location: &str) -> Result<IdentityProfile, MiddlewareAuthError> { 52 match self { 53 Auth::Authenticated { profile, .. } => { 54 trace!(did = %profile.did, "User authenticated"); 55 return Ok(profile.clone()); 56 } 57 Auth::Unauthenticated => {} 58 } 59 60 Err(MiddlewareAuthError::AccessDenied(location.to_string())) 61 } 62 63 /// Simpler authentication check that just redirects to root path 64 /// 65 /// Use this when you don't need to return to the original page after login 66 pub(crate) fn require_flat(&self) -> Result<IdentityProfile, MiddlewareAuthError> { 67 match self { 68 Auth::Authenticated { profile, .. } => { 69 trace!(did = %profile.did, "User authenticated"); 70 return Ok(profile.clone()); 71 } 72 Auth::Unauthenticated => {} 73 } 74 75 debug!("Authentication required, redirecting to root"); 76 Err(MiddlewareAuthError::AccessDenied("/".to_string())) 77 } 78 79 /// Requires admin authentication 80 /// 81 /// Returns NotFound error instead of redirecting to login for security reasons 82 pub(crate) fn require_admin( 83 &self, 84 config: &Config, 85 ) -> Result<IdentityProfile, MiddlewareAuthError> { 86 match self { 87 Auth::Authenticated { profile, .. } => { 88 if config.is_admin(&profile.did) { 89 debug!(did = %profile.did, "Admin authenticated"); 90 return Ok(profile.clone()); 91 } 92 debug!(did = %profile.did, "User not an admin"); 93 } 94 Auth::Unauthenticated => { 95 debug!("No authentication found for admin check"); 96 } 97 } 98 99 // Return NotFound instead of redirect for security reasons 100 Err(MiddlewareAuthError::NotFound) 101 } 102} 103 104impl<S> FromRequestParts<S> for Auth 105where 106 S: Send + Sync, 107 WebContext: FromRef<S>, 108{ 109 type Rejection = Response; 110 111 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> { 112 trace!("Extracting Auth from request"); 113 let web_context = WebContext::from_ref(context); 114 115 let cookie_jar = PrivateCookieJar::from_headers( 116 &parts.headers, 117 web_context.config.http_cookie_key.as_ref().clone(), 118 ); 119 120 // Try to get session from cookie 121 let session = cookie_jar 122 .get(AUTH_COOKIE_NAME) 123 .map(|user_cookie| user_cookie.value().to_owned()) 124 .and_then(|inner_value| SessionCookie::try_from(inner_value).ok()); 125 126 if let Some(session_cookie) = session { 127 trace!(did = %session_cookie.did, "Found session cookie"); 128 129 // Look up the user's profile from the database 130 match crate::storage::identity_profile::handle_for_did( 131 &web_context.pool, 132 &session_cookie.did, 133 ) 134 .await 135 { 136 Ok(profile) => { 137 debug!(did = %session_cookie.did, "Session validated"); 138 return Ok(Auth::Authenticated { 139 profile, 140 session: session_cookie, 141 }); 142 } 143 Err(err) => { 144 debug!(did = %session_cookie.did, ?err, "Failed to look up profile"); 145 return Ok(Auth::Unauthenticated); 146 } 147 }; 148 } 149 150 trace!("No session cookie found"); 151 Ok(Auth::Unauthenticated) 152 } 153}