From f5946e5ea0762b6c3162bdf3f8a1b21c5844df6f Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Wed, 23 Aug 2023 12:33:06 +0000 Subject: [PATCH] Add LoggedInUser extractor --- src/error.rs | 3 ++ src/routes/login.rs | 16 +++------- src/routes/mod.rs | 4 +-- src/state/mod.rs | 2 ++ src/state/sessions.rs | 71 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/error.rs b/src/error.rs index 1209a4e..3211963 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,6 +29,9 @@ pub enum Error { #[snafu(display("Failed to set group on file {}", path.display()))] PermissionsChgrp { path: PathBuf, source: file_owner::FileOwnerError }, + + #[snafu(display("No cookie jar"))] + Cookie, } impl IntoResponse for Error { diff --git a/src/routes/login.rs b/src/routes/login.rs index b20e49d..9026d3a 100644 --- a/src/routes/login.rs +++ b/src/routes/login.rs @@ -11,8 +11,7 @@ use yunohost_api::{Username, Password}; use crate::{ error::*, - routes::COOKIE_NAME, - state::RoutableAppState, + state::{COOKIE_NAME, RoutableAppState, sessions::LoggedInUser}, }; #[derive(Debug, TryFromMultipart, Deserialize)] @@ -61,16 +60,11 @@ where } #[debug_handler] -pub async fn route(cookies: Cookies, state: State, form: LoginForm) -> Result { +pub async fn route(user: Option, cookies: Cookies, state: State, form: LoginForm) -> Result { trace!("ROUTE: /login/"); - if let Some(session_cookie) = cookies.get(COOKIE_NAME) { - trace!("User claims to have valid {} session: {}", COOKIE_NAME, &session_cookie); - if let Some(username) = state.sessions.verify_cookie(session_cookie.value()).await.context(SessionSnafu)? { - debug!("User claims were verified. They are identified as {}", &username); - return Ok(format!("Welcome back, {}! You were already logged in.", username)); - } - debug!("User claims for a {} session were unfounded. Performing login again.", COOKIE_NAME); + if let Some(username) = user { + return Ok(format!("Welcome back, {}! You were already logged in.", username)); } debug!("Performing login attempt for user {}", &form.username); @@ -79,7 +73,7 @@ pub async fn route(cookies: Cookies, state: State, form: Login if state.check_login(&form.username, &form.password).await.unwrap() { debug!("Login was successful for user {}. Saving cookie now.", &form.username); let (cookie_name, cookie_value) = state.sessions.make_session(COOKIE_NAME, &form.username).await; - cookies.add(Cookie::new(cookie_name, cookie_value)); + cookies.add(Cookie::new(COOKIE_NAME, cookie_value)); Ok(format!("Welcome {}", &form.username)) } else { debug!("Login failed for user {}", &form.username); diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 7b6108c..7f101be 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -9,8 +9,6 @@ use crate::state::RoutableAppState; mod index; mod login; -pub const COOKIE_NAME: &'static str = "yunohost.ssowat"; - /// Build a router for the application, in a specific subpath eg `/yunohost/sso/` pub fn router(subpath: Option, state: RoutableAppState) -> Router { let app = Router::new() @@ -24,4 +22,4 @@ pub fn router(subpath: Option, state: RoutableAppState) -> Router { } else { app } -} \ No newline at end of file +} diff --git a/src/state/mod.rs b/src/state/mod.rs index 7f4f83a..345f99c 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -10,6 +10,8 @@ use sessions::SessionManager; pub type RoutableAppState = Arc; +pub const COOKIE_NAME: &'static str = "yunohost.ssowat"; + pub struct AppState { pub sessions: SessionManager, pub users: YunohostUsers, diff --git a/src/state/sessions.rs b/src/state/sessions.rs index 8ca4706..8f476a5 100644 --- a/src/state/sessions.rs +++ b/src/state/sessions.rs @@ -1,9 +1,19 @@ +use axum::{ + extract::FromRequestParts, + http::request::Parts, + RequestPartsExt, +}; use ring::{hmac,rand}; use snafu::prelude::*; use tokio::sync::RwLock; +use tower_cookies::Cookies; use yunohost_api::Username; -use crate::utils::time::now; +use crate::{ + error::*, + state::{COOKIE_NAME, RoutableAppState}, + utils::time::now, +}; /// An error related to session management #[derive(Debug, Snafu)] @@ -19,6 +29,10 @@ pub enum SessionError { MalformedCookieTimestamp { timestamp: String, source: std::num::ParseIntError }, #[snafu(display("Malformed cookie username (empty)"))] MalformedCookieUsername { source: yunohost_api::Error }, + #[snafu(display("Invalid or expired session {}", cookie_name))] + InvalidOrExpired { cookie_name: String }, + #[snafu(display("No session {}", cookie_name))] + NoSession { cookie_name: String }, } /// Holds the currently active cookie-based user sessions for a certain cookie type. @@ -177,3 +191,58 @@ impl Cookie { self.username.clone() } } + +#[derive(Clone, Debug)] +pub struct LoggedInUser(Username); + +impl LoggedInUser { + pub fn new(username: Username) -> LoggedInUser { + LoggedInUser(username) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn as_username(&self) -> &Username { + &self.0 + } +} + +impl std::fmt::Display for LoggedInUser { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(fmt) + } +} + +#[async_trait] +impl FromRequestParts for LoggedInUser { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { + // TODO: error + let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; + trace!("[SESSION:{}] Checking if the user has a valid session", COOKIE_NAME); + if let Some(session_cookie) = cookies.get(COOKIE_NAME) { + trace!("[SESSION:{}] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); + if let Some(username) = state.sessions.verify_cookie(session_cookie.value()).await.context(SessionSnafu)? { + debug!("[SESSION:{}] User {} resumed session.", COOKIE_NAME, &username); + return Ok(LoggedInUser::new(username)); + } else { + trace!("[SESSION:{}] User session is invalid or has expired.", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::InvalidOrExpired { + cookie_name: COOKIE_NAME.to_string(), + } + }); + } + } else { + trace!("[SESSION:{}] No current session cookie", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::NoSession { + cookie_name: COOKIE_NAME.to_string(), + } + }); + } + } +}