implement authentication with separate public and private routers
This commit is contained in:
parent
3a05396afb
commit
5d37c12913
|
@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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) => {
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod change;
|
||||
pub mod forgot;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod reset;
|
||||
|
|
Loading…
Reference in New Issue