i18n+filtering fork - fluent-templates v2
at main 199 lines 6.4 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 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}