Tidy the reauth ui (#3130)

* Tidy the reauth ui
This commit is contained in:
Firstyear 2024-10-23 11:59:05 +10:00 committed by GitHub
parent 8b4d0d6ead
commit 48cd6638fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 114 additions and 45 deletions

View file

@ -23,6 +23,7 @@ use kanidmd_lib::idm::AuthState;
use kanidmd_lib::prelude::OperationError; use kanidmd_lib::prelude::OperationError;
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use webauthn_rs::prelude::PublicKeyCredential; use webauthn_rs::prelude::PublicKeyCredential;
@ -45,10 +46,33 @@ struct SessionContext {
after_auth_loc: Option<String>, after_auth_loc: Option<String>,
} }
pub enum ReauthPurpose {
ProfileSettings,
}
impl fmt::Display for ReauthPurpose {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReauthPurpose::ProfileSettings => write!(f, "Profile and Settings"),
}
}
}
pub struct Reauth {
pub username: String,
pub purpose: ReauthPurpose,
}
pub struct LoginDisplayCtx {
pub domain_info: DomainInfoRead,
// We only need this on the first re-auth screen to indicate what we are doing
pub reauth: Option<Reauth>,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "login.html")] #[template(path = "login.html")]
struct LoginView { struct LoginView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
username: String, username: String,
remember_me: bool, remember_me: bool,
} }
@ -61,7 +85,7 @@ pub struct Mech<'a> {
#[derive(Template)] #[derive(Template)]
#[template(path = "login_mech_choose.html")] #[template(path = "login_mech_choose.html")]
struct LoginMechView<'a> { struct LoginMechView<'a> {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
mechs: Vec<Mech<'a>>, mechs: Vec<Mech<'a>>,
} }
@ -72,10 +96,10 @@ enum LoginTotpError {
Syntax, Syntax,
} }
#[derive(Template, Default)] #[derive(Template)]
#[template(path = "login_totp.html")] #[template(path = "login_totp.html")]
struct LoginTotpView { struct LoginTotpView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
totp: String, totp: String,
errors: LoginTotpError, errors: LoginTotpError,
} }
@ -83,30 +107,30 @@ struct LoginTotpView {
#[derive(Template)] #[derive(Template)]
#[template(path = "login_password.html")] #[template(path = "login_password.html")]
struct LoginPasswordView { struct LoginPasswordView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
password: String, password: String,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "login_backupcode.html")] #[template(path = "login_backupcode.html")]
struct LoginBackupCodeView { struct LoginBackupCodeView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "login_webauthn.html")] #[template(path = "login_webauthn.html")]
struct LoginWebauthnView { struct LoginWebauthnView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
// Control if we are rendering in security key or passkey mode. // Control if we are rendering in security key or passkey mode.
passkey: bool, passkey: bool,
// chal: RequestChallengeResponse, // chal: RequestChallengeResponse,
chal: String, chal: String,
} }
#[derive(Template, Default)] #[derive(Template)]
#[template(path = "login_denied.html")] #[template(path = "login_denied.html")]
struct LoginDeniedView { struct LoginDeniedView {
domain_custom_image: bool, display_ctx: LoginDisplayCtx,
reason: String, reason: String,
operation_id: Uuid, operation_id: Uuid,
} }
@ -142,7 +166,7 @@ pub async fn view_reauth_get(
kopid: KOpId, kopid: KOpId,
jar: CookieJar, jar: CookieJar,
return_location: &str, return_location: &str,
domain_info: DomainInfoRead, display_ctx: LoginDisplayCtx,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let session_valid_result = state let session_valid_result = state
.qe_r_ref .qe_r_ref
@ -179,7 +203,7 @@ pub async fn view_reauth_get(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
domain_info, display_ctx,
) )
.await .await
{ {
@ -209,10 +233,8 @@ pub async fn view_reauth_get(
let remember_me = !username.is_empty(); let remember_me = !username.is_empty();
let domain_custom_image = domain_info.image().is_some();
HtmlTemplate(LoginView { HtmlTemplate(LoginView {
domain_custom_image, display_ctx,
username, username,
remember_me, remember_me,
}) })
@ -255,10 +277,13 @@ pub async fn view_index_get(
let remember_me = !username.is_empty(); let remember_me = !username.is_empty();
let domain_custom_image = domain_info.image().is_some(); let display_ctx = LoginDisplayCtx {
domain_info,
reauth: None,
};
HtmlTemplate(LoginView { HtmlTemplate(LoginView {
domain_custom_image, display_ctx,
username, username,
remember_me, remember_me,
}) })
@ -328,6 +353,11 @@ pub async fn view_login_begin_post(
after_auth_loc: None, after_auth_loc: None,
}; };
let display_ctx = LoginDisplayCtx {
domain_info,
reauth: None,
};
// Now process the response if ok. // Now process the response if ok.
match inter { match inter {
Ok(ar) => { Ok(ar) => {
@ -338,7 +368,7 @@ pub async fn view_login_begin_post(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
domain_info, display_ctx,
) )
.await .await
{ {
@ -393,6 +423,11 @@ pub async fn view_login_mech_choose_post(
) )
.await; .await;
let display_ctx = LoginDisplayCtx {
domain_info,
reauth: None,
};
// Now process the response if ok. // Now process the response if ok.
match inter { match inter {
Ok(ar) => { Ok(ar) => {
@ -403,7 +438,7 @@ pub async fn view_login_mech_choose_post(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
domain_info, display_ctx,
) )
.await .await
{ {
@ -440,10 +475,14 @@ pub async fn view_login_totp_post(
) -> Response { ) -> Response {
// trim leading and trailing white space. // trim leading and trailing white space.
let Ok(totp) = u32::from_str(login_totp_form.totp.trim()) else { let Ok(totp) = u32::from_str(login_totp_form.totp.trim()) else {
let domain_custom_image = domain_info.image().is_some(); let display_ctx = LoginDisplayCtx {
domain_info,
reauth: None,
};
// If not an int, we need to re-render with an error // If not an int, we need to re-render with an error
return HtmlTemplate(LoginTotpView { return HtmlTemplate(LoginTotpView {
domain_custom_image, display_ctx,
totp: String::default(), totp: String::default(),
errors: LoginTotpError::Syntax, errors: LoginTotpError::Syntax,
}) })
@ -540,6 +579,11 @@ async fn credential_step(
cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID) cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID)
.unwrap_or_default(); .unwrap_or_default();
let display_ctx = LoginDisplayCtx {
domain_info,
reauth: None,
};
let inter = state // This may change in the future ... let inter = state // This may change in the future ...
.qe_r_ref .qe_r_ref
.handle_auth( .handle_auth(
@ -562,7 +606,7 @@ async fn credential_step(
ar, ar,
client_auth_info, client_auth_info,
session_context, session_context,
domain_info, display_ctx,
) )
.await .await
{ {
@ -591,7 +635,7 @@ async fn view_login_step(
auth_result: AuthResult, auth_result: AuthResult,
client_auth_info: ClientAuthInfo, client_auth_info: ClientAuthInfo,
mut session_context: SessionContext, mut session_context: SessionContext,
domain_info: DomainInfoRead, display_ctx: LoginDisplayCtx,
) -> Result<Response, OperationError> { ) -> Result<Response, OperationError> {
trace!(?auth_result); trace!(?auth_result);
@ -601,8 +645,7 @@ async fn view_login_step(
} = auth_result; } = auth_result;
session_context.id = Some(sessionid); session_context.id = Some(sessionid);
let domain_custom_image = domain_info.image().is_some(); // This lets us break out the loop incase of a fault. Take that halting problem!
let mut safety = 3; let mut safety = 3;
// Unlike the api version, only set the cookie. // Unlike the api version, only set the cookie.
@ -662,11 +705,7 @@ async fn view_login_step(
name: m, name: m,
}) })
.collect(); .collect();
HtmlTemplate(LoginMechView { HtmlTemplate(LoginMechView { display_ctx, mechs }).into_response()
domain_custom_image,
mechs,
})
.into_response()
} }
}; };
// break acts as return in a loop. // break acts as return in a loop.
@ -693,24 +732,24 @@ async fn view_login_step(
match auth_allowed { match auth_allowed {
AuthAllowed::Totp => HtmlTemplate(LoginTotpView { AuthAllowed::Totp => HtmlTemplate(LoginTotpView {
display_ctx,
totp: session_context.totp.clone().unwrap_or_default(), totp: session_context.totp.clone().unwrap_or_default(),
..Default::default() errors: LoginTotpError::default(),
}) })
.into_response(), .into_response(),
AuthAllowed::Password => HtmlTemplate(LoginPasswordView { AuthAllowed::Password => HtmlTemplate(LoginPasswordView {
domain_custom_image, display_ctx,
password: session_context.password.clone().unwrap_or_default(), password: session_context.password.clone().unwrap_or_default(),
}) })
.into_response(), .into_response(),
AuthAllowed::BackupCode => HtmlTemplate(LoginBackupCodeView { AuthAllowed::BackupCode => {
domain_custom_image, HtmlTemplate(LoginBackupCodeView { display_ctx }).into_response()
}) }
.into_response(),
AuthAllowed::SecurityKey(chal) => { AuthAllowed::SecurityKey(chal) => {
let chal_json = serde_json::to_string(&chal) let chal_json = serde_json::to_string(&chal)
.map_err(|_| OperationError::SerdeJsonError)?; .map_err(|_| OperationError::SerdeJsonError)?;
HtmlTemplate(LoginWebauthnView { HtmlTemplate(LoginWebauthnView {
domain_custom_image, display_ctx,
passkey: false, passkey: false,
chal: chal_json, chal: chal_json,
}) })
@ -720,7 +759,7 @@ async fn view_login_step(
let chal_json = serde_json::to_string(&chal) let chal_json = serde_json::to_string(&chal)
.map_err(|_| OperationError::SerdeJsonError)?; .map_err(|_| OperationError::SerdeJsonError)?;
HtmlTemplate(LoginWebauthnView { HtmlTemplate(LoginWebauthnView {
domain_custom_image, display_ctx,
passkey: true, passkey: true,
chal: chal_json, chal: chal_json,
}) })
@ -798,7 +837,7 @@ async fn view_login_step(
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID)); jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
break HtmlTemplate(LoginDeniedView { break HtmlTemplate(LoginDeniedView {
domain_custom_image, display_ctx,
reason, reason,
operation_id: kopid.eventid, operation_id: kopid.eventid,
}) })

View file

@ -1,6 +1,7 @@
use crate::https::extractors::{DomainInfo, VerifiedClientInformation}; use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
use crate::https::middleware::KOpId; use crate::https::middleware::KOpId;
use crate::https::views::errors::HtmxError; use crate::https::views::errors::HtmxError;
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
use crate::https::views::HtmlTemplate; use crate::https::views::HtmlTemplate;
use crate::https::ServerState; use crate::https::ServerState;
use askama::Template; use askama::Template;
@ -81,13 +82,27 @@ pub(crate) async fn view_profile_unlock_get(
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
jar: CookieJar, jar: CookieJar,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let uat: UserAuthToken = state
.qe_r_ref
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
.map_err(|op_err| HtmxError::new(&kopid, op_err))
.await?;
let display_ctx = LoginDisplayCtx {
domain_info,
reauth: Some(Reauth {
username: uat.spn,
purpose: ReauthPurpose::ProfileSettings,
}),
};
super::login::view_reauth_get( super::login::view_reauth_get(
state, state,
client_auth_info, client_auth_info,
kopid, kopid,
jar, jar,
"/ui/profile", "/ui/profile",
domain_info, display_ctx,
) )
.await .await
} }

View file

@ -28,6 +28,7 @@ use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInforma
use crate::https::middleware::KOpId; use crate::https::middleware::KOpId;
use crate::https::views::constants::ProfileMenuItems; use crate::https::views::constants::ProfileMenuItems;
use crate::https::views::errors::HtmxError; use crate::https::views::errors::HtmxError;
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
use crate::https::ServerState; use crate::https::ServerState;
use super::{HtmlTemplate, UnrecoverableErrorView}; use super::{HtmlTemplate, UnrecoverableErrorView};
@ -612,13 +613,21 @@ pub(crate) async fn view_self_reset_get(
jar = add_cu_cookie(jar, &state, cu_session_token); jar = add_cu_cookie(jar, &state, cu_session_token);
Ok((jar, cu_resp).into_response()) Ok((jar, cu_resp).into_response())
} else { } else {
let display_ctx = LoginDisplayCtx {
domain_info,
reauth: Some(Reauth {
username: uat.spn,
purpose: ReauthPurpose::ProfileSettings,
}),
};
super::login::view_reauth_get( super::login::view_reauth_get(
state, state,
client_auth_info, client_auth_info,
kopid, kopid,
jar, jar,
"/ui/update_credentials", "/ui/update_credentials",
domain_info, display_ctx,
) )
.await .await
} }

View file

@ -271,7 +271,6 @@ body {
#cred-update-commit-bar { #cred-update-commit-bar {
display: block; display: block;
position: fixed; position: fixed;
bottom: 5%;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: white; background: white;

View file

@ -112,10 +112,12 @@
(% let primary_state = primary_state %) (% let primary_state = primary_state %)
(% include "credentials_update_primary.html" %) (% include "credentials_update_primary.html" %)
<hr class="my-4" />
<div id="cred-update-commit-bar" class="toast" role="alert" <div id="cred-update-commit-bar" class="toast" role="alert"
aria-live="assertive" aria-atomic="true"> aria-live="assertive" aria-atomic="true">
<div class="toast-body"> <div class="toast-body">
<span class="d-flex align-items-center"> <span class="d-flex align-items-center">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" <svg xmlns="http://www.w3.org/2000/svg" width="16"
height="16" fill="currentColor" height="16" fill="currentColor"
class="bi bi-floppy2-fill" viewBox="0 0 16 16"> class="bi bi-floppy2-fill" viewBox="0 0 16 16">
@ -123,15 +125,15 @@
<path <path
d="M1.5 0A1.5 1.5 0 0 0 0 1.5v13A1.5 1.5 0 0 0 1.5 16h13a1.5 1.5 0 0 0 1.5-1.5V2.914a1.5 1.5 0 0 0-.44-1.06L14.147.439A1.5 1.5 0 0 0 13.086 0zM4 6a1 1 0 0 1-1-1V1h10v4a1 1 0 0 1-1 1zM3 9h10a1 1 0 0 1 1 1v5H2v-5a1 1 0 0 1 1-1" /> d="M1.5 0A1.5 1.5 0 0 0 0 1.5v13A1.5 1.5 0 0 0 1.5 16h13a1.5 1.5 0 0 0 1.5-1.5V2.914a1.5 1.5 0 0 0-.44-1.06L14.147.439A1.5 1.5 0 0 0 13.086 0zM4 6a1 1 0 0 1-1-1V1h10v4a1 1 0 0 1-1 1zM3 9h10a1 1 0 0 1 1 1v5H2v-5a1 1 0 0 1 1-1" />
</svg> </svg>
<b class="px-1">Careful</b>- save when you're done: <b>Careful</b> - Unsaved changes will be lost</div>
</span> </span>
<div class="mt-2 pt-2 border-top"> <div class="mt-2 pt-2 border-top">
<button class="btn btn-danger" <button class="btn btn-danger"
hx-post="/ui/api/cu_cancel" hx-post="/ui/api/cu_cancel"
hx-target="body">Cancel</button> hx-target="body">Discard Changes</button>
<span class="d-inline-block" tabindex="0" <span class="d-inline-block" tabindex="0"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="Resolve the warnings at the top."> data-bs-title="Unresolved warnings">
<button <button
class="btn btn-success" class="btn btn-success"
type="submit" type="submit"

View file

@ -8,7 +8,7 @@
(% block body %) (% block body %)
<main id="main" class="flex-shrink-0 form-signin"> <main id="main" class="flex-shrink-0 form-signin">
<center> <center>
(% if domain_custom_image %) (% if display_ctx.domain_info.image().is_some() %)
<img src="/ui/images/domain" <img src="/ui/images/domain"
alt="Kanidm" class="kanidm_logo" /> alt="Kanidm" class="kanidm_logo" />
(% else %) (% else %)
@ -17,6 +17,9 @@
alt="Kanidm" class="kanidm_logo" /> alt="Kanidm" class="kanidm_logo" />
(% endif %) (% endif %)
<h3>Kanidm</h3> <h3>Kanidm</h3>
(% if let Some(reauth) = display_ctx.reauth %)
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose ))</h5>
(% endif %)
</center> </center>
<div id="login-form-container" class="container"> <div id="login-form-container" class="container">
(% block logincontainer %) (% block logincontainer %)

View file

@ -12,6 +12,7 @@
src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))" src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
defer></script> defer></script>
<div class="identity-verification-container">
(% if passkey %) (% if passkey %)
<form id="cred-form" action="/ui/login/passkey" method="POST"> <form id="cred-form" action="/ui/login/passkey" method="POST">
<input hidden="hidden" name="cred" id="cred"> <input hidden="hidden" name="cred" id="cred">
@ -27,5 +28,6 @@
>Use Security Key</button> >Use Security Key</button>
</form> </form>
(% endif %) (% endif %)
</div>
(% endblock %) (% endblock %)