diff --git a/Cargo.lock b/Cargo.lock index d8c939c..3192727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,9 +2581,11 @@ dependencies = [ name = "peach-web" version = "0.5.0" dependencies = [ + "async-std", "base64 0.13.0", "dirs 4.0.0", "env_logger 0.8.4", + "futures 0.3.21", "golgi", "lazy_static", "log 0.4.14", diff --git a/peach-web/src/routes/authentication.rs b/peach-web/src/routes/authentication.rs deleted file mode 100644 index 3ff8f2b..0000000 --- a/peach-web/src/routes/authentication.rs +++ /dev/null @@ -1,333 +0,0 @@ -use log::info; -use rocket::{ - form::{Form, FromForm}, - get, - http::{Cookie, CookieJar, Status}, - post, - request::{self, FlashMessage, FromRequest, Request}, - response::{Flash, Redirect}, -}; -use rocket_dyn_templates::{tera::Context, Template}; - -use peach_lib::{error::PeachError, password_utils}; - -use crate::error::PeachWebError; -use crate::utils; -use crate::utils::TemplateOrRedirect; -use crate::RocketConfig; - -// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES - -pub const AUTH_COOKIE_KEY: &str = "peachweb_auth"; -pub const ADMIN_USERNAME: &str = "admin"; - -/// Note: Currently we use an empty struct for the Authenticated request guard -/// because there is only one user to be authenticated, and no data needs to be stored here. -/// In a multi-user authentication scheme, we would store the user_id in this struct, -/// and retrieve the correct user via the user_id stored in the cookie. -pub struct Authenticated; - -#[derive(Debug)] -pub enum LoginError { - UserNotLoggedIn, -} - -/// Request guard which returns an empty Authenticated struct from the request -/// if and only if the user has a cookie which proves they are authenticated with peach-web. -/// -/// Note that cookies.get_private uses encryption, which means that this private cookie -/// cannot be inspected, tampered with, or manufactured by clients. -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Authenticated { - type Error = LoginError; - - async fn from_request(req: &'r Request<'_>) -> request::Outcome { - // retrieve auth state from managed state (returns `Option`). - // this value is read from the Rocket.toml config file on start-up - let authentication_is_disabled: bool = *req - .rocket() - .state::() - .map(|config| (&config.disable_auth)) - .unwrap_or(&false); - - if authentication_is_disabled { - let auth = Authenticated {}; - request::Outcome::Success(auth) - } else { - let authenticated = req - .cookies() - .get_private(AUTH_COOKIE_KEY) - .and_then(|cookie| cookie.value().parse().ok()) - .map(|_value: String| Authenticated {}); - match authenticated { - Some(auth) => request::Outcome::Success(auth), - None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)), - } - } - } -} - -// HELPERS AND ROUTES FOR /login - -#[get("/login")] -pub fn login(flash: Option) -> Template { - // retrieve current ui theme - let theme = utils::get_theme(); - - let mut context = Context::new(); - context.insert("theme", &theme); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Login".to_string())); - - // check to see if there is a flash message to display - if let Some(flash) = flash { - context.insert("flash_name", &Some(flash.kind().to_string())); - context.insert("flash_msg", &Some(flash.message().to_string())); - }; - - Template::render("login", &context.into_json()) -} - -#[derive(Debug, FromForm)] -pub struct LoginForm { - pub password: String, -} - -/// Takes in a LoginForm and returns Ok(()) if the password is correct. -/// -/// Note: there is currently only one user, therefore we don't need a username. -pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> { - password_utils::verify_password(&login_form.password) -} - -#[post("/login", data = "")] -pub fn login_post(login_form: Form, cookies: &CookieJar<'_>) -> TemplateOrRedirect { - match verify_login_form(login_form.into_inner()) { - Ok(_) => { - // 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)); - - TemplateOrRedirect::Redirect(Redirect::to("/")) - } - Err(e) => { - let err_msg = format!("Invalid password: {}", e); - // if unsuccessful login, render /login page again - let mut context = Context::new(); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Login".to_string())); - context.insert("flash_name", &("error".to_string())); - context.insert("flash_msg", &(err_msg)); - - TemplateOrRedirect::Template(Template::render("login", &context.into_json())) - } - } -} - -// HELPERS AND ROUTES FOR /logout - -#[get("/logout")] -pub fn logout(cookies: &CookieJar<'_>) -> Flash { - // logout authenticated user - info!("Attempting deauthentication of user."); - cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY)); - Flash::success(Redirect::to("/login"), "Logged out") -} - -// HELPERS AND ROUTES FOR /reset_password - -#[derive(Debug, FromForm)] -pub struct ResetPasswordForm { - pub temporary_password: String, - pub new_password1: String, - pub new_password2: String, -} - -/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password. -pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> { - info!( - "reset password!: {} {} {}", - password_form.temporary_password, password_form.new_password1, password_form.new_password2 - ); - password_utils::verify_temporary_password(&password_form.temporary_password)?; - // if the previous line did not throw an error, then the secret_link is correct - password_utils::validate_new_passwords( - &password_form.new_password1, - &password_form.new_password2, - )?; - // if the previous line did not throw an error, then the new password is valid - password_utils::set_new_password(&password_form.new_password1)?; - Ok(()) -} - -/// Password reset request handler. This route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password. -#[get("/reset_password")] -pub fn reset_password(flash: Option) -> Template { - // retrieve current ui theme - let theme = utils::get_theme(); - - let mut context = Context::new(); - context.insert("theme", &theme); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Reset Password".to_string())); - - // check to see if there is a flash message to display - if let Some(flash) = flash { - context.insert("flash_name", &Some(flash.kind().to_string())); - context.insert("flash_msg", &Some(flash.message().to_string())); - }; - - Template::render("settings/admin/reset_password", &context.into_json()) -} - -/// Password reset form request handler. This route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password. -#[post("/reset_password", data = "")] -pub fn reset_password_post(reset_password_form: Form) -> Template { - let mut context = Context::new(); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Reset Password".to_string())); - - let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) { - Ok(_) => ( - "success".to_string(), - "New password has been saved. Return home to login".to_string(), - ), - Err(err) => ( - "error".to_string(), - format!("Failed to reset password: {}", err), - ), - }; - - context.insert("flash_name", &Some(flash_name)); - context.insert("flash_msg", &Some(flash_msg)); - - Template::render("settings/admin/reset_password", &context.into_json()) -} - -// HELPERS AND ROUTES FOR /send_password_reset - -/// Page for users who have forgotten their password. -/// This route is used by a user who is not logged in -/// to initiate the sending of a new password reset. -#[get("/forgot_password")] -pub fn forgot_password_page(flash: Option) -> Template { - // retrieve current ui theme - let theme = utils::get_theme(); - - let mut context = Context::new(); - context.insert("theme", &theme); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Send Password Reset".to_string())); - - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.insert("flash_name", &Some(flash.kind().to_string())); - context.insert("flash_msg", &Some(flash.message().to_string())); - }; - - Template::render("settings/admin/forgot_password", &context.into_json()) -} - -/// Send password reset request handler. This route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password. A successful request results -/// in a Scuttlebutt private message being sent to the account of the device admin. -#[post("/send_password_reset")] -pub fn send_password_reset_post() -> Template { - info!("++ send password reset post"); - let mut context = Context::new(); - context.insert("back", &Some("/".to_string())); - context.insert("title", &Some("Send Password Reset".to_string())); - - let (flash_name, flash_msg) = match password_utils::send_password_reset() { - Ok(_) => ( - "success".to_string(), - "A password reset link has been sent to the admin of this device".to_string(), - ), - Err(err) => ( - "error".to_string(), - format!("Failed to send password reset link: {}", err), - ), - }; - - context.insert("flash_name", &Some(flash_name)); - context.insert("flash_msg", &Some(flash_msg)); - - Template::render("settings/admin/forgot_password", &context.into_json()) -} - -// HELPERS AND ROUTES FOR /settings/change_password - -#[derive(Debug, FromForm)] -pub struct PasswordForm { - pub current_password: String, - pub new_password1: String, - pub new_password2: String, -} - -/// Password save form request handler. This function is for use by a user who is already logged in to change their password. -pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> { - info!( - "change password!: {} {} {}", - password_form.current_password, password_form.new_password1, password_form.new_password2 - ); - password_utils::verify_password(&password_form.current_password)?; - // if the previous line did not throw an error, then the old password is correct - password_utils::validate_new_passwords( - &password_form.new_password1, - &password_form.new_password2, - )?; - // if the previous line did not throw an error, then the new password is valid - password_utils::set_new_password(&password_form.new_password1)?; - Ok(()) -} - -/// Change password request handler. This is used by a user who is already logged in. -#[get("/change_password")] -pub fn change_password(flash: Option, _auth: Authenticated) -> Template { - // retrieve current ui theme - let theme = utils::get_theme(); - - let mut context = Context::new(); - context.insert("theme", &theme); - context.insert("back", &Some("/settings/admin".to_string())); - context.insert("title", &Some("Change Password".to_string())); - - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.insert("flash_name", &Some(flash.kind().to_string())); - context.insert("flash_msg", &Some(flash.message().to_string())); - }; - - Template::render("settings/admin/change_password", &context.into_json()) -} - -/// Change password form request handler. This route is used by a user who is already logged in. -#[post("/change_password", data = "")] -pub fn change_password_post(password_form: Form, _auth: Authenticated) -> Template { - let mut context = Context::new(); - context.insert("back", &Some("/settings/admin".to_string())); - context.insert("title", &Some("Change Password".to_string())); - - let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) { - Ok(_) => ( - "success".to_string(), - "New password has been saved".to_string(), - ), - Err(err) => ( - "error".to_string(), - format!("Failed to save new password: {}", err), - ), - }; - - context.insert("flash_name", &Some(flash_name)); - context.insert("flash_msg", &Some(flash_msg)); - - Template::render("settings/admin/change_password", &context.into_json()) -}