Implement opaque login flow in the client

This commit is contained in:
Valentin Tolmer 2021-06-23 09:40:04 +02:00 committed by nitnelave
parent 4d68a2a015
commit 659eab3706
4 changed files with 121 additions and 42 deletions

1
app/Cargo.lock generated
View File

@ -754,6 +754,7 @@ dependencies = [
"http", "http",
"jwt", "jwt",
"lldap_model", "lldap_model",
"rand 0.8.4",
"serde", "serde",
"serde_json", "serde_json",
"wasm-bindgen", "wasm-bindgen",

View File

@ -9,6 +9,7 @@ anyhow = "1"
chrono = "*" chrono = "*"
http = "0.2.4" http = "0.2.4"
jwt = "0.13" jwt = "0.13"
rand = "0.8"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"

View File

@ -28,7 +28,6 @@ fn create_handler<Resp, CallbackResult, F>(
) -> Callback<Response<Result<Resp>>> ) -> Callback<Response<Result<Resp>>>
where where
F: Fn(http::StatusCode, Resp) -> Result<CallbackResult> + 'static, F: Fn(http::StatusCode, Resp) -> Result<CallbackResult> + 'static,
Resp: std::fmt::Display,
CallbackResult: 'static, CallbackResult: 'static,
{ {
Callback::once(move |response: Response<Result<Resp>>| { Callback::once(move |response: Response<Result<Resp>>| {
@ -59,11 +58,33 @@ impl HostService {
FetchService::fetch_with_options(request, get_default_options(), handler) FetchService::fetch_with_options(request, get_default_options(), handler)
} }
pub fn authenticate( pub fn login_start(
request: BindRequest, request: login::ClientLoginStartRequest,
callback: Callback<Result<login::ServerLoginStartResponse>>,
) -> Result<FetchTask> {
let url = "/auth/opaque/login/start";
let request = Request::post(url)
.header("Content-Type", "application/json")
.body(Json(&request))?;
let handler = create_handler(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
))
}
});
FetchService::fetch_with_options(request, get_default_options(), handler)
}
pub fn login_finish(
request: login::ClientLoginFinishRequest,
callback: Callback<Result<String>>, callback: Callback<Result<String>>,
) -> Result<FetchTask> { ) -> Result<FetchTask> {
let url = "/auth"; let url = "/auth/opaque/login/finish";
let request = Request::post(url) let request = Request::post(url)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Json(&request))?; .body(Json(&request))?;
@ -76,10 +97,12 @@ impl HostService {
.map(|_| jwt_claims.user.clone()) .map(|_| jwt_claims.user.clone())
.map_err(|e| anyhow!("Error clearing cookie: {}", e)) .map_err(|e| anyhow!("Error clearing cookie: {}", e))
}) })
} else if status == 401 {
Err(anyhow!("Invalid username or password"))
} else { } else {
Err(anyhow!("Could not authenticate: [{}]: {}", status, data)) Err(anyhow!(
"Could not finish authentication: [{}]: {}",
status,
data
))
} }
}); });
FetchService::fetch_with_options(request, get_default_options(), handler) FetchService::fetch_with_options(request, get_default_options(), handler)

View File

@ -11,6 +11,7 @@ pub struct LoginForm {
on_logged_in: Callback<String>, on_logged_in: Callback<String>,
error: Option<anyhow::Error>, error: Option<anyhow::Error>,
node_ref: NodeRef, node_ref: NodeRef,
login_start: Option<opaque::client::login::ClientLogin>,
// Used to keep the request alive long enough. // Used to keep the request alive long enough.
_task: Option<FetchTask>, _task: Option<FetchTask>,
} }
@ -22,7 +23,19 @@ pub struct Props {
pub enum Msg { pub enum Msg {
Submit, Submit,
AuthenticationResponse(Result<String>), AuthenticationStartResponse(Result<login::ServerLoginStartResponse>),
AuthenticationFinishResponse(Result<String>),
}
fn get_form_field(field_id: &str) -> Option<String> {
let document = web_sys::window()?.document()?;
Some(
document
.get_element_by_id(field_id)?
.dyn_into::<web_sys::HtmlInputElement>()
.ok()?
.value(),
)
} }
impl LoginForm { impl LoginForm {
@ -30,6 +43,76 @@ impl LoginForm {
ConsoleService::error(&error.to_string()); ConsoleService::error(&error.to_string());
self.error = Some(error); self.error = Some(error);
} }
fn call_backend<M, Req, C, Resp>(&mut self, method: M, req: Req, callback: C) -> Result<()>
where
M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
C: Fn(Resp) -> <Self as Component>::Message + 'static,
{
self._task = Some(method(req, self.link.callback(callback))?);
Ok(())
}
fn handle_message(&mut self, msg: <Self as Component>::Message) -> Result<()> {
match msg {
Msg::Submit => {
let username = get_form_field("username")
.ok_or(anyhow!("Could not get username from form"))?;
let password = get_form_field("password")
.ok_or(anyhow!("Could not get password from form"))?;
let mut rng = rand::rngs::OsRng;
let login_start_request =
opaque::client::login::start_login(&password, &mut rng)
.map_err(|e| anyhow!("Could not initialize login: {}", e))?;
self.login_start = Some(login_start_request.state);
let req = login::ClientLoginStartRequest {
username,
login_start_request: login_start_request.message,
};
self.call_backend(
HostService::login_start,
req,
Msg::AuthenticationStartResponse,
)?;
Ok(())
}
Msg::AuthenticationStartResponse(Ok(res)) => {
debug_assert!(self.login_start.is_some());
let login_finish = match opaque::client::login::finish_login(
self.login_start.as_ref().unwrap().clone(),
res.credential_response,
) {
Err(e) => {
// Common error, we want to print a full error to the console but only a
// simple one to the user.
ConsoleService::error(&format!("Invalid username or password: {}", e));
self.error = Some(anyhow!("Invalid username or password"));
return Ok(());
}
Ok(l) => l,
};
let req = login::ClientLoginFinishRequest {
login_key: res.login_key,
credential_finalization: login_finish.message,
};
self.call_backend(
HostService::login_finish,
req,
Msg::AuthenticationFinishResponse,
)?;
Ok(())
}
Msg::AuthenticationStartResponse(Err(e)) => Err(anyhow!(
"Could not log in (invalid response to login start): {}",
e
)),
Msg::AuthenticationFinishResponse(Ok(user_id)) => {
self.on_logged_in.emit(user_id);
Ok(())
}
Msg::AuthenticationFinishResponse(Err(e)) => Err(anyhow!("Could not log in: {}", e)),
}
}
} }
impl Component for LoginForm { impl Component for LoginForm {
@ -42,45 +125,16 @@ impl Component for LoginForm {
on_logged_in: props.on_logged_in, on_logged_in: props.on_logged_in,
error: None, error: None,
node_ref: NodeRef::default(), node_ref: NodeRef::default(),
login_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::Submit => { if let Err(e) = self.handle_message(msg) {
let document = web_sys::window().unwrap().document().unwrap(); self.set_error(e);
let username = document }
.get_element_by_id("username")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap()
.value();
let password = document
.get_element_by_id("password")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap()
.value();
let req = BindRequest {
name: username,
password,
};
match HostService::authenticate(
req,
self.link.callback(Msg::AuthenticationResponse),
) {
Ok(task) => self._task = Some(task),
Err(e) => self.set_error(e),
}
}
Msg::AuthenticationResponse(Ok(user_id)) => {
self.on_logged_in.emit(user_id);
}
Msg::AuthenticationResponse(Err(e)) => {
self.set_error(anyhow!("Could not log in: {}", e));
}
};
true true
} }