diff options
| author | metamuffin <metamuffin@disroot.org> | 2023-08-01 14:46:14 +0200 | 
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2023-08-01 14:46:14 +0200 | 
| commit | dbb8c1c2f0035ea41224dec319a996b89e13ec84 (patch) | |
| tree | b99f5c6b25e54fccb23f789ca748bd9ddeba5ed4 /server | |
| parent | 705fba50de94a7efdb9fe635ee683d6a615348f8 (diff) | |
| download | jellything-dbb8c1c2f0035ea41224dec319a996b89e13ec84.tar jellything-dbb8c1c2f0035ea41224dec319a996b89e13ec84.tar.bz2 jellything-dbb8c1c2f0035ea41224dec319a996b89e13ec84.tar.zst | |
new session based login
Diffstat (limited to 'server')
| -rw-r--r-- | server/Cargo.toml | 6 | ||||
| -rw-r--r-- | server/src/main.rs | 2 | ||||
| -rw-r--r-- | server/src/routes/ui/account/mod.rs | 10 | ||||
| -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.rs | 17 | ||||
| -rw-r--r-- | server/src/routes/ui/account/session/token.rs | 72 | ||||
| -rw-r--r-- | server/src/routes/ui/style/transition.js | 1 | 
7 files changed, 123 insertions, 49 deletions
| diff --git a/server/Cargo.toml b/server/Cargo.toml index b73841e..13b7aef 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,7 @@ jellycommon = { path = "../common" }  jellyremuxer = { path = "../remuxer" }  serde = { version = "1.0.163", features = ["derive"] } +bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] }  serde_json = "1.0.96"  log = "0.4.18" @@ -15,10 +16,13 @@ env_logger = "0.10.0"  anyhow = "1.0.71"  once_cell = "1.17.2"  chashmap = "2.2.2" -argon2 = "0.5.0"  rand = "0.8.5" +base64 = "0.21.2"  chrono = { version = "0.4.26", features = ["serde"] } +argon2 = "0.5.0" +aes-gcm-siv = "0.11.1" +  async-std = "1.12.0"  rocket = { version = "0.5.0-rc.3", features = ["secrets", "json"] }  tokio = { version = "1.28.2", features = ["io-util"] } diff --git a/server/src/main.rs b/server/src/main.rs index a9a22cf..eba9532 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,6 +3,8 @@      which is licensed under the GNU Affero General Public License (version 3); see /COPYING.      Copyright (C) 2023 metamuffin <metamuffin.org>  */ +#![feature(lazy_cell)] +  use config::{load_global_config, GlobalConfig};  use database::Database;  use jellyremuxer::RemuxerContext; 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) | 
