The smokesignal.events web application
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}