forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
1use anyhow::Result;
2use axum::{
3 extract::{FromRef, FromRequestParts},
4 http::request::Parts,
5 response::Response,
6};
7use axum_extra::extract::PrivateCookieJar;
8use base64::{engine::general_purpose, Engine as _};
9use p256::{
10 ecdsa::{signature::Signer, Signature, SigningKey},
11 SecretKey,
12};
13use serde::{Deserialize, Serialize};
14use tracing::{debug, instrument, trace};
15
16use crate::{
17 config::Config,
18 encoding::ToBase64,
19 http::context::WebContext,
20 http::errors::{AuthMiddlewareError, WebSessionError},
21 storage::handle::model::Handle,
22 storage::oauth::model::OAuthSession,
23 storage::oauth::web_session_lookup,
24};
25
26use super::errors::middleware_errors::MiddlewareAuthError;
27
28pub const AUTH_COOKIE_NAME: &str = "session1";
29
30#[derive(Clone, PartialEq, Serialize, Deserialize)]
31pub struct WebSession {
32 pub did: String,
33 pub session_group: String,
34}
35
36impl TryFrom<String> for WebSession {
37 type Error = anyhow::Error;
38
39 fn try_from(value: String) -> Result<Self, Self::Error> {
40 serde_json::from_str(&value)
41 .map_err(WebSessionError::DeserializeFailed)
42 .map_err(Into::into)
43 }
44}
45
46impl TryInto<String> for WebSession {
47 type Error = anyhow::Error;
48
49 fn try_into(self) -> Result<String, Self::Error> {
50 serde_json::to_string(&self)
51 .map_err(WebSessionError::SerializeFailed)
52 .map_err(Into::into)
53 }
54}
55
56#[derive(Clone, Serialize, Deserialize)]
57pub struct DestinationClaims {
58 #[serde(rename = "d")]
59 pub destination: String,
60
61 #[serde(rename = "n")]
62 pub nonce: String,
63}
64
65#[derive(Clone)]
66pub struct Auth(pub Option<Handle>, pub Option<OAuthSession>);
67
68impl Auth {
69 /// Requires authentication and redirects to login with a signed token containing the original destination
70 ///
71 /// This creates a redirect URL with a signed token containing the destination,
72 /// which the login handler can verify and redirect back to after successful authentication.
73 #[instrument(level = "debug", skip(self, secret_key), err)]
74 pub fn require(
75 &self,
76 secret_key: &SecretKey,
77 location: &str,
78 ) -> Result<Handle, MiddlewareAuthError> {
79 if let Some(handle) = &self.0 {
80 trace!(did = %handle.did, "User authenticated");
81 return Ok(handle.clone());
82 }
83
84 debug!(
85 location,
86 "Authentication required, creating signed redirect"
87 );
88
89 // Create claims with destination and random nonce
90 let claims = DestinationClaims {
91 destination: location.to_string(),
92 nonce: ulid::Ulid::new().to_string(),
93 };
94
95 // Encode claims to base64
96 let claims = claims.to_base64()?;
97 let claim_content = claims.to_string();
98 let encoded_json_bytes = general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes());
99
100 // Sign the encoded claims
101 let signing_key = SigningKey::from(secret_key);
102 let signature: Signature = signing_key
103 .try_sign(encoded_json_bytes.as_bytes())
104 .map_err(AuthMiddlewareError::SigningFailed)?;
105
106 // Format the final destination with claims and signature
107 let destination = format!(
108 "{}.{}",
109 claim_content,
110 general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes())
111 );
112
113 trace!(
114 destination_length = destination.len(),
115 "Created signed destination token"
116 );
117 Err(MiddlewareAuthError::AccessDenied(destination))
118 }
119
120 /// Simpler authentication check that just redirects to root path
121 ///
122 /// Use this when you don't need to return to the original page after login
123 #[instrument(level = "debug", skip(self), err)]
124 pub fn require_flat(&self) -> Result<Handle, MiddlewareAuthError> {
125 if let Some(handle) = &self.0 {
126 trace!(did = %handle.did, "User authenticated");
127 return Ok(handle.clone());
128 }
129
130 debug!("Authentication required, redirecting to root");
131 Err(MiddlewareAuthError::AccessDenied("/".to_string()))
132 }
133
134 /// Requires admin authentication
135 ///
136 /// Returns NotFound error instead of redirecting to login for security reasons
137 #[instrument(level = "debug", skip(self, config), err)]
138 pub fn require_admin(&self, config: &Config) -> Result<Handle, MiddlewareAuthError> {
139 if let Some(handle) = &self.0 {
140 if config.is_admin(&handle.did) {
141 debug!(did = %handle.did, "Admin authenticated");
142 return Ok(handle.clone());
143 }
144 debug!(did = %handle.did, "User not an admin");
145 } else {
146 debug!("No authentication found for admin check");
147 }
148
149 // Return NotFound instead of redirect for security reasons
150 Err(MiddlewareAuthError::NotFound)
151 }
152}
153
154impl<S> FromRequestParts<S> for Auth
155where
156 S: Send + Sync,
157 WebContext: FromRef<S>,
158{
159 type Rejection = Response;
160
161 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> {
162 trace!("Extracting Auth from request");
163 let web_context = WebContext::from_ref(context);
164
165 let cookie_jar = PrivateCookieJar::from_headers(
166 &parts.headers,
167 web_context.config.http_cookie_key.as_ref().clone(),
168 );
169
170 let session = cookie_jar
171 .get(AUTH_COOKIE_NAME)
172 .map(|user_cookie| user_cookie.value().to_owned())
173 .and_then(|inner_value| WebSession::try_from(inner_value).ok());
174
175 if let Some(web_session) = session {
176 trace!(?web_session.session_group, "Found session cookie");
177
178 match web_session_lookup(
179 &web_context.pool,
180 &web_session.session_group,
181 Some(&web_session.did),
182 )
183 .await
184 {
185 Ok(record) => {
186 debug!(?web_session.session_group, "Session validated");
187 return Ok(Self(Some(record.0), Some(record.1)));
188 }
189 Err(err) => {
190 debug!(?web_session.session_group, ?err, "Invalid session");
191 return Ok(Self(None, None));
192 }
193 };
194 }
195
196 trace!("No session cookie found");
197 Ok(Self(None, None))
198 }
199}