aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/ui
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes/ui')
-rw-r--r--server/src/routes/ui/account/mod.rs10
-rw-r--r--server/src/routes/ui/account/session/guard.rs (renamed from server/src/routes/ui/account/session.rs)64
-rw-r--r--server/src/routes/ui/account/session/mod.rs17
-rw-r--r--server/src/routes/ui/account/session/token.rs72
-rw-r--r--server/src/routes/ui/style/transition.js1
5 files changed, 116 insertions, 48 deletions
diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs
index 0e4e0cc..79fa652 100644
--- a/server/src/routes/ui/account/mod.rs
+++ b/server/src/routes/ui/account/mod.rs
@@ -7,7 +7,6 @@ pub mod admin;
pub mod session;
pub mod settings;
-use self::session::SessionCookie;
use super::{error::MyError, layout::LayoutPage};
use crate::{
database::{Database, User},
@@ -16,6 +15,7 @@ use crate::{
};
use anyhow::anyhow;
use argon2::{password_hash::Salt, Argon2, PasswordHasher};
+use chrono::Duration;
use rocket::{
form::{Contextual, Form},
get,
@@ -159,7 +159,7 @@ pub fn r_account_login_post(
#[post("/account/logout")]
pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> {
- jar.remove_private(Cookie::named("user"));
+ jar.remove_private(Cookie::named("session"));
Ok(Redirect::found(uri!(r_home())))
}
@@ -181,10 +181,10 @@ pub fn login_logic(
Err(anyhow!("invalid password"))?
}
- jar.add_private(
+ jar.add(
Cookie::build(
- "user",
- serde_json::to_string(&SessionCookie::new(user.name)).unwrap(),
+ "session",
+ session::token::create(user.name, Duration::days(CONF.login_expire)),
)
.permanent()
.finish(),
diff --git a/server/src/routes/ui/account/session.rs b/server/src/routes/ui/account/session/guard.rs
index c41c968..58dfe01 100644
--- a/server/src/routes/ui/account/session.rs
+++ b/server/src/routes/ui/account/session/guard.rs
@@ -3,63 +3,38 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use crate::{
- database::{Database, User},
- routes::ui::error::MyError,
- CONF,
-};
+use super::{token, Session};
+use crate::{database::Database, routes::ui::error::MyError};
use anyhow::anyhow;
-use chrono::{DateTime, Duration, Utc};
+use log::warn;
use rocket::{
outcome::Outcome,
request::{self, FromRequest},
Request, State,
};
-use serde::{Deserialize, Serialize};
-
-pub struct Session {
- pub user: User,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct SessionCookie {
- name: String,
- expire: DateTime<Utc>,
-}
-
-impl SessionCookie {
- pub fn new(name: String) -> Self {
- Self {
- name,
- expire: Utc::now() + Duration::days(CONF.login_expire),
- }
- }
-}
impl Session {
pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> {
+ let username;
+
#[cfg(not(feature = "bypass-auth"))]
- let cookie = req
- .cookies()
- .get_private("user")
- .ok_or(anyhow!("login required"))?;
- #[cfg(not(feature = "bypass-auth"))]
- let cookie = serde_json::from_str::<SessionCookie>(cookie.value())?;
- #[cfg(feature = "bypass-auth")]
- let cookie = SessionCookie {
- name: crate::CONF.admin_username.to_string(),
- expire: Utc::now() + Duration::days(CONF.login_expire),
+ {
+ let token = req
+ .query_value("session")
+ .map(|e| e.expect("str parse should not fail, right?"))
+ .or(req.cookies().get("session").map(|cookie| cookie.value()))
+ .ok_or(anyhow!("not logged in"))?;
+
+ username = token::validate(token)?;
};
- if cookie.expire < Utc::now() {
- Err(anyhow!("cookie expired"))?;
+ #[cfg(feature = "bypass-auth")]
+ {
+ username = "admin".to_string();
}
let db = req.guard::<&State<Database>>().await.unwrap();
- let user = db
- .users
- .get(&cookie.name.to_string())?
- .ok_or(anyhow!("user not found"))?;
+ let user = db.users.get(&username)?.ok_or(anyhow!("user not found"))?;
Ok(Session { user })
}
@@ -85,7 +60,10 @@ impl<'r> FromRequest<'r> for Session {
Box::pin(async move {
match Self::from_request_ut(request).await {
Ok(x) => Outcome::Success(x),
- Err(_) => Outcome::Forward(()),
+ Err(e) => {
+ warn!("authentificated route rejected: {e:?}");
+ Outcome::Forward(())
+ }
}
})
}
diff --git a/server/src/routes/ui/account/session/mod.rs b/server/src/routes/ui/account/session/mod.rs
new file mode 100644
index 0000000..1546ee7
--- /dev/null
+++ b/server/src/routes/ui/account/session/mod.rs
@@ -0,0 +1,17 @@
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+
+use crate::database::User;
+
+pub mod guard;
+pub mod token;
+
+pub struct Session {
+ pub user: User,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SessionData {
+ username: String,
+ expire: DateTime<Utc>,
+}
diff --git a/server/src/routes/ui/account/session/token.rs b/server/src/routes/ui/account/session/token.rs
new file mode 100644
index 0000000..d4546aa
--- /dev/null
+++ b/server/src/routes/ui/account/session/token.rs
@@ -0,0 +1,72 @@
+use super::SessionData;
+use aes_gcm_siv::{
+ aead::{generic_array::GenericArray, Aead},
+ KeyInit,
+};
+use anyhow::anyhow;
+use base64::Engine;
+use chrono::{Duration, Utc};
+use std::sync::LazyLock;
+
+static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| [(); 32].map(|_| rand::random()));
+
+pub fn create(username: String, expire: Duration) -> String {
+ let session_data = SessionData {
+ expire: Utc::now() + expire,
+ username,
+ };
+ let mut plaintext =
+ bincode::serde::encode_to_vec(&session_data, bincode::config::standard()).unwrap();
+
+ while plaintext.len() % 16 == 0 {
+ plaintext.push(0);
+ }
+
+ let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
+ let nonce = [(); 12].map(|_| rand::random());
+ eprintln!("SESSION_KEY={SESSION_KEY:?}");
+ let mut ciphertext = cipher
+ .encrypt(&GenericArray::from(nonce), plaintext.as_slice())
+ .unwrap();
+ ciphertext.extend(nonce);
+
+ base64::engine::general_purpose::STANDARD.encode(&ciphertext)
+}
+
+pub fn validate(token: &str) -> anyhow::Result<String> {
+ let ciphertext = base64::engine::general_purpose::STANDARD.decode(token)?;
+ let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
+ let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12);
+ let plaintext = cipher
+ .decrypt(nonce.into(), ciphertext)
+ .map_err(|e| anyhow!("decryption failed: {e:?}"))?;
+
+ let (session_data, _): (SessionData, _) =
+ bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?;
+
+ if session_data.expire < Utc::now() {
+ Err(anyhow!("session expired"))?
+ }
+
+ Ok(session_data.username)
+}
+
+#[test]
+fn test() {
+ let tok = create("blub".to_string(), Duration::days(1));
+ validate(&tok).unwrap();
+}
+
+#[test]
+fn test_crypto() {
+ let nonce = [(); 12].map(|_| rand::random());
+ let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap();
+ let plaintext = b"testing stuff---";
+ let ciphertext = cipher
+ .encrypt(&GenericArray::from(nonce), plaintext.as_slice())
+ .unwrap();
+ let plaintext2 = cipher
+ .decrypt((&nonce).into(), ciphertext.as_slice())
+ .unwrap();
+ assert_eq!(plaintext, plaintext2.as_slice());
+}
diff --git a/server/src/routes/ui/style/transition.js b/server/src/routes/ui/style/transition.js
index c125c42..7d39176 100644
--- a/server/src/routes/ui/style/transition.js
+++ b/server/src/routes/ui/style/transition.js
@@ -36,6 +36,7 @@ function prepare_load(href, back) {
let rt = ""
try {
const r = await r_promise
+ if (!r.ok) return document.body.innerHTML = "<h1>error</h1>"
rt = await r.text()
} catch (e) {
console.error(e)