//! Login flow components // use anyhow::Error; use gloo::console; use kanidm_proto::v1::{ AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState, AuthStep, }; use kanidm_proto::webauthn::PublicKeyCredential; use kanidmd_web_ui_shared::utils::{autofocus, do_footer, window}; use kanidmd_web_ui_shared::{ add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus, }; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::CredentialRequestOptions; use yew::prelude::*; use yew::virtual_dom::VNode; use kanidmd_web_ui_shared::constants::{ CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD, CSS_ALERT_DANGER, URL_USER_HOME, }; use kanidmd_web_ui_shared::models::{ self, clear_bearer_token, get_bearer_token, get_login_hint, pop_login_hint, pop_login_remember_me, pop_return_location, push_login_remember_me, set_bearer_token, }; use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod}; use yew_router::BrowserRouter; #[derive(Clone)] pub struct LoginApp { state: LoginState, } impl Default for LoginApp { fn default() -> Self { Self { state: LoginState::InitLogin { enable: true, remember_me: false, username: String::new(), }, } } } #[derive(PartialEq, Clone, Copy)] pub enum LoginWorkflow { Login, Reauth, } impl std::fmt::Display for LoginWorkflow { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { LoginWorkflow::Login => "LoginWorkflow::Login", LoginWorkflow::Reauth => "LoginWorkflow::Reauth", }) } } impl Default for LoginWorkflow { fn default() -> Self { Self::Login } } #[derive(PartialEq, Properties, Default)] pub struct LoginAppProps { pub workflow: LoginWorkflow, } #[derive(PartialEq, Clone, Copy)] enum TotpState { Enabled, Disabled, Invalid, } #[derive(Clone)] enum LoginState { InitLogin { enable: bool, remember_me: bool, username: String, }, InitReauth { enable: bool, spn: String, }, // Select between different cred types, either password (and MFA) or Passkey Select(Vec<AuthMech>), // The choices of authentication mechanism. Continue(Vec<AuthAllowed>), // The different methods Password(bool), BackupCode(bool), Totp(TotpState), Passkey(CredentialRequestOptions), SecurityKey(CredentialRequestOptions), // Error, state handling. Error { emsg: String, kopid: Option<String>, }, UnknownUser, Denied(String), Authenticated, } pub enum LoginAppMsg { Restart, Begin, PasswordSubmit, BackupCodeSubmit, TotpSubmit, PasskeySubmit(PublicKeyCredential), SecurityKeySubmit(PublicKeyCredential), Start(AuthResponse), Next(AuthResponse), Continue(usize), Select(usize), // DoNothing, UnknownUser, AlreadyAuthenticated, Error { emsg: String, kopid: Option<String> }, } impl From<FetchError> for LoginAppMsg { fn from(fe: FetchError) -> Self { LoginAppMsg::Error { emsg: fe.as_string(), kopid: None, } } } impl From<SessionStatus> for LoginAppMsg { fn from(s: SessionStatus) -> Self { match s { SessionStatus::TokenValid => LoginAppMsg::AlreadyAuthenticated, SessionStatus::LoginRequired => LoginAppMsg::Begin, SessionStatus::Error { emsg, kopid } => LoginAppMsg::Error { emsg, kopid }, } } } impl LoginApp { /// Validate that the current auth token's OK async fn fetch_session_valid() -> Result<LoginAppMsg, FetchError> { fetch_session_valid().await.map(|v| v.into()) } async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> { let authreq = AuthRequest { step: AuthStep::Init2 { username, issue: AuthIssueSession::Token, privileged: false, }, }; let req_jsvalue = serde_json::to_string(&authreq) .map(|s| JsValue::from(&s)) .expect_throw("Failed to serialise authreq"); let (kopid, status, value, _) = do_request("/v1/auth", RequestMethod::POST, Some(req_jsvalue)).await?; if status == 200 { let state: AuthResponse = serde_wasm_bindgen::from_value(value) .expect_throw("Invalid response type - auth_init::AuthResponse"); Ok(LoginAppMsg::Start(state)) } else if status == 404 { console::error!(format!( "User not found: {:?}. Operation ID: {:?}", value.as_string().unwrap_or_default(), kopid )); Ok(LoginAppMsg::UnknownUser) } else { let emsg = value.as_string().unwrap_or_default(); Ok(LoginAppMsg::Error { emsg, kopid }) } } async fn reauth_init() -> Result<LoginAppMsg, FetchError> { let issue = AuthIssueSession::Token; let authreq_jsvalue = serde_json::to_string(&issue) .map(|s| JsValue::from(&s)) .expect_throw("Failed to serialise authreq"); let url = "/v1/reauth"; let (kopid, status, value, _) = do_request(url, RequestMethod::POST, Some(authreq_jsvalue)).await?; if status == 200 { let state: AuthResponse = serde_wasm_bindgen::from_value(value) .expect_throw("Invalid response type during reauth_init::AuthResponse"); Ok(LoginAppMsg::Next(state)) } else if status == 404 { console::error!(format!( "User not found during reauth_init: {:?}. Operation ID: {:?}", value.as_string(), kopid )); Ok(LoginAppMsg::UnknownUser) } else { let emsg = value.as_string().unwrap_or_default(); Ok(LoginAppMsg::Error { emsg, kopid }) } } async fn auth_step(authreq: AuthRequest) -> Result<LoginAppMsg, FetchError> { let authreq_jsvalue = serde_json::to_string(&authreq) .map(|s| JsValue::from(&s)) .expect_throw("Failed to serialise authreq"); let (kopid, status, value, _) = do_request("/v1/auth", RequestMethod::POST, Some(authreq_jsvalue)).await?; if status == 200 { let state: AuthResponse = serde_wasm_bindgen::from_value(value) .map_err(|e| { console::error!(format!("auth_step::AuthResponse: {:?}", e)); e }) .expect_throw("Invalid response type - auth_step::AuthResponse"); Ok(LoginAppMsg::Next(state)) } else { let emsg = value.as_string() .unwrap_or_else(|| "Unhandled error, please report this along with the operation ID below to your administrator. 😔".to_string()); Ok(LoginAppMsg::Error { emsg, kopid }) } } /// Renders the "Start again" button fn button_start_again(&self, ctx: &Context<Self>) -> VNode { html! { <div class="col-md-auto text-center"> <button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } > {" Start Again "} </button> </div> } } fn render_auth_allowed(&self, ctx: &Context<Self>, idx: usize, allow: &AuthAllowed) -> Html { html! { <li class="text-center mb-2"> <button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(move |_| LoginAppMsg::Continue(idx)) } >{ allow.to_string() }</button> </li> } } fn render_mech_select(&self, ctx: &Context<Self>, idx: usize, allow: &AuthMech) -> Html { html! { <li class="text-center mb-2"> <button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(move |_| LoginAppMsg::Select(idx)) } >{ allow.to_string() }</button> </li> } } /// shows an error-alert in a bootstrap alert container fn do_alert_error( &self, alert_title: &str, alert_message: Option<&str>, ctx: &Context<Self>, ) -> VNode { html! { <div class="container"> <div class="row justify-content-md-center"> <div class={CSS_ALERT_DANGER} role="alert"> <p><strong>{ alert_title }</strong></p> if let Some(value) = alert_message { <p>{ value }</p> } </div> { self.button_start_again(ctx) } </div> </div> } } fn view_state(&self, ctx: &Context<Self>) -> Html { match &self.state { LoginState::InitLogin { enable, remember_me, username, } => { let username = username.clone(); html! { <> <div class="container"> <label for="username" class="form-label">{ "Username" }</label> <form id="login" onsubmit={ ctx.link().callback(|e: SubmitEvent| { #[cfg(debug_assertions)] console::debug!("login::view_state -> Init - prevent_default()".to_string()); e.prevent_default(); LoginAppMsg::Begin } ) } > <div class={CLASS_DIV_LOGIN_FIELD}> <input autofocus=true class="autofocus form-control" disabled={ !enable } id="username" name="username" type="text" autocomplete="username" value={ username } required=true /> </div> <div class="mb-3 form-check form-switch"> <input type="checkbox" class="form-check-input" role="switch" id="remember_me_check" disabled={ !enable } checked={ *remember_me } /> <label class="form-check-label" for="remember_me_check">{ "Remember my Username" }</label> </div> <div class={CLASS_DIV_LOGIN_BUTTON}> <button type="submit" class={CLASS_BUTTON_DARK} disabled={ !enable } >{" Begin "}</button> </div> </form> </div> </> } } LoginState::InitReauth { enable, spn } => { let msg = format!("Reauthenticate as {} to continue", spn); html! { <> <div class="container"> <p>{ msg }</p> <form id="login" onsubmit={ ctx.link().callback(|e: SubmitEvent| { #[cfg(debug_assertions)] console::debug!("login::view_state -> Init - prevent_default()".to_string()); e.prevent_default(); LoginAppMsg::Begin } ) } > <div class={CLASS_DIV_LOGIN_BUTTON}> <button type="submit" class="autofocus form-control btn btn-dark" autofocus=true id="begin" disabled={ !enable } >{" Begin "}</button> </div> </form> </div> </> } } // Selecting between password (and MFA) or Passkey LoginState::Select(mechs) => { html! { <> <div class="container"> <p> {" Which credential would you like to use? "} </p> </div> <div class="container"> <ul class="list-unstyled"> { for mechs.iter() .enumerate() .map(|(idx, mech)| self.render_mech_select(ctx, idx, mech)) } </ul> </div> </> } } LoginState::Continue(allowed) => { html! { <> <div class="container"> <p> {"Choose how to proceed:"} </p> </div> <div class="container"> <ul class="list-unstyled"> { for allowed.iter() .enumerate() .map(|(idx, allow)| self.render_auth_allowed(ctx, idx, allow)) } </ul> </div> </> } } LoginState::Password(enable) => { html! { <> <div class="container"> <label for="password" class="form-label">{ "Password" }</label> <form id="login" onsubmit={ ctx.link().callback(|e: SubmitEvent| { console::debug!("login::view_state -> Password - prevent_default()".to_string()); e.prevent_default(); LoginAppMsg::PasswordSubmit } ) } > <div> <input hidden=true type="text" autocomplete="username" /> </div> <div class={CLASS_DIV_LOGIN_FIELD}> <input autofocus=true class="autofocus form-control" disabled={ !enable } id="password" name="password" type="password" autocomplete="current-password" value="" /> </div> <div class={CLASS_DIV_LOGIN_BUTTON}> <button type="submit" class={CLASS_BUTTON_DARK} disabled={ !enable }>{ "Submit" }</button> </div> </form> </div> </> } } LoginState::BackupCode(enable) => { html! { <> <div class="container"> <label for="backup_code" class="form-label"> {"Backup Code"} </label> <form id="login" onsubmit={ ctx.link().callback(|e: SubmitEvent| { console::debug!("login::view_state -> BackupCode - prevent_default()".to_string()); e.prevent_default(); LoginAppMsg::BackupCodeSubmit } ) } > <div class={CLASS_DIV_LOGIN_FIELD}> <input autofocus=true class="autofocus form-control" disabled={ !enable } id="backup_code" name="backup_code" type="text" autocomplete="off" value="" /> </div> <div class={CLASS_DIV_LOGIN_BUTTON}> <button type="submit" class={CLASS_BUTTON_DARK}>{" Submit "}</button> </div> </form> </div> </> } } LoginState::Totp(state) => { html! { <> <div class="container"> <label for="totp" class="form-label">{"TOTP"}</label> <form id="login" onsubmit={ ctx.link().callback(|e: SubmitEvent| { console::debug!("login::view_state -> Totp - prevent_default()".to_string()); e.prevent_default(); LoginAppMsg::TotpSubmit } ) } > <div class={CLASS_DIV_LOGIN_FIELD}> <input autofocus=true class="autofocus form-control" disabled={ state==&TotpState::Disabled } id="totp" name="totp" type="text" autocomplete="off" value="" /> </div> <div class={CLASS_DIV_LOGIN_BUTTON}> <button type="submit" class={CLASS_BUTTON_DARK} disabled={ state==&TotpState::Disabled }>{" Submit "}</button> </div> </form> </div> </> } } LoginState::SecurityKey(challenge) => { // Start the navigator parts. if let Some(win) = web_sys::window() { let promise = win .navigator() .credentials() .get_with_options(challenge) .expect_throw("Unable to create promise"); let fut = JsFuture::from(promise); let linkc = ctx.link().clone(); spawn_local(async move { match fut.await { Ok(data) => { let data = PublicKeyCredential::from( web_sys::PublicKeyCredential::from(data), ); linkc.send_message(LoginAppMsg::SecurityKeySubmit(data)); } Err(e) => { linkc.send_message(LoginAppMsg::Error { emsg: format!("{:?}", e), kopid: None, }); } } }); } else { ctx.link().send_message(LoginAppMsg::Error { emsg: "failed to access navigator credentials".to_string(), kopid: None, }); }; html! { <div class="container"> <p> {"Security Key"} </p> </div> } } LoginState::Passkey(challenge) => { // Start the navigator parts. if let Some(win) = web_sys::window() { let promise = win .navigator() .credentials() .get_with_options(challenge) .expect_throw("Unable to create promise"); let fut = JsFuture::from(promise); let linkc = ctx.link().clone(); spawn_local(async move { match fut.await { Ok(data) => { let data = PublicKeyCredential::from( web_sys::PublicKeyCredential::from(data), ); linkc.send_message(LoginAppMsg::PasskeySubmit(data)); } Err(e) => { linkc.send_message(LoginAppMsg::Error { emsg: format!("{:?}", e), kopid: None, }); } } }); } else { ctx.link().send_message(LoginAppMsg::Error { emsg: "failed to access navigator credentials".to_string(), kopid: None, }); }; html! { <div class="container text-center"> <p> {"Prompting for Passkey authentication..."} </p> </div> } } LoginState::Authenticated => { let loc = pop_return_location(); // redirect to the "return location" #[cfg(debug_assertions)] console::debug!(format!("authenticated, trying to go to {:?}", loc)); let window = gloo_utils::window(); window .location() .set_href(&loc) .expect_throw(&format!("failed to set location to {}", loc)); // this isn't likely to actually render but we might as well... html! { <div class="alert alert-success"> <h3>{ "Login Success 🎉" }</h3> <a href={loc}>{"Click here to continue if you aren't redirected..."}</a> </div> } } LoginState::Denied(msg) => { self.do_alert_error("Authentication Denied", Some(msg.as_str()), ctx) } LoginState::UnknownUser => { self.do_alert_error("Username not found", Some("Please try again"), ctx) } LoginState::Error { emsg, kopid } => self.do_alert_error( "An error has occurred 😔 ", Some( format!( "{}\n\n{}", emsg.as_str(), if let Some(opid) = kopid.as_ref() { format!("Operation ID: {}", opid.clone()) } else { "Error occurred client-side.".to_string() } ) .as_str(), ), ctx, ), } } } impl Component for LoginApp { type Message = LoginAppMsg; type Properties = LoginAppProps; fn create(ctx: &Context<Self>) -> Self { let workflow = ctx.props().workflow.to_owned(); #[cfg(debug_assertions)] console::debug!(&format!("login::create -> workflow: {}", workflow)); let state = match workflow { LoginWorkflow::Login => { // let's check if they're already authenticated! if get_bearer_token().is_some() { ctx.link().send_future(async { match Self::fetch_session_valid().await { Ok(_) => { console::info!( "Already logged in, redirecting to user home page" ); let window = gloo_utils::window(); window .location() .set_href(URL_USER_HOME) .expect_throw(&["failed to set location to ", URL_USER_HOME].concat()); LoginAppMsg::AlreadyAuthenticated } Err(v) => { console::error!( "Error checking session validity, clearing token and returning to login page: {:?}", v.as_string() ); clear_bearer_token(); LoginAppMsg::Restart } } }); } if get_bearer_token().is_some() { // We're already logged in, so we're going to redirect to the apps page. return Self::default(); } // Do we have a login hint? let (username, remember_me) = get_login_hint() .map(|user| (user, false)) .or_else(|| models::get_login_remember_me().map(|user| (user, true))) .unwrap_or_default(); LoginState::InitLogin { enable: true, remember_me, username, } } LoginWorkflow::Reauth => match get_login_hint() { Some(spn) => LoginState::InitReauth { enable: true, spn }, None => LoginState::Error { emsg: "Client Error - No login hint available".to_string(), kopid: None, }, }, }; add_body_form_classes!(); LoginApp { state } } fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool { false } fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { LoginAppMsg::AlreadyAuthenticated => { #[cfg(debug_assertions)] console::info!("User is already authenticated, redirecting to user home"); window().location().set_href(URL_USER_HOME).unwrap_throw(); // no need to render it, we're going away now false } LoginAppMsg::Restart => { // Clear any leftover input. Reset to the remembered username if any. match &ctx.props().workflow { LoginWorkflow::Login => { let (username, remember_me) = get_login_hint() .map(|user| (user, false)) .or_else(|| models::get_login_remember_me().map(|user| (user, true))) .unwrap_or_default(); self.state = LoginState::InitLogin { enable: true, remember_me, username, }; } LoginWorkflow::Reauth => { match get_login_hint() { Some(spn) => { self.state = LoginState::InitReauth { enable: true, spn: spn.clone(), }; LoginState::InitReauth { enable: true, spn } } None => LoginState::Error { emsg: "Client Error - No login hint available".to_string(), kopid: None, }, }; } } true } LoginAppMsg::Begin => { match &ctx.props().workflow { LoginWorkflow::Login => { // Disable the button? let username = utils::get_value_from_element_id("username").unwrap_or_default(); #[cfg(debug_assertions)] console::debug!(format!("begin for username -> {:?}", username)); // If the remember-me was checked, stash it here. // If it was false, clear existing data. let remember_me = if utils::get_inputelement_by_id("remember_me_check") .map(|element| element.checked()) .unwrap_or(false) { push_login_remember_me(username.clone()); true } else { pop_login_remember_me(); false }; #[cfg(debug_assertions)] console::debug!(format!("begin remember_me -> {:?}", remember_me)); let username_clone = username.clone(); ctx.link().send_future(async { match Self::auth_init(username_clone).await { Ok(v) => v, Err(v) => v.into(), } }); self.state = LoginState::InitLogin { enable: false, remember_me, username, }; } LoginWorkflow::Reauth => { ctx.link().send_future(async { match Self::reauth_init().await { Ok(v) => v, Err(v) => v.into(), } }); self.state = match get_login_hint() { Some(spn) => LoginState::InitReauth { enable: false, spn }, None => LoginState::Error { emsg: "Client Error - No login hint available".to_string(), kopid: None, }, }; } } true } LoginAppMsg::PasswordSubmit => { let password = utils::get_value_from_element_id("password").unwrap_or_default(); #[cfg(debug_assertions)] console::debug!("password step".to_string()); // Disable the button? self.state = LoginState::Password(false); let authreq = AuthRequest { step: AuthStep::Cred(AuthCredential::Password(password)), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); true } LoginAppMsg::BackupCodeSubmit => { let backup_code = utils::get_value_from_element_id("backup_code").unwrap_or_default(); #[cfg(debug_assertions)] console::debug!("backup_code".to_string()); // Disable the button? self.state = LoginState::BackupCode(false); let authreq = AuthRequest { step: AuthStep::Cred(AuthCredential::BackupCode(backup_code)), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); true } LoginAppMsg::TotpSubmit => { let totp_str = utils::get_value_from_element_id("totp").unwrap_or_default(); #[cfg(debug_assertions)] console::debug!("totp".to_string()); // Disable the button? match totp_str.parse::<u32>() { Ok(totp) => { self.state = LoginState::Totp(TotpState::Disabled); let authreq = AuthRequest { step: AuthStep::Cred(AuthCredential::Totp(totp)), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); } Err(_) => { self.state = LoginState::Totp(TotpState::Invalid); } } true } LoginAppMsg::SecurityKeySubmit(resp) => { #[cfg(debug_assertions)] console::debug!("At securitykey step".to_string()); let authreq = AuthRequest { step: AuthStep::Cred(AuthCredential::SecurityKey(Box::new(resp))), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); // Do not submit here, we need to wait for the next ui transition. false } LoginAppMsg::PasskeySubmit(resp) => { #[cfg(debug_assertions)] console::debug!("At passkey step".to_string()); let authreq = AuthRequest { step: AuthStep::Cred(AuthCredential::Passkey(Box::new(resp))), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); // Do not submit here, we need to wait for the next ui transition. false } LoginAppMsg::Start(resp) => { // Clear any leftover input #[cfg(debug_assertions)] console::debug!(format!("start -> {:?}", resp)); match resp.state { AuthState::Choose(mut mechs) => { if mechs.len() == 1 { // If it's only one mech, just submit that. let mech = mechs.pop().expect_throw("Memory corruption occurred"); let authreq = AuthRequest { step: AuthStep::Begin(mech), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); // We do NOT need to change state or redraw false } else { #[cfg(debug_assertions)] console::debug!("multiple mechs exist".to_string()); self.state = LoginState::Select(mechs); true } } AuthState::Denied(reason) => { #[cfg(debug_assertions)] console::debug!(format!("denied -> {:?}", reason)); self.state = LoginState::Denied(reason); true } _ => { console::error!("invalid state transition".to_string()); self.state = LoginState::Error { emsg: "Invalid UI State Transition".to_string(), kopid: None, }; true } } } LoginAppMsg::Select(idx) => { #[cfg(debug_assertions)] console::debug!(format!("chose -> {:?}", idx)); match &self.state { LoginState::Select(allowed) => match allowed.get(idx) { Some(mech) => { let authreq = AuthRequest { step: AuthStep::Begin(mech.clone()), }; ctx.link().send_future(async { match Self::auth_step(authreq).await { Ok(v) => v, Err(v) => v.into(), } }); } None => { console::error!("invalid allowed mech idx".to_string()); self.state = LoginState::Error { emsg: "Invalid Continue Index".to_string(), kopid: None, }; } }, _ => { console::error!("invalid state transition".to_string()); self.state = LoginState::Error { emsg: "Invalid UI State Transition".to_string(), kopid: None, }; } }; true } LoginAppMsg::Next(resp) => { // Clear any leftover input #[cfg(debug_assertions)] console::debug!(format!("next -> {:?}", resp)); // Based on the state we have, we need to chose our steps. match resp.state { AuthState::Choose(_mechs) => { console::error!("invalid state transition".to_string()); self.state = LoginState::Error { emsg: "Invalid UI State Transition".to_string(), kopid: None, }; true } AuthState::Continue(mut allowed) => { if allowed.len() == 1 { // If there is only one, change our state for that input type. match allowed.pop().expect_throw("Memory corruption occurred") { AuthAllowed::Anonymous => { // Just submit this. } AuthAllowed::Password => { // Go to the password view. self.state = LoginState::Password(true); } AuthAllowed::BackupCode => { self.state = LoginState::BackupCode(true); } AuthAllowed::Totp => { self.state = LoginState::Totp(TotpState::Enabled); } AuthAllowed::SecurityKey(challenge) => { self.state = LoginState::SecurityKey(challenge.into()) } AuthAllowed::Passkey(challenge) => { self.state = LoginState::Passkey(challenge.into()) } } } else { // Else, present the options in a choice. #[cfg(debug_assertions)] console::debug!("multiple auth method choices exist!"); self.state = LoginState::Continue(allowed); } true } AuthState::Denied(reason) => { console::error!(format!("Authentication denied -> {:?}", reason)); self.state = LoginState::Denied(reason); true } AuthState::Success(bearer_token) => { // Store the bearer here! // We need to format the bearer onto it. #[cfg(debug_assertions)] console::info!( "User has successfully authenticated, setting the bearer token" ); let bearer_token = format!("Bearer {}", bearer_token); set_bearer_token(bearer_token); self.state = LoginState::Authenticated; true } } } LoginAppMsg::Continue(idx) => { // Are we in the correct internal state? #[cfg(debug_assertions)] console::debug!(format!("chose -> {:?}", idx)); match &self.state { LoginState::Continue(allowed) => { match allowed.get(idx) { Some(AuthAllowed::Anonymous) => { // Just submit this. } Some(AuthAllowed::Password) => { // Go to the password view. self.state = LoginState::Password(true); } Some(AuthAllowed::BackupCode) => { self.state = LoginState::BackupCode(true); } Some(AuthAllowed::Totp) => { self.state = LoginState::Totp(TotpState::Enabled); } Some(AuthAllowed::SecurityKey(challenge)) => { self.state = LoginState::SecurityKey(challenge.clone().into()) } Some(AuthAllowed::Passkey(challenge)) => { self.state = LoginState::Passkey(challenge.clone().into()) } None => { console::error!("invalid allowed mech idx".to_string()); self.state = LoginState::Error { emsg: "Invalid Continue Index".to_string(), kopid: None, }; } } } _ => { console::error!("invalid state transition".to_string()); self.state = LoginState::Error { emsg: "Invalid UI State Transition".to_string(), kopid: None, }; } } true } LoginAppMsg::UnknownUser => { // Clear any leftover input console::warn!("Unknown user".to_string()); self.state = LoginState::UnknownUser; true } LoginAppMsg::Error { emsg, kopid } => { // Clear any leftover input console::error!(format!("error -> {:?}, {:?}", emsg, kopid)); self.state = LoginState::Error { emsg, kopid }; true } } } fn view(&self, ctx: &Context<Self>) -> Html { #[cfg(debug_assertions)] console::debug!("login::view".to_string()); html! { <BrowserRouter> <main class="flex-shrink-0 form-signin"> <center> {logo_img()} // TODO: make a call to domain info to show the domain name // or more likely we should have this passed in from the props when we start. <h3>{ "Kanidm" }</h3> </center> // <Switch<LoginRoute> render={switch} /> { self.view_state(ctx) } </main> { do_footer() } </BrowserRouter> } } fn destroy(&mut self, _ctx: &Context<Self>) { #[cfg(debug_assertions)] console::debug!("login::destroy".to_string()); // Done with this, clear it. let _ = pop_login_hint(); remove_body_form_classes!(); } fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) { #[cfg(debug_assertions)] console::debug!("login::rendered".to_string()); // Force autofocus on elements that need it if present. autofocus("username"); autofocus("password"); autofocus("backup_code"); autofocus("otp"); autofocus("begin"); } }