kanidm/server/web_ui/login_flows/src/components.rs
James Hodgkinson e02328ae8b
Splitting the SPAs ()
* doing some work for enumerating how the accounts work together
* fixing up build scripts and removing extra things
* making JavaScript as_tag use the struct field names
* making shared.js a module, removing wasmloader.js
* don't compress compressed things
2023-10-27 06:03:58 +00:00

1163 lines
46 KiB
Rust

//! 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");
}
}