mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
Register clients' passwords using OPAQUE
This commit is contained in:
parent
8b73de0df7
commit
e09c73efce
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)]
|
||||||
|
@ -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();
|
||||||
|
@ -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(),
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user