summaryrefslogtreecommitdiff
path: root/src/modules/auth/cookie.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
committermetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
commit532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a (patch)
treec4422c4d54e01f63bae391cd95788cad74f59fbb /src/modules/auth/cookie.rs
parent8b39940a58c28bc1bbe291eb5229e9ce1444e33c (diff)
downloadgnix-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.rs182
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()
+}