mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Fixes the logout flow in htmx and improves the login error dialog (#2889)
This commit is contained in:
parent
d7a5097527
commit
966e26f874
|
@ -541,17 +541,24 @@ impl QueryServerWriteV1 {
|
||||||
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||||
|
|
||||||
// We specifically need a uat here to assess the auth type!
|
// We specifically need a uat here to assess the auth type!
|
||||||
let ident = idms_prox_write
|
let validate_result =
|
||||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
idms_prox_write.validate_client_auth_info_to_ident(client_auth_info, ct);
|
||||||
.map_err(|e| {
|
|
||||||
admin_error!(err = ?e, "Invalid identity");
|
let ident = match validate_result {
|
||||||
e
|
Ok(ident) => ident,
|
||||||
})?;
|
Err(OperationError::SessionExpired) | Err(OperationError::NotAuthenticated) => {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
admin_error!(?err, "Invalid identity");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !ident.can_logout() {
|
if !ident.can_logout() {
|
||||||
info!("Ignoring request to logout session - these sessions are not recorded");
|
info!("Ignoring request to logout session - these sessions are not recorded");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
};
|
||||||
|
|
||||||
let target = ident.get_uuid().ok_or_else(|| {
|
let target = ident.get_uuid().ok_or_else(|| {
|
||||||
admin_error!("Invalid identity - no uuid present");
|
admin_error!("Invalid identity - no uuid present");
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
//! Where we hide the error handling widgets
|
//! Where we hide the error handling widgets
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use axum::http::{HeaderValue, StatusCode};
|
|
||||||
use axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
use axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||||
|
use axum::http::{HeaderValue, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use kanidm_proto::internal::OperationError;
|
use kanidm_proto::internal::OperationError;
|
||||||
|
|
||||||
|
|
||||||
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
||||||
#[derive(Debug, ToSchema)]
|
#[derive(Debug, ToSchema)]
|
||||||
pub enum WebError {
|
pub enum WebError {
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
|
||||||
extract::State,
|
extract::State,
|
||||||
http::uri::Uri,
|
http::uri::Uri,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
|
Extension,
|
||||||
};
|
};
|
||||||
use axum_htmx::{HxPushUrl, HxReswap, HxRetarget, SwapOption};
|
|
||||||
use axum_htmx::extractors::HxRequest;
|
use axum_htmx::extractors::HxRequest;
|
||||||
|
use axum_htmx::{HxPushUrl, HxReswap, HxRetarget, SwapOption};
|
||||||
|
|
||||||
use kanidm_proto::internal::AppLink;
|
use kanidm_proto::internal::AppLink;
|
||||||
|
|
||||||
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
|
||||||
use crate::https::views::errors::HtmxError;
|
|
||||||
use super::HtmlTemplate;
|
use super::HtmlTemplate;
|
||||||
|
use crate::https::views::errors::HtmxError;
|
||||||
|
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "apps.html")]
|
#[template(path = "apps.html")]
|
||||||
|
@ -43,9 +43,7 @@ pub(crate) async fn view_apps_get(
|
||||||
.map_err(|old| HtmxError::new(&kopid, old))?;
|
.map_err(|old| HtmxError::new(&kopid, old))?;
|
||||||
|
|
||||||
let apps_view = AppsView {
|
let apps_view = AppsView {
|
||||||
apps_partial: AppsPartialView {
|
apps_partial: AppsPartialView { apps: app_links },
|
||||||
apps: app_links,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(if hx_request {
|
Ok(if hx_request {
|
||||||
|
@ -60,7 +58,8 @@ pub(crate) async fn view_apps_get(
|
||||||
// We send our own main, replace the existing one.
|
// We send our own main, replace the existing one.
|
||||||
HxReswap(SwapOption::OuterHtml),
|
HxReswap(SwapOption::OuterHtml),
|
||||||
HtmlTemplate(apps_view),
|
HtmlTemplate(apps_view),
|
||||||
).into_response()
|
)
|
||||||
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
HtmlTemplate(apps_view).into_response()
|
HtmlTemplate(apps_view).into_response()
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,6 @@ use crate::https::middleware::KOpId;
|
||||||
// recovery_boosted: bool,
|
// recovery_boosted: bool,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
||||||
#[derive(Debug, ToSchema)]
|
#[derive(Debug, ToSchema)]
|
||||||
pub(crate) enum HtmxError {
|
pub(crate) enum HtmxError {
|
||||||
|
@ -46,7 +45,9 @@ impl IntoResponse for HtmxError {
|
||||||
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
||||||
(StatusCode::FORBIDDEN, body).into_response()
|
(StatusCode::FORBIDDEN, body).into_response()
|
||||||
}
|
}
|
||||||
OperationError::NoMatchingEntries => (StatusCode::NOT_FOUND, body).into_response(),
|
OperationError::NoMatchingEntries => {
|
||||||
|
(StatusCode::NOT_FOUND, body).into_response()
|
||||||
|
}
|
||||||
OperationError::PasswordQuality(_)
|
OperationError::PasswordQuality(_)
|
||||||
| OperationError::EmptyRequest
|
| OperationError::EmptyRequest
|
||||||
| OperationError::SchemaViolation(_)
|
| OperationError::SchemaViolation(_)
|
||||||
|
|
|
@ -100,6 +100,45 @@ struct LoginWebauthnView {
|
||||||
chal: String,
|
chal: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template, Default)]
|
||||||
|
#[template(path = "login_denied.html")]
|
||||||
|
struct LoginDeniedView {
|
||||||
|
reason: String,
|
||||||
|
operation_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn view_logout_get(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
mut jar: CookieJar,
|
||||||
|
) -> Response {
|
||||||
|
if let Err(err_code) = state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_logout(client_auth_info, kopid.eventid)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
HtmlTemplate(UnrecoverableErrorView {
|
||||||
|
err_code,
|
||||||
|
operation_id: kopid.eventid,
|
||||||
|
})
|
||||||
|
.into_response()
|
||||||
|
} else {
|
||||||
|
let response = Redirect::to("/ui").into_response();
|
||||||
|
|
||||||
|
jar = if let Some(bearer_cookie) = jar.get(COOKIE_BEARER_TOKEN) {
|
||||||
|
let mut bearer_cookie = bearer_cookie.clone();
|
||||||
|
bearer_cookie.make_removal();
|
||||||
|
bearer_cookie.set_path("/");
|
||||||
|
jar.add(bearer_cookie)
|
||||||
|
} else {
|
||||||
|
jar
|
||||||
|
};
|
||||||
|
|
||||||
|
(jar, response).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn view_index_get(
|
pub async fn view_index_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
@ -606,6 +645,7 @@ async fn view_login_step(
|
||||||
let mut bearer_cookie = Cookie::new(COOKIE_BEARER_TOKEN, token_str.clone());
|
let mut bearer_cookie = Cookie::new(COOKIE_BEARER_TOKEN, token_str.clone());
|
||||||
bearer_cookie.set_secure(state.secure_cookies);
|
bearer_cookie.set_secure(state.secure_cookies);
|
||||||
bearer_cookie.set_same_site(SameSite::Lax);
|
bearer_cookie.set_same_site(SameSite::Lax);
|
||||||
|
// Prevent Document.cookie accessing this. Still works with fetch.
|
||||||
bearer_cookie.set_http_only(true);
|
bearer_cookie.set_http_only(true);
|
||||||
// We set a domain here because it allows subdomains
|
// We set a domain here because it allows subdomains
|
||||||
// of the idm to share the cookie. If domain was incorrect
|
// of the idm to share the cookie. If domain was incorrect
|
||||||
|
@ -617,7 +657,7 @@ async fn view_login_step(
|
||||||
let mut username_cookie =
|
let mut username_cookie =
|
||||||
Cookie::new(COOKIE_USERNAME, session_context.username.clone());
|
Cookie::new(COOKIE_USERNAME, session_context.username.clone());
|
||||||
username_cookie.set_secure(state.secure_cookies);
|
username_cookie.set_secure(state.secure_cookies);
|
||||||
username_cookie.set_same_site(SameSite::Strict);
|
username_cookie.set_same_site(SameSite::Lax);
|
||||||
username_cookie.set_http_only(true);
|
username_cookie.set_http_only(true);
|
||||||
username_cookie.set_domain(state.domain.clone());
|
username_cookie.set_domain(state.domain.clone());
|
||||||
username_cookie.set_path("/");
|
username_cookie.set_path("/");
|
||||||
|
@ -636,12 +676,15 @@ async fn view_login_step(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AuthState::Denied(_reason) => {
|
AuthState::Denied(reason) => {
|
||||||
debug!("🧩 -> AuthState::Denied");
|
debug!("🧩 -> AuthState::Denied");
|
||||||
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
||||||
|
|
||||||
// Render a denial.
|
break HtmlTemplate(LoginDeniedView {
|
||||||
break Redirect::temporary("/ui/getrekt").into_response();
|
reason,
|
||||||
|
operation_id: kopid.eventid,
|
||||||
|
})
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,8 +17,8 @@ use crate::https::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod apps;
|
mod apps;
|
||||||
mod login;
|
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod login;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "unrecoverable_error.html")]
|
#[template(path = "unrecoverable_error.html")]
|
||||||
|
@ -31,6 +31,7 @@ pub fn view_router() -> Router<ServerState> {
|
||||||
let unguarded_router = Router::new()
|
let unguarded_router = Router::new()
|
||||||
.route("/", get(login::view_index_get))
|
.route("/", get(login::view_index_get))
|
||||||
.route("/apps", get(apps::view_apps_get))
|
.route("/apps", get(apps::view_apps_get))
|
||||||
|
.route("/logout", get(login::view_logout_get))
|
||||||
// The login routes are htmx-free to make them simpler, which means
|
// The login routes are htmx-free to make them simpler, which means
|
||||||
// they need manual guarding for direct get requests which can occur
|
// they need manual guarding for direct get requests which can occur
|
||||||
// if a user attempts to reload the page.
|
// if a user attempts to reload the page.
|
||||||
|
|
13
server/core/templates/login_denied.html
Normal file
13
server/core/templates/login_denied.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
(% extends "login_base.html" %)
|
||||||
|
|
||||||
|
(% block logincontainer %)
|
||||||
|
<h3>Login Failed</h3>
|
||||||
|
<main id="main">
|
||||||
|
<p>Reason: (( reason ))</p>
|
||||||
|
<p>Operation ID: (( operation_id ))</p>
|
||||||
|
<a href="/ui">
|
||||||
|
<button type="button" class="btn btn-success">Return to Login</button>
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
(% endblock %)
|
|
@ -8,6 +8,7 @@
|
||||||
(% block body %)
|
(% block body %)
|
||||||
<h2>Error</h2>
|
<h2>Error</h2>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
<p>An unrecoverable error occured. Please contact your administrator with the details below.</p>
|
||||||
<p>Error Code: (( err_code ))</p>
|
<p>Error Code: (( err_code ))</p>
|
||||||
<p>Operation ID: (( operation_id ))</p>
|
<p>Operation ID: (( operation_id ))</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
Loading…
Reference in a new issue