From 659eab370604efd4570563b2ca31cb61dfdec5b0 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 23 Jun 2021 09:40:04 +0200 Subject: [PATCH] Implement opaque login flow in the client --- app/Cargo.lock | 1 + app/Cargo.toml | 1 + app/src/api.rs | 37 +++++++++++--- app/src/login.rs | 124 ++++++++++++++++++++++++++++++++++------------- 4 files changed, 121 insertions(+), 42 deletions(-) diff --git a/app/Cargo.lock b/app/Cargo.lock index 1a1803e..27cc2bb 100644 --- a/app/Cargo.lock +++ b/app/Cargo.lock @@ -754,6 +754,7 @@ dependencies = [ "http", "jwt", "lldap_model", + "rand 0.8.4", "serde", "serde_json", "wasm-bindgen", diff --git a/app/Cargo.toml b/app/Cargo.toml index b7608f2..3a04413 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1" chrono = "*" http = "0.2.4" jwt = "0.13" +rand = "0.8" serde = "1" serde_json = "1" wasm-bindgen = "0.2" diff --git a/app/src/api.rs b/app/src/api.rs index 91d0543..894d1e2 100644 --- a/app/src/api.rs +++ b/app/src/api.rs @@ -28,7 +28,6 @@ fn create_handler( ) -> Callback>> where F: Fn(http::StatusCode, Resp) -> Result + 'static, - Resp: std::fmt::Display, CallbackResult: 'static, { Callback::once(move |response: Response>| { @@ -59,11 +58,33 @@ impl HostService { FetchService::fetch_with_options(request, get_default_options(), handler) } - pub fn authenticate( - request: BindRequest, + pub fn login_start( + request: login::ClientLoginStartRequest, + callback: Callback>, + ) -> Result { + 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 { - let url = "/auth"; + let url = "/auth/opaque/login/finish"; let request = Request::post(url) .header("Content-Type", "application/json") .body(Json(&request))?; @@ -76,10 +97,12 @@ impl HostService { .map(|_| jwt_claims.user.clone()) .map_err(|e| anyhow!("Error clearing cookie: {}", e)) }) - } else if status == 401 { - Err(anyhow!("Invalid username or password")) } 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) diff --git a/app/src/login.rs b/app/src/login.rs index 6cb2655..c809b5c 100644 --- a/app/src/login.rs +++ b/app/src/login.rs @@ -11,6 +11,7 @@ pub struct LoginForm { on_logged_in: Callback, error: Option, node_ref: NodeRef, + login_start: Option, // Used to keep the request alive long enough. _task: Option, } @@ -22,7 +23,19 @@ pub struct Props { pub enum Msg { Submit, - AuthenticationResponse(Result), + AuthenticationStartResponse(Result), + AuthenticationFinishResponse(Result), +} + +fn get_form_field(field_id: &str) -> Option { + let document = web_sys::window()?.document()?; + Some( + document + .get_element_by_id(field_id)? + .dyn_into::() + .ok()? + .value(), + ) } impl LoginForm { @@ -30,6 +43,76 @@ impl LoginForm { ConsoleService::error(&error.to_string()); self.error = Some(error); } + + fn call_backend(&mut self, method: M, req: Req, callback: C) -> Result<()> + where + M: Fn(Req, Callback) -> Result, + C: Fn(Resp) -> ::Message + 'static, + { + self._task = Some(method(req, self.link.callback(callback))?); + Ok(()) + } + + fn handle_message(&mut self, msg: ::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 { @@ -42,45 +125,16 @@ impl Component for LoginForm { on_logged_in: props.on_logged_in, error: None, node_ref: NodeRef::default(), + login_start: None, _task: None, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::Submit => { - let document = web_sys::window().unwrap().document().unwrap(); - let username = document - .get_element_by_id("username") - .unwrap() - .dyn_into::() - .unwrap() - .value(); - let password = document - .get_element_by_id("password") - .unwrap() - .dyn_into::() - .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)); - } - }; + self.error = None; + if let Err(e) = self.handle_message(msg) { + self.set_error(e); + } true }