diff options
author | metamuffin <metamuffin@disroot.org> | 2024-05-30 00:09:11 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-05-30 00:09:11 +0200 |
commit | 532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a (patch) | |
tree | c4422c4d54e01f63bae391cd95788cad74f59fbb /src/modules/auth/cookie.rs | |
parent | 8b39940a58c28bc1bbe291eb5229e9ce1444e33c (diff) | |
download | gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.bz2 gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.zst |
rename filters dir
Diffstat (limited to 'src/modules/auth/cookie.rs')
-rw-r--r-- | src/modules/auth/cookie.rs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/src/modules/auth/cookie.rs b/src/modules/auth/cookie.rs new file mode 100644 index 0000000..4615938 --- /dev/null +++ b/src/modules/auth/cookie.rs @@ -0,0 +1,182 @@ +use crate::{ + config::{return_true, DynNode}, + error::ServiceError, + modules::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse}, +}; +use aes_gcm_siv::{ + aead::{Aead, Payload}, + Nonce, +}; +use base64::Engine; +use futures::Future; +use headers::{Cookie, HeaderMapExt}; +use http_body_util::{combinators::BoxBody, BodyExt}; +use hyper::{ + header::{HeaderValue, LOCATION, REFERER, SET_COOKIE}, + Method, Response, StatusCode, +}; +use log::debug; +use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; +use rand::random; +use serde::Deserialize; +use serde_yaml::Value; +use std::fmt::Write; +use std::{pin::Pin, sync::Arc, time::SystemTime}; + +use super::Credentials; + +pub struct CookieAuthKind; +impl NodeKind for CookieAuthKind { + fn name(&self) -> &'static str { + "cookie_auth" + } + fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> { + Ok(Arc::new(serde_yaml::from_value::<CookieAuth>(config)?)) + } +} + +#[derive(Deserialize)] +pub struct CookieAuth { + users: Credentials, + expire: Option<u64>, + #[serde(default = "return_true")] + secure: bool, + next: DynNode, + fail: DynNode, +} + +impl Node for CookieAuth { + fn handle<'a>( + &'a self, + context: &'a mut NodeContext, + request: NodeRequest, + ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> { + Box::pin(async move { + if request.method() == Method::POST && request.uri().path() == "/_gnix_login" { + let referrer = request.headers().get(REFERER).cloned(); + let d = request + .into_body() + .collect() + .await + .map_err(|_| todo!()) + .unwrap(); + let d = String::from_utf8(d.to_bytes().to_vec()).unwrap(); + + // TODO proper parser + let mut username = "user"; + let mut password = ""; + for kv in d.split("&") { + let (key, value) = kv.split_once("=").ok_or(ServiceError::BadAuth)?; + match key { + "username" => username = value, + "password" => password = value, + _ => (), + } + } + let mut r = Response::new(BoxBody::<_, ServiceError>::new( + String::new().clone().map_err(|_| unreachable!()), + )); + *r.status_mut() = StatusCode::FOUND; + debug!("login attempt for {username:?}"); + if self.users.authentificate(username, password) { + debug!("login success"); + let nonce = [(); 12].map(|_| random::<u8>()); + let plaintext = unix_seconds().to_le_bytes(); + let mut ciphertext = context + .state + .crypto_key + .encrypt( + Nonce::from_slice(&nonce), + Payload { + msg: &plaintext, + aad: username.as_bytes(), + }, + ) + .unwrap(); + + ciphertext.extend(nonce); + let auth = base64::engine::general_purpose::URL_SAFE.encode(ciphertext); + + let mut cookie_opts = String::new(); + if let Some(e) = self.expire { + write!(cookie_opts, "; Expire={e}").unwrap(); + } + if self.secure { + write!(cookie_opts, "; Secure").unwrap(); + } + + r.headers_mut().append( + SET_COOKIE, + HeaderValue::from_str(&format!( + "gnix_username={}{}", + percent_encode(username.as_bytes(), NON_ALPHANUMERIC), + cookie_opts + )) + .unwrap(), + ); + r.headers_mut().append( + SET_COOKIE, + HeaderValue::from_str(&format!("gnix_auth={}{}", auth, cookie_opts)) + .unwrap(), + ); + } else { + debug!("login fail"); + } + r.headers_mut() + .append(LOCATION, referrer.unwrap_or(HeaderValue::from_static("/"))); + + Ok(r) + } else { + if let Some(cookie) = request.headers().typed_get::<Cookie>() { + if let Some(auth) = cookie.get("gnix_auth") { + let username = + percent_decode_str(cookie.get("gnix_username").unwrap_or("user")) + .decode_utf8()?; + + let auth = base64::engine::general_purpose::URL_SAFE.decode(auth)?; + if auth.len() < 12 { + return Err(ServiceError::BadAuth); + } + let (msg, nonce) = auth.split_at(auth.len() - 12); + let plaintext = context.state.crypto_key.decrypt( + Nonce::from_slice(nonce), + Payload { + msg, + aad: username.as_bytes(), + }, + ); + if let Ok(plaintext) = plaintext { + let created = u64::from_le_bytes(plaintext[0..8].try_into().unwrap()); + + if self + .expire + .map(|e| created + e > unix_seconds()) + .unwrap_or(true) + { + debug!("valid auth for {username:?}"); + return self.next.handle(context, request).await; + } else { + debug!("auth expired"); + } + } else { + debug!("aead invalid"); + } + } else { + debug!("no auth cookie"); + } + } + debug!("unauthorized"); + let mut r = self.fail.handle(context, request).await?; + *r.status_mut() = StatusCode::UNAUTHORIZED; + Ok(r) + } + }) + } +} + +fn unix_seconds() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} |