use log::info; use rocket::form::{Form, FromForm}; use rocket::http::{Cookie, CookieJar, Status}; use rocket::request::{self, FlashMessage, FromRequest, Request}; use rocket::response::{Flash, Redirect}; use rocket::serde::{ json::{Json, Value}, Deserialize, Serialize, }; use rocket::{get, post, Config}; use rocket_dyn_templates::Template; use peach_lib::error::PeachError; use peach_lib::password_utils; use crate::error::PeachWebError; use crate::utils::{build_json_response, TemplateOrRedirect}; // 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 { // check for `disable_auth` config value; set to `false` if unset // can be set via the `ROCKET_DISABLE_AUTH` environment variable // - env var, if set, takes precedence over value defined in `Rocket.toml` let authentication_is_disabled: bool = match Config::figment().find_value("disable_auth") { // deserialize the boolean value; set to `false` if an error is encountered Ok(value) => value.deserialize().unwrap_or(false), Err(_) => 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 #[derive(Debug, Serialize)] pub struct LoginContext { pub back: Option, pub flash_name: Option, pub flash_msg: Option, pub title: Option, } impl LoginContext { pub fn build() -> LoginContext { LoginContext { back: None, flash_name: None, flash_msg: None, title: None, } } } #[get("/login")] pub fn login(flash: Option) -> Template { let mut context = LoginContext::build(); context.back = Some("/".to_string()); context.title = Some("Login".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.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; Template::render("login", &context) } #[derive(Debug, Deserialize, FromForm)] pub struct LoginForm { pub username: String, pub password: String, } /// Takes in a LoginForm and returns Ok(()) if username and password /// are correct to authenticate with peach-web. /// /// Note: currently there is only one user, and the username should always /// be "admin". 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 { let result = verify_login_form(login_form.into_inner()); match result { 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(_) => { // if unsuccessful login, render /login page again let mut context = LoginContext::build(); context.back = Some("/".to_string()); context.title = Some("Login".to_string()); context.flash_name = Some("error".to_string()); let flash_msg = "Invalid password".to_string(); context.flash_msg = Some(flash_msg); TemplateOrRedirect::Template(Template::render("login", &context)) } } } // 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, Deserialize, FromForm)] pub struct ResetPasswordForm { pub temporary_password: String, pub new_password1: String, pub new_password2: String, } #[derive(Debug, Serialize)] pub struct ResetPasswordContext { pub back: Option, pub title: Option, pub flash_name: Option, pub flash_msg: Option, } impl ResetPasswordContext { pub fn build() -> ResetPasswordContext { ResetPasswordContext { back: None, title: None, flash_name: None, flash_msg: None, } } } #[derive(Debug, Serialize)] pub struct ChangePasswordContext { pub back: Option, pub title: Option, pub flash_name: Option, pub flash_msg: Option, } impl ChangePasswordContext { pub fn build() -> ChangePasswordContext { ChangePasswordContext { back: None, title: None, flash_name: None, flash_msg: None, } } } /// 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 { let mut context = ResetPasswordContext::build(); context.back = Some("/".to_string()); context.title = Some("Reset 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.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; Template::render("settings/admin/reset_password", &context) } /// 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 result = save_reset_password_form(reset_password_form.into_inner()); match result { Ok(_) => { let mut context = ChangePasswordContext::build(); context.back = Some("/".to_string()); context.title = Some("Reset Password".to_string()); context.flash_name = Some("success".to_string()); let flash_msg = "New password is now saved. Return home to login".to_string(); context.flash_msg = Some(flash_msg); Template::render("settings/admin/reset_password", &context) } Err(err) => { let mut context = ChangePasswordContext::build(); // set back icon link to network route context.back = Some("/".to_string()); context.title = Some("Reset Password".to_string()); context.flash_name = Some("error".to_string()); context.flash_msg = Some(format!("Failed to reset password: {}", err)); Template::render("settings/admin/reset_password", &context) } } } /// 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_form_endpoint(reset_password_form: Json) -> Value { let result = save_reset_password_form(reset_password_form.into_inner()); match result { Ok(_) => { let status = "success".to_string(); let msg = "New password is now saved. Return home to login.".to_string(); build_json_response(status, None, Some(msg)) } Err(err) => { let status = "error".to_string(); let msg = format!("{}", err); build_json_response(status, None, Some(msg)) } } } // HELPERS AND ROUTES FOR /send_password_reset #[derive(Debug, Serialize)] pub struct SendPasswordResetContext { pub back: Option, pub title: Option, pub flash_name: Option, pub flash_msg: Option, } impl SendPasswordResetContext { pub fn build() -> SendPasswordResetContext { SendPasswordResetContext { back: None, title: None, flash_name: None, flash_msg: None, } } } /// 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 { let mut context = SendPasswordResetContext::build(); context.back = Some("/".to_string()); context.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.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; Template::render("settings/admin/forgot_password", &context) } /// 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 result = password_utils::send_password_reset(); match result { Ok(_) => { let mut context = ChangePasswordContext::build(); context.back = Some("/".to_string()); context.title = Some("Send Password Reset".to_string()); context.flash_name = Some("success".to_string()); let flash_msg = "A password reset link has been sent to the admin of this device".to_string(); context.flash_msg = Some(flash_msg); Template::render("settings/admin/forgot_password", &context) } Err(err) => { let mut context = ChangePasswordContext::build(); context.back = Some("/".to_string()); context.title = Some("Send Password Reset".to_string()); context.flash_name = Some("error".to_string()); context.flash_msg = Some(format!("Failed to send password reset link: {}", err)); Template::render("settings/admin/forgot_password", &context) } } } // HELPERS AND ROUTES FOR /settings/change_password #[derive(Debug, Deserialize, FromForm)] pub struct PasswordForm { pub old_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.old_password, password_form.new_password1, password_form.new_password2 ); password_utils::verify_password(&password_form.old_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 { let mut context = ChangePasswordContext::build(); // set back icon link to network route context.back = Some("/settings/admin".to_string()); context.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.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; Template::render("settings/admin/change_password", &context) } /// 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 result = save_password_form(password_form.into_inner()); match result { Ok(_) => { let mut context = ChangePasswordContext::build(); // set back icon link to network route context.back = Some("/settings/admin".to_string()); context.title = Some("Change Password".to_string()); context.flash_name = Some("success".to_string()); context.flash_msg = Some("New password is now saved".to_string()); // template_dir is set in Rocket.toml Template::render("settings/admin/change_password", &context) } Err(err) => { let mut context = ChangePasswordContext::build(); // set back icon link to network route context.back = Some("/settings/admin".to_string()); context.title = Some("Change Password".to_string()); context.flash_name = Some("error".to_string()); context.flash_msg = Some(format!("Failed to save new password: {}", err)); Template::render("settings/admin/change_password", &context) } } } /// JSON change password form request handler. #[post("/change_password", data = "")] pub fn save_password_form_endpoint( password_form: Json, _auth: Authenticated, ) -> Value { let result = save_password_form(password_form.into_inner()); match result { Ok(_) => { let status = "success".to_string(); let msg = "Your password was successfully changed".to_string(); build_json_response(status, None, Some(msg)) } Err(err) => { let status = "error".to_string(); let msg = format!("{}", err); build_json_response(status, None, Some(msg)) } } }