implement authentication with separate public and private routers

This commit is contained in:
glyph 2022-03-23 14:56:31 +02:00
parent 3a05396afb
commit 5d37c12913
7 changed files with 218 additions and 73 deletions

View File

@ -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<dyn std::error::Error>;
@ -88,6 +92,13 @@ pub fn init_rocket() -> Rocket<Build> {
}
*/
/// 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<HashMap<String, SessionData>> = 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<SessionData>`.
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<SessionData>` 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);
}
}
*/

View File

@ -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<SessionData>,
) -> 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) => {

View File

@ -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<SessionData>) -> 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")
}
}
)
}

View File

@ -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<String> {
let form_template = html! {
(PreEscaped("<!-- PASSWORD RESET REQUEST CARD -->"))
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)
}

View File

@ -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<String> {
(PreEscaped("<!-- login (form submission) button -->"))
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<String> {
/// 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<SessionData>) -> 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

View File

@ -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<SessionData>) -> 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)
}

View File

@ -1,4 +1,5 @@
pub mod change;
pub mod forgot;
pub mod login;
pub mod logout;
pub mod reset;