mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
parent
8b4d0d6ead
commit
48cd6638fe
|
@ -23,6 +23,7 @@ use kanidmd_lib::idm::AuthState;
|
|||
use kanidmd_lib::prelude::OperationError;
|
||||
use kanidmd_lib::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use webauthn_rs::prelude::PublicKeyCredential;
|
||||
|
||||
|
@ -45,10 +46,33 @@ struct SessionContext {
|
|||
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)]
|
||||
#[template(path = "login.html")]
|
||||
struct LoginView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
username: String,
|
||||
remember_me: bool,
|
||||
}
|
||||
|
@ -61,7 +85,7 @@ pub struct Mech<'a> {
|
|||
#[derive(Template)]
|
||||
#[template(path = "login_mech_choose.html")]
|
||||
struct LoginMechView<'a> {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
mechs: Vec<Mech<'a>>,
|
||||
}
|
||||
|
||||
|
@ -72,10 +96,10 @@ enum LoginTotpError {
|
|||
Syntax,
|
||||
}
|
||||
|
||||
#[derive(Template, Default)]
|
||||
#[derive(Template)]
|
||||
#[template(path = "login_totp.html")]
|
||||
struct LoginTotpView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
totp: String,
|
||||
errors: LoginTotpError,
|
||||
}
|
||||
|
@ -83,30 +107,30 @@ struct LoginTotpView {
|
|||
#[derive(Template)]
|
||||
#[template(path = "login_password.html")]
|
||||
struct LoginPasswordView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login_backupcode.html")]
|
||||
struct LoginBackupCodeView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login_webauthn.html")]
|
||||
struct LoginWebauthnView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
// Control if we are rendering in security key or passkey mode.
|
||||
passkey: bool,
|
||||
// chal: RequestChallengeResponse,
|
||||
chal: String,
|
||||
}
|
||||
|
||||
#[derive(Template, Default)]
|
||||
#[derive(Template)]
|
||||
#[template(path = "login_denied.html")]
|
||||
struct LoginDeniedView {
|
||||
domain_custom_image: bool,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
reason: String,
|
||||
operation_id: Uuid,
|
||||
}
|
||||
|
@ -142,7 +166,7 @@ pub async fn view_reauth_get(
|
|||
kopid: KOpId,
|
||||
jar: CookieJar,
|
||||
return_location: &str,
|
||||
domain_info: DomainInfoRead,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
) -> axum::response::Result<Response> {
|
||||
let session_valid_result = state
|
||||
.qe_r_ref
|
||||
|
@ -179,7 +203,7 @@ pub async fn view_reauth_get(
|
|||
ar,
|
||||
client_auth_info,
|
||||
session_context,
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -209,10 +233,8 @@ pub async fn view_reauth_get(
|
|||
|
||||
let remember_me = !username.is_empty();
|
||||
|
||||
let domain_custom_image = domain_info.image().is_some();
|
||||
|
||||
HtmlTemplate(LoginView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
username,
|
||||
remember_me,
|
||||
})
|
||||
|
@ -255,10 +277,13 @@ pub async fn view_index_get(
|
|||
|
||||
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 {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
username,
|
||||
remember_me,
|
||||
})
|
||||
|
@ -328,6 +353,11 @@ pub async fn view_login_begin_post(
|
|||
after_auth_loc: None,
|
||||
};
|
||||
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
};
|
||||
|
||||
// Now process the response if ok.
|
||||
match inter {
|
||||
Ok(ar) => {
|
||||
|
@ -338,7 +368,7 @@ pub async fn view_login_begin_post(
|
|||
ar,
|
||||
client_auth_info,
|
||||
session_context,
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -393,6 +423,11 @@ pub async fn view_login_mech_choose_post(
|
|||
)
|
||||
.await;
|
||||
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
};
|
||||
|
||||
// Now process the response if ok.
|
||||
match inter {
|
||||
Ok(ar) => {
|
||||
|
@ -403,7 +438,7 @@ pub async fn view_login_mech_choose_post(
|
|||
ar,
|
||||
client_auth_info,
|
||||
session_context,
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -440,10 +475,14 @@ pub async fn view_login_totp_post(
|
|||
) -> Response {
|
||||
// trim leading and trailing white space.
|
||||
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
|
||||
return HtmlTemplate(LoginTotpView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
totp: String::default(),
|
||||
errors: LoginTotpError::Syntax,
|
||||
})
|
||||
|
@ -540,6 +579,11 @@ async fn credential_step(
|
|||
cookies::get_signed::<SessionContext>(&state, &jar, COOKIE_AUTH_SESSION_ID)
|
||||
.unwrap_or_default();
|
||||
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
};
|
||||
|
||||
let inter = state // This may change in the future ...
|
||||
.qe_r_ref
|
||||
.handle_auth(
|
||||
|
@ -562,7 +606,7 @@ async fn credential_step(
|
|||
ar,
|
||||
client_auth_info,
|
||||
session_context,
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
@ -591,7 +635,7 @@ async fn view_login_step(
|
|||
auth_result: AuthResult,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
mut session_context: SessionContext,
|
||||
domain_info: DomainInfoRead,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
) -> Result<Response, OperationError> {
|
||||
trace!(?auth_result);
|
||||
|
||||
|
@ -601,8 +645,7 @@ async fn view_login_step(
|
|||
} = auth_result;
|
||||
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;
|
||||
|
||||
// Unlike the api version, only set the cookie.
|
||||
|
@ -662,11 +705,7 @@ async fn view_login_step(
|
|||
name: m,
|
||||
})
|
||||
.collect();
|
||||
HtmlTemplate(LoginMechView {
|
||||
domain_custom_image,
|
||||
mechs,
|
||||
})
|
||||
.into_response()
|
||||
HtmlTemplate(LoginMechView { display_ctx, mechs }).into_response()
|
||||
}
|
||||
};
|
||||
// break acts as return in a loop.
|
||||
|
@ -693,24 +732,24 @@ async fn view_login_step(
|
|||
|
||||
match auth_allowed {
|
||||
AuthAllowed::Totp => HtmlTemplate(LoginTotpView {
|
||||
display_ctx,
|
||||
totp: session_context.totp.clone().unwrap_or_default(),
|
||||
..Default::default()
|
||||
errors: LoginTotpError::default(),
|
||||
})
|
||||
.into_response(),
|
||||
AuthAllowed::Password => HtmlTemplate(LoginPasswordView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
password: session_context.password.clone().unwrap_or_default(),
|
||||
})
|
||||
.into_response(),
|
||||
AuthAllowed::BackupCode => HtmlTemplate(LoginBackupCodeView {
|
||||
domain_custom_image,
|
||||
})
|
||||
.into_response(),
|
||||
AuthAllowed::BackupCode => {
|
||||
HtmlTemplate(LoginBackupCodeView { display_ctx }).into_response()
|
||||
}
|
||||
AuthAllowed::SecurityKey(chal) => {
|
||||
let chal_json = serde_json::to_string(&chal)
|
||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||
HtmlTemplate(LoginWebauthnView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
passkey: false,
|
||||
chal: chal_json,
|
||||
})
|
||||
|
@ -720,7 +759,7 @@ async fn view_login_step(
|
|||
let chal_json = serde_json::to_string(&chal)
|
||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||
HtmlTemplate(LoginWebauthnView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
passkey: true,
|
||||
chal: chal_json,
|
||||
})
|
||||
|
@ -798,7 +837,7 @@ async fn view_login_step(
|
|||
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
||||
|
||||
break HtmlTemplate(LoginDeniedView {
|
||||
domain_custom_image,
|
||||
display_ctx,
|
||||
reason,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
use crate::https::views::HtmlTemplate;
|
||||
use crate::https::ServerState;
|
||||
use askama::Template;
|
||||
|
@ -81,13 +82,27 @@ pub(crate) async fn view_profile_unlock_get(
|
|||
Extension(kopid): Extension<KOpId>,
|
||||
jar: CookieJar,
|
||||
) -> 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(
|
||||
state,
|
||||
client_auth_info,
|
||||
kopid,
|
||||
jar,
|
||||
"/ui/profile",
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInforma
|
|||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::constants::ProfileMenuItems;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
use crate::https::ServerState;
|
||||
|
||||
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);
|
||||
Ok((jar, cu_resp).into_response())
|
||||
} else {
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: Some(Reauth {
|
||||
username: uat.spn,
|
||||
purpose: ReauthPurpose::ProfileSettings,
|
||||
}),
|
||||
};
|
||||
|
||||
super::login::view_reauth_get(
|
||||
state,
|
||||
client_auth_info,
|
||||
kopid,
|
||||
jar,
|
||||
"/ui/update_credentials",
|
||||
domain_info,
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -271,7 +271,6 @@ body {
|
|||
#cred-update-commit-bar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 5%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: white;
|
||||
|
|
|
@ -112,10 +112,12 @@
|
|||
(% let primary_state = primary_state %)
|
||||
(% include "credentials_update_primary.html" %)
|
||||
|
||||
<hr class="my-4" />
|
||||
<div id="cred-update-commit-bar" class="toast" role="alert"
|
||||
aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-body">
|
||||
<span class="d-flex align-items-center">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16"
|
||||
height="16" fill="currentColor"
|
||||
class="bi bi-floppy2-fill" viewBox="0 0 16 16">
|
||||
|
@ -123,15 +125,15 @@
|
|||
<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" />
|
||||
</svg>
|
||||
<b class="px-1">Careful</b>- save when you're done:
|
||||
<b>Careful</b> - Unsaved changes will be lost</div>
|
||||
</span>
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/ui/api/cu_cancel"
|
||||
hx-target="body">Cancel</button>
|
||||
hx-target="body">Discard Changes</button>
|
||||
<span class="d-inline-block" tabindex="0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="Resolve the warnings at the top.">
|
||||
data-bs-title="Unresolved warnings">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="submit"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(% block body %)
|
||||
<main id="main" class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
(% if domain_custom_image %)
|
||||
(% if display_ctx.domain_info.image().is_some() %)
|
||||
<img src="/ui/images/domain"
|
||||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% else %)
|
||||
|
@ -17,6 +17,9 @@
|
|||
alt="Kanidm" class="kanidm_logo" />
|
||||
(% endif %)
|
||||
<h3>Kanidm</h3>
|
||||
(% if let Some(reauth) = display_ctx.reauth %)
|
||||
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose ))</h5>
|
||||
(% endif %)
|
||||
</center>
|
||||
<div id="login-form-container" class="container">
|
||||
(% block logincontainer %)
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
|
||||
defer></script>
|
||||
|
||||
<div class="identity-verification-container">
|
||||
(% if passkey %)
|
||||
<form id="cred-form" action="/ui/login/passkey" method="POST">
|
||||
<input hidden="hidden" name="cred" id="cred">
|
||||
|
@ -27,5 +28,6 @@
|
|||
>Use Security Key</button>
|
||||
</form>
|
||||
(% endif %)
|
||||
</div>
|
||||
|
||||
(% endblock %)
|
||||
|
|
Loading…
Reference in a new issue