use log::{info}; use rocket::request::{FlashMessage}; use rocket::form::{Form, FromForm}; use rocket::response::{Flash, Redirect}; use rocket::{get, post}; use rocket::serde::json::Json; use rocket_dyn_templates::Template; use rocket::serde::{Deserialize, Serialize}; use peach_lib::password_utils; use peach_lib::error::PeachError; use crate::error::PeachWebError; use crate::utils::{build_json_response, TemplateOrRedirect}; use rocket::serde::json::Value; use rocket::request::{self, FromRequest, Request}; use rocket::http::{Cookie, CookieJar, Status}; // 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 Authenticated struct with is_authenticated=true /// iff 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 { 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 { info!("call to login post"); 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. /// All routes under /public/* are excluded from nginx basic auth via the nginx config. #[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("password/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. /// This route is excluded from nginx basic auth via the nginx config. #[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("password/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("password/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. /// All routes under /public/* are excluded from nginx basic auth via the nginx config. #[post("/public/api/v1/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, } } } /// Password reset request handler. This route is used by a user who is not logged in to send a new password reset link. #[get("/send_password_reset")] pub fn send_password_reset_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("password/send_password_reset", &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("password/send_password_reset", &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("password/send_password_reset", &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("/settings/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("/network".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("password/change_password", &context) } /// Change password form request handler. This route is used by a user who is already logged in. #[post("/settings/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("/network".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("password/change_password", &context) } Err(err) => { let mut context = ChangePasswordContext::build(); // set back icon link to network route context.back = Some("/network".to_string()); context.title = Some("Configure DNS".to_string()); context.flash_name = Some("error".to_string()); context.flash_msg = Some(format!("Failed to save new password: {}", err)); Template::render("password/change_password", &context) } } } /// JSON change password form request handler. #[post("/api/v1/settings/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)) } } }