Register clients' passwords using OPAQUE

This commit is contained in:
Valentin Tolmer 2021-06-24 18:23:23 +02:00 committed by nitnelave
parent 8b73de0df7
commit e09c73efce
6 changed files with 199 additions and 107 deletions

View File

@ -76,40 +76,48 @@ where
FetchService::fetch_with_options(request, get_default_options(), handler) FetchService::fetch_with_options(request, get_default_options(), handler)
} }
fn call_server_json_with_error_message<CallbackResult, RB, Req>(
url: &str,
request: RB,
callback: Callback<Result<CallbackResult>>,
error_message: &'static str,
) -> Result<FetchTask>
where
CallbackResult: serde::de::DeserializeOwned + 'static,
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
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 { impl HostService {
pub fn list_users( pub fn list_users(
request: ListUsersRequest, request: ListUsersRequest,
callback: Callback<Result<Vec<User>>>, callback: Callback<Result<Vec<User>>>,
) -> Result<FetchTask> { ) -> Result<FetchTask> {
call_server("/api/users", &request, callback, |status, data: String| { call_server_json_with_error_message("/api/users", &request, callback, "")
if status.is_success() {
serde_json::from_str(&data).map_err(|e| anyhow!("Could not parse response: {}", e))
} else {
Err(anyhow!("[{}]: {}", status, data))
}
})
} }
pub fn login_start( pub fn login_start(
request: login::ClientLoginStartRequest, request: login::ClientLoginStartRequest,
callback: Callback<Result<Box<login::ServerLoginStartResponse>>>, callback: Callback<Result<Box<login::ServerLoginStartResponse>>>,
) -> Result<FetchTask> { ) -> Result<FetchTask> {
call_server( call_server_json_with_error_message(
"/auth/opaque/login/start", "/auth/opaque/login/start",
&request, &request,
callback, callback,
|status, data: String| { "Could not start authentication: ",
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
))
}
},
) )
} }
@ -141,6 +149,36 @@ impl HostService {
) )
} }
pub fn register_start(
request: registration::ClientRegistrationStartRequest,
callback: Callback<Result<Box<registration::ServerRegistrationStartResponse>>>,
) -> Result<FetchTask> {
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<()>>,
) -> Result<FetchTask> {
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<()>>) -> Result<FetchTask> { pub fn logout(callback: Callback<Result<()>>) -> Result<FetchTask> {
call_server( call_server(
"/auth/logout", "/auth/logout",

View File

@ -1,5 +1,5 @@
use crate::api::HostService; use crate::api::HostService;
use anyhow::Result; use anyhow::{anyhow, Result};
use lldap_model::*; use lldap_model::*;
use yew::prelude::*; use yew::prelude::*;
use yew::services::{fetch::FetchTask, ConsoleService}; use yew::services::{fetch::FetchTask, ConsoleService};
@ -13,6 +13,7 @@ pub struct CreateUserForm {
route_dispatcher: RouteAgentDispatcher, route_dispatcher: RouteAgentDispatcher,
node_ref: NodeRef, node_ref: NodeRef,
error: Option<anyhow::Error>, error: Option<anyhow::Error>,
registration_start: Option<opaque::client::registration::ClientRegistration>,
// Used to keep the request alive long enough. // Used to keep the request alive long enough.
_task: Option<FetchTask>, _task: Option<FetchTask>,
} }
@ -20,19 +21,112 @@ pub struct CreateUserForm {
pub enum Msg { pub enum Msg {
CreateUserResponse(Result<()>), CreateUserResponse(Result<()>),
SubmitForm, SubmitForm,
SuccessfulCreation,
RegistrationStartResponse(Result<Box<registration::ServerRegistrationStartResponse>>),
RegistrationFinishResponse(Result<()>),
}
#[allow(clippy::ptr_arg)]
fn not_empty(s: &String) -> bool {
!s.is_empty()
} }
impl CreateUserForm { impl CreateUserForm {
fn create_user(&mut self, req: CreateUserRequest) { fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<()> {
match HostService::create_user(req, self.link.callback(Msg::CreateUserResponse)) { match msg {
Ok(task) => self._task = Some(task), Msg::SubmitForm => {
Err(e) => { let req = CreateUserRequest {
self._task = None; user_id: get_element("username")
ConsoleService::log(format!("Error trying to create user: {}", e).as_str()) .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<String> {
use wasm_bindgen::JsCast;
Some(
web_sys::window()?
.document()?
.get_element_by_id(name)?
.dyn_into::<web_sys::HtmlInputElement>()
.ok()?
.value(),
)
}
impl Component for CreateUserForm { impl Component for CreateUserForm {
type Message = Msg; type Message = Msg;
@ -44,42 +138,16 @@ impl Component for CreateUserForm {
route_dispatcher: RouteAgentDispatcher::new(), route_dispatcher: RouteAgentDispatcher::new(),
node_ref: NodeRef::default(), node_ref: NodeRef::default(),
error: None, error: None,
registration_start: None,
_task: None, _task: None,
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg { self.error = None;
Msg::SubmitForm => { if let Err(e) = self.handle_msg(msg) {
use wasm_bindgen::JsCast; ConsoleService::error(&e.to_string());
let document = web_sys::window().unwrap().document().unwrap(); self.error = Some(e);
let get_element = |name: &str| {
document
.get_element_by_id(name)
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()
.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));
}
} }
true true
} }

View File

@ -42,6 +42,7 @@ pub mod login {
} }
/// The messages for the 3-step OPAQUE registration process. /// The messages for the 3-step OPAQUE registration process.
/// It is used to reset a user's password.
pub mod registration { pub mod registration {
use super::*; use super::*;
@ -117,7 +118,6 @@ pub struct CreateUserRequest {
pub display_name: Option<String>, pub display_name: Option<String>,
pub first_name: Option<String>, pub first_name: Option<String>,
pub last_name: Option<String>, pub last_name: Option<String>,
pub password: Option<String>,
} }
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]

View File

@ -3,7 +3,6 @@ use crate::infra::configuration::Configuration;
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::StreamExt; use futures_util::StreamExt;
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use lldap_model::opaque;
use sea_query::{Expr, Iden, Order, Query, SimpleExpr, Value}; use sea_query::{Expr, Iden, Order, Query, SimpleExpr, Value};
use sqlx::Row; use sqlx::Row;
use std::collections::HashSet; 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<opaque::server::ServerRegistration> {
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 { fn get_filter_expr(filter: RequestFilter) -> SimpleExpr {
use RequestFilter::*; use RequestFilter::*;
fn get_repeated_filter( fn get_repeated_filter(
@ -178,7 +150,7 @@ impl BackendHandler for SqlBackendHandler {
} }
async fn create_user(&self, request: CreateUserRequest) -> Result<()> { async fn create_user(&self, request: CreateUserRequest) -> Result<()> {
let mut columns = vec![ let columns = vec![
Users::UserId, Users::UserId,
Users::Email, Users::Email,
Users::DisplayName, Users::DisplayName,
@ -186,7 +158,7 @@ impl BackendHandler for SqlBackendHandler {
Users::LastName, Users::LastName,
Users::CreationDate, Users::CreationDate,
]; ];
let mut values = vec![ let values = vec![
request.user_id.clone().into(), request.user_id.clone().into(),
request.email.into(), request.email.into(),
request.display_name.map(Into::into).unwrap_or(Value::Null), 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), request.last_name.map(Into::into).unwrap_or(Value::Null),
chrono::Utc::now().naive_utc().into(), 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() let query = Query::insert()
.into_table(Users::Table) .into_table(Users::Table)
.columns(columns) .columns(columns)
@ -271,12 +235,28 @@ mod tests {
} }
async fn insert_user(handler: &SqlBackendHandler, name: &str, pass: &str) { 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 handler
.create_user(CreateUserRequest { .registration_finish(registration::ClientRegistrationFinishRequest {
user_id: name.to_string(), server_data: response.server_data,
email: "bob@bob.bob".to_string(), registration_upload: registration_upload.message,
password: Some(pass.to_string()),
..Default::default()
}) })
.await .await
.unwrap(); .unwrap();

View File

@ -55,10 +55,16 @@ impl SqlBackendHandler {
.and_where(Expr::col(Users::UserId).eq(username)) .and_where(Expr::col(Users::UserId).eq(username))
.to_string(DbQueryBuilder {}); .to_string(DbQueryBuilder {});
if let Some(row) = sqlx::query(&query).fetch_optional(&self.sql_pool).await? { if let Some(row) = sqlx::query(&query).fetch_optional(&self.sql_pool).await? {
row.get::<Option<Vec<u8>>, _>(&*Users::PasswordHash.to_string()) if let Some(bytes) =
// If no password, always fail. row.get::<Option<Vec<u8>>, _>(&*Users::PasswordHash.to_string())
.ok_or_else(|| DomainError::AuthenticationError(username.to_string()))? {
bytes
} else {
// No password set.
return Ok(None);
}
} else { } else {
// No such user.
return Ok(None); 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 maybe_password_file = self.get_password_file_for_user(&request.username).await?;
let mut rng = rand::rngs::OsRng; 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( let start_response = opaque::server::login::start_login(
&mut rng, &mut rng,
self.config.get_server_setup(), self.config.get_server_setup(),

View File

@ -17,7 +17,6 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration)
handler handler
.create_user(lldap_model::CreateUserRequest { .create_user(lldap_model::CreateUserRequest {
user_id: config.ldap_user_dn.clone(), user_id: config.ldap_user_dn.clone(),
password: Some(config.ldap_user_pass.clone()),
..Default::default() ..Default::default()
}) })
.await .await