diff --git a/Cargo.toml b/Cargo.toml index f27a9a3..dadc47a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,5 @@ yunohost-api = { path = "yunohost-api", features = [ "axum" ] } axum_typed_multipart = "0.8" async-trait = "0.1" serde = { version = "1", features = [ "derive" ] } -file-owner = { version = "0.1" } \ No newline at end of file +file-owner = { version = "0.1" } +tower-cookies = "0.9" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index a2984ba..1209a4e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use axum::response::{IntoResponse, Response}; use snafu::Snafu; use std::path::PathBuf; @@ -29,3 +30,9 @@ pub enum Error { #[snafu(display("Failed to set group on file {}", path.display()))] PermissionsChgrp { path: PathBuf, source: file_owner::FileOwnerError }, } + +impl IntoResponse for Error { + fn into_response(self) -> Response { + self.to_string().into_response() + } +} diff --git a/src/routes/login.rs b/src/routes/login.rs index e255c2e..b20e49d 100644 --- a/src/routes/login.rs +++ b/src/routes/login.rs @@ -1,13 +1,19 @@ use axum::{ - extract::{FromRequest, Form, Json, Query, State}, + extract::{FromRequest, Form, Json, State}, http::{self, Request, StatusCode}, response::{IntoResponse, Response}, RequestExt, }; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; +use snafu::prelude::*; +use tower_cookies::{Cookies, Cookie}; use yunohost_api::{Username, Password}; -use crate::state::RoutableAppState; +use crate::{ + error::*, + routes::COOKIE_NAME, + state::RoutableAppState, +}; #[derive(Debug, TryFromMultipart, Deserialize)] pub struct LoginForm { @@ -55,10 +61,28 @@ where } #[debug_handler] -pub async fn route(state: State, form: LoginForm) -> String { - if state.check_login(&form.username, &form.password).await.unwrap() { - format!("Welcome {}", &form.username) +pub async fn route(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); + } + + debug!("Performing login attempt for user {}", &form.username); + + // No cookie, or cookie is invalid. Perform 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)); + Ok(format!("Welcome {}", &form.username)) } else { - format!("Invalid login for {}", &form.username) + debug!("Login failed for user {}", &form.username); + Ok(format!("Invalid login for {}", &form.username)) } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 1a63e07..7b6108c 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,19 +1,22 @@ use axum::{ - extract::State, routing::{get, post}, Router, }; +use tower_cookies::CookieManagerLayer; 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() .route("/", get(index::route)) .route("/login/", post(login::route)) + .layer(CookieManagerLayer::new()) .with_state(state); if let Some(p) = subpath { Router::new() diff --git a/src/state/mod.rs b/src/state/mod.rs index 4e28dfe..7f4f83a 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -11,8 +11,8 @@ use sessions::SessionManager; pub type RoutableAppState = Arc; pub struct AppState { - sessions: SessionManager, - users: YunohostUsers, + pub sessions: SessionManager, + pub users: YunohostUsers, } impl AppState { diff --git a/src/state/sessions.rs b/src/state/sessions.rs index d5ea720..8ca4706 100644 --- a/src/state/sessions.rs +++ b/src/state/sessions.rs @@ -89,7 +89,7 @@ impl SessionManager { /// Generates a new valid cookie inside the `SessionManager`, and returns: /// - the cookie name to be set /// - the cookie content that can be sent to a client - pub async fn make_session(&mut self, cookie_name: &str, username: &Username) -> (String, String) { + pub async fn make_session(&self, cookie_name: &str, username: &Username) -> (String, String) { let now = now(); let signable_payload = format!("{now}:{cookie_name}:{username}"); let signed_payload = hmac::sign(&self.secret, signable_payload.as_bytes()).as_ref().to_vec();