Add LoggedInUser extractor

This commit is contained in:
selfhoster selfhoster 2023-08-23 12:33:06 +00:00
parent a82e4e1d50
commit f5946e5ea0
5 changed files with 81 additions and 15 deletions

View File

@ -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 {

View File

@ -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<RoutableAppState>, form: LoginForm) -> Result<String, Error> {
pub async fn route(user: Option<LoggedInUser>, cookies: Cookies, state: State<RoutableAppState>, form: LoginForm) -> Result<String, Error> {
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<RoutableAppState>, 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);

View File

@ -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<String>, state: RoutableAppState) -> Router {
let app = Router::new()
@ -24,4 +22,4 @@ pub fn router(subpath: Option<String>, state: RoutableAppState) -> Router {
} else {
app
}
}
}

View File

@ -10,6 +10,8 @@ use sessions::SessionManager;
pub type RoutableAppState = Arc<AppState>;
pub const COOKIE_NAME: &'static str = "yunohost.ssowat";
pub struct AppState {
pub sessions: SessionManager,
pub users: YunohostUsers,

View File

@ -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<RoutableAppState> for LoggedInUser {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> {
// 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(),
}
});
}
}
}