From 5d37c1291363c07485b00e3707919e6fc15f7f8a Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 23 Mar 2022 14:56:31 +0200 Subject: [PATCH] implement authentication with separate public and private routers --- peach-web/src/main.rs | 101 ++++++++++++------ .../src/{router.rs => private_router.rs} | 30 ++---- peach-web/src/public_router.rs | 73 +++++++++++++ peach-web/src/routes/authentication/forgot.rs | 43 ++++++++ peach-web/src/routes/authentication/login.rs | 18 ++-- peach-web/src/routes/authentication/logout.rs | 25 +++-- peach-web/src/routes/authentication/mod.rs | 1 + 7 files changed, 218 insertions(+), 73 deletions(-) rename peach-web/src/{router.rs => private_router.rs} (88%) create mode 100644 peach-web/src/public_router.rs create mode 100644 peach-web/src/routes/authentication/forgot.rs diff --git a/peach-web/src/main.rs b/peach-web/src/main.rs index ce85fbf..5704998 100644 --- a/peach-web/src/main.rs +++ b/peach-web/src/main.rs @@ -15,14 +15,18 @@ //mod context; mod config; pub mod error; -mod router; +mod private_router; +mod public_router; mod routes; //#[cfg(test)] //mod tests; mod templates; pub mod utils; -use std::sync::RwLock; +use std::{ + collections::HashMap, + sync::{Mutex, RwLock}, +}; use lazy_static::lazy_static; //use log::{debug, error, info}; @@ -32,7 +36,7 @@ use log::info; // crate-local dependencies use config::Config; -use utils::{sbot, theme::Theme}; +use utils::theme::Theme; pub type BoxError = Box; @@ -88,6 +92,13 @@ pub fn init_rocket() -> Rocket { } */ +/// Session data for each authenticated client. +#[derive(Debug, Clone)] +pub struct SessionData { + _login: String, +} + +// TODO: parse these values from config file (or env var) const HOSTNAME_AND_PORT: &str = "localhost:8000"; /// Launch the peach-web server. @@ -109,43 +120,67 @@ fn main() { } */ + // store the session data for each session and a hashmap that associates + // each session id with the data + // note: we are storing this data in memory. all sessions are erased when + // the program is restarted. + let sessions_storage: Mutex> = Mutex::new(HashMap::new()); + info!("Launching web server..."); - // the `start_server` starts listening forever on the given address. + // the `start_server` starts listening forever on the given address rouille::start_server(HOSTNAME_AND_PORT, move |request| { info!("Now listening on {}", HOSTNAME_AND_PORT); - // static file server - // matches on assets in the `static` directory - let static_response = rouille::match_assets(request, "static"); - if static_response.is_success() { - return static_response; - } + // We call `session::session` in order to assign a unique identifier + // to each client. This identifier is tracked through a cookie that + // is automatically appended to the response. + // + // The parameters of the function are the name of the cookie + // ("SID") and the duration of the session in seconds (one hour). + rouille::session::session(request, "SID", 3600, |session| { + // If the client already has an identifier from a previous request, + // we try to load the existing session data. If we successfully + // load data from `sessions_storage`, we make a copy of the data + // in order to avoid locking the session for too long. + // + // We thus obtain a `Option`. + let mut session_data = if session.client_has_sid() { + sessions_storage.lock().unwrap().get(session.id()).cloned() + } else { + None + }; - // set the `.ssb-go` path in order to mount the blob fileserver - let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path"); - let blobstore = format!("{}/blobs/sha256", ssb_path); + // Pass the request to the public router. + // + // The public router includes authentication-related routes which + // do not require the user to be authenticated (ie. login and reset + // password). + // + // If the user is already authenticated, their request will be + // passed to the private router by public_router::handle_route(). + // + // We pass a mutable reference to the `Option` so that + // the function is free to modify it. + let response = public_router::handle_route(request, &mut session_data); - // blobstore file server - // removes the /blob url prefix and serves blobs from blobstore - // matches on assets in the `static` directory - if let Some(request) = request.remove_prefix("/blob") { - return rouille::match_assets(&request, &blobstore); - } + // Since the function call to `handle_route` can modify the session + // data, we have to store it back in the `sessions_storage` when + // necessary. + if let Some(data) = session_data { + sessions_storage + .lock() + .unwrap() + .insert(session.id().to_owned(), data); + } else if session.client_has_sid() { + // If `handle_route` erased the content of the `Option`, we + // remove the session from the storage. This is only done + // if the client already has an identifier, otherwise calling + // `session.id()` will assign one. + sessions_storage.lock().unwrap().remove(session.id()); + } - router::mount_peachpub_routes(request) + response + }) }); } - -// TODO: write a test for each route -// look at `init_test_rocket()` from old code -// https://docs.rs/rouille/latest/rouille/struct.Request.html#method.fake_http -/* -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} -*/ diff --git a/peach-web/src/router.rs b/peach-web/src/private_router.rs similarity index 88% rename from peach-web/src/router.rs rename to peach-web/src/private_router.rs index 3237431..6b72a26 100644 --- a/peach-web/src/router.rs +++ b/peach-web/src/private_router.rs @@ -1,6 +1,6 @@ use rouille::{router, Request, Response}; -use crate::{routes, templates, utils::flash::FlashResponse}; +use crate::{routes, templates, utils::flash::FlashResponse, SessionData}; // TODO: add mount_peachcloud_routes() // https://github.com/tomaka/rouille/issues/232#issuecomment-919225104 @@ -10,9 +10,15 @@ use crate::{routes, templates, utils::flash::FlashResponse}; /// Takes an incoming request and matches on the defined routes, /// returning either a template or a redirect. /// +/// All of these routes require the user to be authenticated. See the +/// `public_router` for publically-accessible, authentication-related routes. +/// /// Excludes settings and status routes related to networking and the device /// (memory, hard disk, CPU etc.). -pub fn mount_peachpub_routes(request: &Request) -> Response { +pub fn mount_peachpub_routes( + request: &Request, + session_data: &mut Option, +) -> Response { router!(request, (GET) (/) => { Response::html(routes::home::build_template()) @@ -29,26 +35,8 @@ pub fn mount_peachpub_routes(request: &Request) -> Response { routes::authentication::change::handle_form(request) }, - (GET) (/auth/login) => { - Response::html(routes::authentication::login::build_template(request)) - .reset_flash() - }, - - (POST) (/auth/login) => { - routes::authentication::login::handle_form(request) - }, - (GET) (/auth/logout) => { - routes::authentication::logout::deauthenticate() - }, - - (GET) (/auth/reset) => { - Response::html(routes::authentication::reset::build_template(request)) - .reset_flash() - }, - - (POST) (/auth/reset) => { - routes::authentication::reset::handle_form(request) + routes::authentication::logout::deauthenticate(session_data) }, (GET) (/guide) => { diff --git a/peach-web/src/public_router.rs b/peach-web/src/public_router.rs new file mode 100644 index 0000000..8d0cc69 --- /dev/null +++ b/peach-web/src/public_router.rs @@ -0,0 +1,73 @@ +use rouille::{router, Request, Response}; + +use crate::{ + private_router, routes, + utils::{flash::FlashResponse, sbot}, + SessionData, +}; + +/// Receive an incoming request, mount the fileservers for static assets and +/// define the publically-accessible routes. +/// +/// If the request is for a private route (ie. a route requiring successful +/// authentication to view), check the authentication status of the user +/// by querying the `session_data`. If the user is authenticated, pass their +/// request to the private router. Otherwise, redirect them to the login page. +pub fn handle_route(request: &Request, session_data: &mut Option) -> Response { + // static file server + // matches on assets in the `static` directory + let static_response = rouille::match_assets(request, "static"); + if static_response.is_success() { + return static_response; + } + + // set the `.ssb-go` path in order to mount the blob fileserver + let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path"); + let blobstore = format!("{}/blobs/sha256", ssb_path); + + // blobstore file server + // removes the /blob url prefix and serves blobs from blobstore + // matches on assets in the `static` directory + if let Some(request) = request.remove_prefix("/blob") { + return rouille::match_assets(&request, &blobstore); + } + + // handle the routes which are always accessible (ie. whether logged-in + // or not) + router!(request, + (GET) (/auth/forgot) => { + Response::html(routes::authentication::forgot::build_template()) + }, + + (GET) (/auth/login) => { + Response::html(routes::authentication::login::build_template(request)) + .reset_flash() + }, + + (POST) (/auth/login) => { + routes::authentication::login::handle_form(request, session_data) + }, + + (GET) (/auth/reset) => { + Response::html(routes::authentication::reset::build_template(request)) + .reset_flash() + }, + + (POST) (/auth/reset) => { + routes::authentication::reset::handle_form(request) + }, + + _ => { + // now that we handled all the routes that are accessible in all + // circumstances, we check that the user is logged in before proceeding + if let Some(_session) = session_data.as_ref() { + // logged in: + // mount the routes which require authentication to view + private_router::mount_peachpub_routes(request, session_data) + } else { + // not logged in: + Response::redirect_303("/auth/login") + } + } + ) +} diff --git a/peach-web/src/routes/authentication/forgot.rs b/peach-web/src/routes/authentication/forgot.rs new file mode 100644 index 0000000..f9c256f --- /dev/null +++ b/peach-web/src/routes/authentication/forgot.rs @@ -0,0 +1,43 @@ +use maud::{html, PreEscaped}; + +use crate::{templates, utils::theme}; + +// ROUTE: /auth/forgot + +/// Forgot password template builder. +pub fn build_template() -> PreEscaped { + let form_template = html! { + (PreEscaped("")) + div class="card center" { + div class="capsule capsule-container border-info" { + p class="card-text" { + "Click the 'Send Temporary Password' button to send a new temporary password which can be used to change your device password." + } + p class="card-text" style="margin-top: 1rem;" { + "The temporary password will be sent in an SSB private message to the admin of this device." + } + p class="card-text" style="margin-top: 1rem;" { + "Once you have the temporary password, click the 'Set New Password' button to reach the password reset page." + } + } + form id="sendPasswordReset" action="/auth/send_password_reset" method="post" { + div id="buttonDiv" { + input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Temporary Password" title="Send temporary password to Scuttlebutt admin"; + a href="/auth/reset_password" class="button button-primary center" title="Set a new password using the temporary password" { + "Set New Password" + } + } + } + } + }; + + // wrap the nav bars around the settings menu template content + // parameters are template, title and back url + let body = templates::nav::build_template(form_template, "Send Password Reset", Some("/")); + + // query the current theme so we can pass it into the base template builder + let theme = theme::get_theme(); + + // render the base template with the provided body + templates::base::build_template(body, theme) +} diff --git a/peach-web/src/routes/authentication/login.rs b/peach-web/src/routes/authentication/login.rs index 0ad8b9d..3ef76c2 100644 --- a/peach-web/src/routes/authentication/login.rs +++ b/peach-web/src/routes/authentication/login.rs @@ -9,6 +9,7 @@ use crate::{ flash::{FlashRequest, FlashResponse}, theme, }, + SessionData, }; // HELPER AND ROUTES FOR /auth/login (GET and POST) @@ -29,7 +30,7 @@ pub fn build_template(request: &Request) -> PreEscaped { (PreEscaped("")) input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login"; div class="center-text" style="margin-top: 1rem;" { - a href="/settings/admin/forgot_password" class="label-small link font-gray" { "Forgot Password?" } + a href="/auth/forgot" class="label-small link font-gray" { "Forgot Password?" } } } } @@ -55,7 +56,7 @@ pub fn build_template(request: &Request) -> PreEscaped { /// Parse and verify the submitted password. If verification succeeds, set the /// auth session cookie and redirect to the home page. If not, set a flash /// message and redirect to the login page. -pub fn handle_form(request: &Request) -> Response { +pub fn handle_form(request: &Request, session_data: &mut Option) -> Response { // query the request body for form data // return a 400 error if the admin_id field is missing let data = try_or_400!(post_input!(request, { password: String })); @@ -63,12 +64,11 @@ pub fn handle_form(request: &Request) -> Response { match password_utils::verify_password(&data.password) { Ok(_) => { info!("Successful login attempt"); - // if successful login, add a cookie indicating the user is authenticated - // and redirect to home page - // NOTE: since we currently have just one user, the value of the cookie - // is just admin (this is arbitrary). - // If we had multiple users, we could put the user_id here. - //cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME)); + // if password verification is successful, write to `session_data` + // to authenticate the user + *session_data = Some(SessionData { + _login: data.password, + }); Response::redirect_303("/") } @@ -77,7 +77,7 @@ pub fn handle_form(request: &Request) -> Response { let err_msg = format!("Invalid password: {}", err); let (flash_name, flash_msg) = ( "flash_name=error".to_string(), - format!("flash_msg=Failed to save new password: {}", err_msg), + format!("flash_msg={}", err_msg), ); // if unsuccessful login, render /login page again diff --git a/peach-web/src/routes/authentication/logout.rs b/peach-web/src/routes/authentication/logout.rs index d3bb81b..21cc514 100644 --- a/peach-web/src/routes/authentication/logout.rs +++ b/peach-web/src/routes/authentication/logout.rs @@ -1,18 +1,23 @@ use log::info; use rouille::Response; -// HELPER AND ROUTES FOR /auth/logout (GET) +use crate::{utils::flash::FlashResponse, SessionData}; -/// Deauthenticate the logged-in user by removing the auth cookie. +// ROUTE: /auth/logout (GET) + +/// Deauthenticate the logged-in user by erasing the session data. /// Redirect to the login page. -pub fn deauthenticate() -> Response { - // logout authenticated user +pub fn deauthenticate(session_data: &mut Option) -> Response { info!("Attempting deauthentication of user."); - // TODO: remove auth cookie - //cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY)); - // redirect to the login page - // TODO: add flash message - //Flash::success(Redirect::to("/login"), "Logged out") - Response::redirect_303("/auth/login".to_string()) + // erase the content of `session_data` to deauthenticate the user + *session_data = None; + + let (flash_name, flash_msg) = ( + "flash_name=success".to_string(), + "flash_msg=Logged out".to_string(), + ); + + // set the flash cookie headers and redirect to the login page + Response::redirect_303("/auth/login".to_string()).add_flash(flash_name, flash_msg) } diff --git a/peach-web/src/routes/authentication/mod.rs b/peach-web/src/routes/authentication/mod.rs index f0b9e67..b848d99 100644 --- a/peach-web/src/routes/authentication/mod.rs +++ b/peach-web/src/routes/authentication/mod.rs @@ -1,4 +1,5 @@ pub mod change; +pub mod forgot; pub mod login; pub mod logout; pub mod reset;