From e09c73efceae1cadedb19408a6837bb7c21c5f4b Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 24 Jun 2021 18:23:23 +0200 Subject: [PATCH] Register clients' passwords using OPAQUE --- app/src/api.rs | 78 ++++++++++++---- app/src/create_user.rs | 146 ++++++++++++++++++++++-------- model/src/lib.rs | 2 +- src/domain/sql_backend_handler.rs | 66 +++++--------- src/domain/sql_opaque_handler.rs | 13 ++- src/main.rs | 1 - 6 files changed, 199 insertions(+), 107 deletions(-) diff --git a/app/src/api.rs b/app/src/api.rs index c1ff7d1..67f1e04 100644 --- a/app/src/api.rs +++ b/app/src/api.rs @@ -76,40 +76,48 @@ where FetchService::fetch_with_options(request, get_default_options(), handler) } +fn call_server_json_with_error_message( + url: &str, + request: RB, + callback: Callback>, + error_message: &'static str, +) -> Result +where + CallbackResult: serde::de::DeserializeOwned + 'static, + RB: Into>, + Req: Into, +{ + call_server( + url, + request, + callback, + move |status: http::StatusCode, data: String| { + if status.is_success() { + serde_json::from_str(&data).map_err(|e| anyhow!("Could not parse response: {}", e)) + } else { + Err(anyhow!("{}[{}]: {}", error_message, status, data)) + } + }, + ) +} + impl HostService { pub fn list_users( request: ListUsersRequest, callback: Callback>>, ) -> Result { - call_server("/api/users", &request, callback, |status, data: String| { - if status.is_success() { - serde_json::from_str(&data).map_err(|e| anyhow!("Could not parse response: {}", e)) - } else { - Err(anyhow!("[{}]: {}", status, data)) - } - }) + call_server_json_with_error_message("/api/users", &request, callback, "") } pub fn login_start( request: login::ClientLoginStartRequest, callback: Callback>>, ) -> Result { - call_server( + call_server_json_with_error_message( "/auth/opaque/login/start", &request, callback, - |status, data: String| { - if status.is_success() { - serde_json::from_str(&data) - .map_err(|e| anyhow!("Could not parse response: {}", e)) - } else { - Err(anyhow!( - "Could not start authentication: [{}]: {}", - status, - data - )) - } - }, + "Could not start authentication: ", ) } @@ -141,6 +149,36 @@ impl HostService { ) } + pub fn register_start( + request: registration::ClientRegistrationStartRequest, + callback: Callback>>, + ) -> Result { + call_server_json_with_error_message( + "/auth/opaque/registration/start", + &request, + callback, + "Could not start registration: ", + ) + } + + pub fn register_finish( + request: registration::ClientRegistrationFinishRequest, + callback: Callback>, + ) -> Result { + call_server( + "/auth/opaque/registration/finish", + &request, + callback, + |status, data: String| { + if status.is_success() { + Ok(()) + } else { + Err(anyhow!("Could finish registration: [{}]: {}", status, data)) + } + }, + ) + } + pub fn logout(callback: Callback>) -> Result { call_server( "/auth/logout", diff --git a/app/src/create_user.rs b/app/src/create_user.rs index 71a23ea..2e2e988 100644 --- a/app/src/create_user.rs +++ b/app/src/create_user.rs @@ -1,5 +1,5 @@ use crate::api::HostService; -use anyhow::Result; +use anyhow::{anyhow, Result}; use lldap_model::*; use yew::prelude::*; use yew::services::{fetch::FetchTask, ConsoleService}; @@ -13,6 +13,7 @@ pub struct CreateUserForm { route_dispatcher: RouteAgentDispatcher, node_ref: NodeRef, error: Option, + registration_start: Option, // Used to keep the request alive long enough. _task: Option, } @@ -20,19 +21,112 @@ pub struct CreateUserForm { pub enum Msg { CreateUserResponse(Result<()>), SubmitForm, + SuccessfulCreation, + RegistrationStartResponse(Result>), + RegistrationFinishResponse(Result<()>), +} + +#[allow(clippy::ptr_arg)] +fn not_empty(s: &String) -> bool { + !s.is_empty() } impl CreateUserForm { - fn create_user(&mut self, req: CreateUserRequest) { - match HostService::create_user(req, self.link.callback(Msg::CreateUserResponse)) { - Ok(task) => self._task = Some(task), - Err(e) => { - self._task = None; - ConsoleService::log(format!("Error trying to create user: {}", e).as_str()) + fn handle_msg(&mut self, msg: ::Message) -> Result<()> { + match msg { + Msg::SubmitForm => { + let req = CreateUserRequest { + user_id: get_element("username") + .filter(not_empty) + .ok_or_else(|| anyhow!("Missing username"))?, + email: get_element("email") + .filter(not_empty) + .ok_or_else(|| anyhow!("Missing email"))?, + display_name: get_element("displayname").filter(not_empty), + first_name: get_element("firstname").filter(not_empty), + last_name: get_element("lastname").filter(not_empty), + }; + self._task = Some( + HostService::create_user(req, self.link.callback(Msg::CreateUserResponse)) + .map_err(|e| anyhow!("Error trying to create user: {}", e))?, + ); } - }; + Msg::CreateUserResponse(r) => { + if r.is_err() { + return r; + } + let user_id = get_element("username") + .filter(not_empty) + .ok_or_else(|| anyhow!("Missing username"))?; + if let Some(password) = get_element("password").filter(not_empty) { + // User was successfully created, let's register the password. + let mut rng = rand::rngs::OsRng; + let client_registration_start = + opaque::client::registration::start_registration(&password, &mut rng)?; + self.registration_start = Some(client_registration_start.state); + let req = registration::ClientRegistrationStartRequest { + username: user_id, + registration_start_request: client_registration_start.message, + }; + self._task = Some( + HostService::register_start( + req, + self.link.callback(Msg::RegistrationStartResponse), + ) + .map_err(|e| anyhow!("Error trying to create user: {}", e))?, + ); + } else { + self.update(Msg::SuccessfulCreation); + } + } + Msg::RegistrationStartResponse(response) => { + debug_assert!(self.registration_start.is_some()); + let response = response?; + let mut rng = rand::rngs::OsRng; + let registration_upload = opaque::client::registration::finish_registration( + self.registration_start.take().unwrap(), + response.registration_response, + &mut rng, + )?; + let req = registration::ClientRegistrationFinishRequest { + server_data: response.server_data, + registration_upload: registration_upload.message, + }; + self._task = Some( + HostService::register_finish( + req, + self.link.callback(Msg::RegistrationFinishResponse), + ) + .map_err(|e| anyhow!("Error trying to register user: {}", e))?, + ); + } + Msg::RegistrationFinishResponse(response) => { + if response.is_err() { + return response; + } + self.update(Msg::SuccessfulCreation); + } + Msg::SuccessfulCreation => { + self.route_dispatcher + .send(RouteRequest::ChangeRoute(Route::new_no_state( + "/list_users", + ))); + } + } + Ok(()) } } +fn get_element(name: &str) -> Option { + use wasm_bindgen::JsCast; + Some( + web_sys::window()? + .document()? + .get_element_by_id(name)? + .dyn_into::() + .ok()? + .value(), + ) +} impl Component for CreateUserForm { type Message = Msg; @@ -44,42 +138,16 @@ impl Component for CreateUserForm { route_dispatcher: RouteAgentDispatcher::new(), node_ref: NodeRef::default(), error: None, + registration_start: None, _task: None, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::SubmitForm => { - use wasm_bindgen::JsCast; - let document = web_sys::window().unwrap().document().unwrap(); - let get_element = |name: &str| { - document - .get_element_by_id(name) - .unwrap() - .dyn_into::() - .unwrap() - .value() - }; - let req = CreateUserRequest { - user_id: get_element("username"), - email: get_element("email"), - display_name: Some(get_element("displayname")), - first_name: Some(get_element("firstname")), - last_name: Some(get_element("lastname")), - password: Some(get_element("password")), - }; - self.create_user(req); - } - Msg::CreateUserResponse(Ok(())) => { - self.route_dispatcher - .send(RouteRequest::ChangeRoute(Route::new_no_state( - "/list_users", - ))); - } - Msg::CreateUserResponse(Err(e)) => { - ConsoleService::warn(&format!("Error listing users: {}", e)); - } + self.error = None; + if let Err(e) = self.handle_msg(msg) { + ConsoleService::error(&e.to_string()); + self.error = Some(e); } true } diff --git a/model/src/lib.rs b/model/src/lib.rs index 1fd861d..a5f693f 100644 --- a/model/src/lib.rs +++ b/model/src/lib.rs @@ -42,6 +42,7 @@ pub mod login { } /// The messages for the 3-step OPAQUE registration process. +/// It is used to reset a user's password. pub mod registration { use super::*; @@ -117,7 +118,6 @@ pub struct CreateUserRequest { pub display_name: Option, pub first_name: Option, pub last_name: Option, - pub password: Option, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] diff --git a/src/domain/sql_backend_handler.rs b/src/domain/sql_backend_handler.rs index ecce733..ca824d3 100644 --- a/src/domain/sql_backend_handler.rs +++ b/src/domain/sql_backend_handler.rs @@ -3,7 +3,6 @@ use crate::infra::configuration::Configuration; use async_trait::async_trait; use futures_util::StreamExt; use futures_util::TryStreamExt; -use lldap_model::opaque; use sea_query::{Expr, Iden, Order, Query, SimpleExpr, Value}; use sqlx::Row; use std::collections::HashSet; @@ -20,33 +19,6 @@ impl SqlBackendHandler { } } -pub fn get_password_file( - clear_password: &str, - server_setup: &opaque::server::ServerSetup, - username: &str, -) -> Result { - use opaque::{client, server}; - let mut rng = rand::rngs::OsRng; - let client_register_start_result = - client::registration::start_registration(clear_password, &mut rng)?; - - let server_register_start_result = server::registration::start_registration( - server_setup, - client_register_start_result.message, - username, - )?; - - let client_registration_result = client::registration::finish_registration( - client_register_start_result.state, - server_register_start_result.message, - &mut rng, - )?; - - Ok(server::registration::get_password_file( - client_registration_result.message, - )) -} - fn get_filter_expr(filter: RequestFilter) -> SimpleExpr { use RequestFilter::*; fn get_repeated_filter( @@ -178,7 +150,7 @@ impl BackendHandler for SqlBackendHandler { } async fn create_user(&self, request: CreateUserRequest) -> Result<()> { - let mut columns = vec![ + let columns = vec![ Users::UserId, Users::Email, Users::DisplayName, @@ -186,7 +158,7 @@ impl BackendHandler for SqlBackendHandler { Users::LastName, Users::CreationDate, ]; - let mut values = vec![ + let values = vec![ request.user_id.clone().into(), request.email.into(), request.display_name.map(Into::into).unwrap_or(Value::Null), @@ -194,14 +166,6 @@ impl BackendHandler for SqlBackendHandler { request.last_name.map(Into::into).unwrap_or(Value::Null), chrono::Utc::now().naive_utc().into(), ]; - if let Some(pass) = request.password { - columns.push(Users::PasswordHash); - values.push( - get_password_file(&pass, self.config.get_server_setup(), &request.user_id)? - .serialize() - .into(), - ); - } let query = Query::insert() .into_table(Users::Table) .columns(columns) @@ -271,12 +235,28 @@ mod tests { } async fn insert_user(handler: &SqlBackendHandler, name: &str, pass: &str) { + use crate::domain::opaque_handler::OpaqueHandler; + insert_user_no_password(handler, name).await; + let mut rng = rand::rngs::OsRng; + let client_registration_start = + opaque::client::registration::start_registration(pass, &mut rng).unwrap(); + let response = handler + .registration_start(registration::ClientRegistrationStartRequest { + username: name.to_string(), + registration_start_request: client_registration_start.message, + }) + .await + .unwrap(); + let registration_upload = opaque::client::registration::finish_registration( + client_registration_start.state, + response.registration_response, + &mut rng, + ) + .unwrap(); handler - .create_user(CreateUserRequest { - user_id: name.to_string(), - email: "bob@bob.bob".to_string(), - password: Some(pass.to_string()), - ..Default::default() + .registration_finish(registration::ClientRegistrationFinishRequest { + server_data: response.server_data, + registration_upload: registration_upload.message, }) .await .unwrap(); diff --git a/src/domain/sql_opaque_handler.rs b/src/domain/sql_opaque_handler.rs index c39f064..998475f 100644 --- a/src/domain/sql_opaque_handler.rs +++ b/src/domain/sql_opaque_handler.rs @@ -55,10 +55,16 @@ impl SqlBackendHandler { .and_where(Expr::col(Users::UserId).eq(username)) .to_string(DbQueryBuilder {}); if let Some(row) = sqlx::query(&query).fetch_optional(&self.sql_pool).await? { - row.get::>, _>(&*Users::PasswordHash.to_string()) - // If no password, always fail. - .ok_or_else(|| DomainError::AuthenticationError(username.to_string()))? + if let Some(bytes) = + row.get::>, _>(&*Users::PasswordHash.to_string()) + { + bytes + } else { + // No password set. + return Ok(None); + } } else { + // No such user. return Ok(None); } }; @@ -119,6 +125,7 @@ impl OpaqueHandler for SqlOpaqueHandler { let maybe_password_file = self.get_password_file_for_user(&request.username).await?; let mut rng = rand::rngs::OsRng; + // Get the CredentialResponse for the user, or a dummy one if no user/no password. let start_response = opaque::server::login::start_login( &mut rng, self.config.get_server_setup(), diff --git a/src/main.rs b/src/main.rs index 99a4eca..258dcf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,6 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration) handler .create_user(lldap_model::CreateUserRequest { user_id: config.ldap_user_dn.clone(), - password: Some(config.ldap_user_pass.clone()), ..Default::default() }) .await