mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Re-add enrol another device flow
This was a commonly requested re-addition to the new webui. This adds the ability for someone to scan a qr code or follow a link to enrol another device to their account.
This commit is contained in:
parent
11438a9dd5
commit
c59f560e50
|
@ -53,6 +53,7 @@ use tokio::{
|
|||
use tokio_openssl::SslStream;
|
||||
use tower::Service;
|
||||
use tower_http::{services::ServeDir, trace::TraceLayer};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::io::ErrorKind;
|
||||
|
@ -62,16 +63,17 @@ use std::{net::SocketAddr, str::FromStr};
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerState {
|
||||
pub status_ref: &'static StatusActor,
|
||||
pub qe_w_ref: &'static QueryServerWriteV1,
|
||||
pub qe_r_ref: &'static QueryServerReadV1,
|
||||
pub(crate) status_ref: &'static StatusActor,
|
||||
pub(crate) qe_w_ref: &'static QueryServerWriteV1,
|
||||
pub(crate) qe_r_ref: &'static QueryServerReadV1,
|
||||
// Store the token management parts.
|
||||
pub jws_signer: JwsHs256Signer,
|
||||
pub(crate) jws_signer: JwsHs256Signer,
|
||||
pub(crate) trust_x_forward_for: bool,
|
||||
pub csp_header: HeaderValue,
|
||||
pub domain: String,
|
||||
pub(crate) csp_header: HeaderValue,
|
||||
pub(crate) origin: Url,
|
||||
pub(crate) domain: String,
|
||||
// This is set to true by default, and is only false on integration tests.
|
||||
pub secure_cookies: bool,
|
||||
pub(crate) secure_cookies: bool,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
|
@ -209,6 +211,12 @@ pub async fn create_https_server(
|
|||
|
||||
let trust_x_forward_for = config.trust_x_forward_for;
|
||||
|
||||
let origin = Url::parse(&config.origin)
|
||||
// Should be impossible!
|
||||
.map_err(|err| {
|
||||
error!(?err, "Unable to parse origin URL - refusing to start. You must correct the value for origin. {:?}", config.origin);
|
||||
})?;
|
||||
|
||||
let state = ServerState {
|
||||
status_ref,
|
||||
qe_w_ref,
|
||||
|
@ -216,6 +224,7 @@ pub async fn create_https_server(
|
|||
jws_signer,
|
||||
trust_x_forward_for,
|
||||
csp_header,
|
||||
origin,
|
||||
domain: config.domain.clone(),
|
||||
secure_cookies: config.integration_test_config.is_none(),
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub(crate) enum ProfileMenuItems {
|
||||
UserProfile,
|
||||
Credentials,
|
||||
EnrolDevice,
|
||||
UnixPassword,
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ pub(crate) enum Urls {
|
|||
Apps,
|
||||
CredReset,
|
||||
CredResetError,
|
||||
EnrolDevice,
|
||||
Profile,
|
||||
UpdateCredentials,
|
||||
Oauth2Resume,
|
||||
|
@ -38,6 +40,7 @@ impl AsRef<str> for Urls {
|
|||
Self::Apps => "/ui/apps",
|
||||
Self::CredReset => "/ui/reset",
|
||||
Self::CredResetError => "/ui/reset/err",
|
||||
Self::EnrolDevice => "/ui/enrol",
|
||||
Self::Profile => "/ui/profile",
|
||||
Self::UpdateCredentials => "/ui/update_credentials",
|
||||
Self::Oauth2Resume => "/ui/oauth2/resume",
|
||||
|
|
116
server/core/src/https/views/enrol.rs
Normal file
116
server/core/src/https/views/enrol.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::response::Response;
|
||||
use axum::Extension;
|
||||
|
||||
use axum_extra::extract::CookieJar;
|
||||
use kanidm_proto::internal::UserAuthToken;
|
||||
|
||||
use qrcode::render::svg;
|
||||
use qrcode::QrCode;
|
||||
use url::Url;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use super::constants::Urls;
|
||||
use super::navbar::NavbarCtx;
|
||||
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
||||
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;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user_settings.html")]
|
||||
struct ProfileView {
|
||||
navbar_ctx: NavbarCtx,
|
||||
profile_partial: EnrolDeviceView,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "enrol_device.html")]
|
||||
pub(crate) struct EnrolDeviceView {
|
||||
menu_active_item: ProfileMenuItems,
|
||||
secret: String,
|
||||
qr_code_svg: String,
|
||||
uri: Url,
|
||||
}
|
||||
|
||||
pub(crate) async fn view_enrol_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
DomainInfo(domain_info): DomainInfo,
|
||||
jar: CookieJar,
|
||||
) -> axum::response::Result<Response> {
|
||||
let uat: UserAuthToken = state
|
||||
.qe_r_ref
|
||||
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
||||
.await
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
|
||||
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
|
||||
let can_rw = uat.purpose_readwrite_active(time);
|
||||
|
||||
// The user lacks an elevated session, request a re-auth.
|
||||
if !can_rw {
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
oauth2: None,
|
||||
reauth: Some(Reauth {
|
||||
username: uat.spn,
|
||||
purpose: ReauthPurpose::ProfileSettings,
|
||||
}),
|
||||
error: None,
|
||||
};
|
||||
|
||||
return Ok(super::login::view_reauth_get(
|
||||
state,
|
||||
client_auth_info,
|
||||
kopid,
|
||||
jar,
|
||||
Urls::EnrolDevice.as_ref(),
|
||||
display_ctx,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
|
||||
let cu_intent = state
|
||||
.qe_w_ref
|
||||
.handle_idmcredentialupdateintent(
|
||||
client_auth_info,
|
||||
uat.spn,
|
||||
Some(Duration::from_secs(900)),
|
||||
kopid.eventid,
|
||||
)
|
||||
.await
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
|
||||
let secret = cu_intent.token;
|
||||
|
||||
let mut uri = state.origin.clone();
|
||||
uri.set_path(Urls::CredReset.as_ref());
|
||||
uri.set_query(Some(format!("token={secret}").as_str()));
|
||||
|
||||
let qr_code_svg = match QrCode::new(uri.as_str()) {
|
||||
Ok(qr) => qr.render::<svg::Color>().build(),
|
||||
Err(qr_err) => {
|
||||
error!("Failed to create TOTP QR code: {qr_err}");
|
||||
"QR Code Generation Failed".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ProfileView {
|
||||
navbar_ctx: NavbarCtx { domain_info },
|
||||
profile_partial: EnrolDeviceView {
|
||||
menu_active_item: ProfileMenuItems::EnrolDevice,
|
||||
qr_code_svg,
|
||||
secret,
|
||||
uri,
|
||||
},
|
||||
}
|
||||
.into_response())
|
||||
}
|
|
@ -14,8 +14,8 @@ use axum::{
|
|||
};
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use kanidm_proto::internal::{
|
||||
COOKIE_AUTH_SESSION_ID, COOKIE_BEARER_TOKEN, COOKIE_OAUTH2_REQ, COOKIE_USERNAME,
|
||||
COOKIE_CU_SESSION_TOKEN
|
||||
COOKIE_AUTH_SESSION_ID, COOKIE_BEARER_TOKEN, COOKIE_CU_SESSION_TOKEN, COOKIE_OAUTH2_REQ,
|
||||
COOKIE_USERNAME,
|
||||
};
|
||||
use kanidm_proto::v1::{
|
||||
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthStep,
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::https::ServerState;
|
|||
mod apps;
|
||||
mod constants;
|
||||
mod cookies;
|
||||
mod enrol;
|
||||
mod errors;
|
||||
mod login;
|
||||
mod navbar;
|
||||
|
@ -37,6 +38,7 @@ pub fn view_router() -> Router<ServerState> {
|
|||
get(|| async { Redirect::permanent(Urls::Login.as_ref()) }),
|
||||
)
|
||||
.route("/apps", get(apps::view_apps_get))
|
||||
.route("/enrol", get(enrol::view_enrol_get))
|
||||
.route("/reset", get(reset::view_reset_get))
|
||||
.route("/update_credentials", get(reset::view_self_reset_get))
|
||||
.route("/profile", get(profile::view_profile_get))
|
||||
|
|
3
server/core/static/img/icons/phone-flip.svg
Normal file
3
server/core/static/img/icons/phone-flip.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-phone-flip" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11 1H5a1 1 0 0 0-1 1v6a.5.5 0 0 1-1 0V2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v6a.5.5 0 0 1-1 0V2a1 1 0 0 0-1-1m1 13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a.5.5 0 0 0-1 0v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2a.5.5 0 0 0-1 0zM1.713 7.954a.5.5 0 1 0-.419-.908c-.347.16-.654.348-.882.57C.184 7.842 0 8.139 0 8.5c0 .546.408.94.823 1.201.44.278 1.043.51 1.745.696C3.978 10.773 5.898 11 8 11q.148 0 .294-.002l-1.148 1.148a.5.5 0 0 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708l1.145 1.144L8 10c-2.04 0-3.87-.221-5.174-.569-.656-.175-1.151-.374-1.47-.575C1.012 8.639 1 8.506 1 8.5c0-.003 0-.059.112-.17.115-.112.31-.242.6-.376Zm12.993-.908a.5.5 0 0 0-.419.908c.292.134.486.264.6.377.113.11.113.166.113.169s0 .065-.13.187c-.132.122-.352.26-.677.4-.645.28-1.596.523-2.763.687a.5.5 0 0 0 .14.99c1.212-.17 2.26-.43 3.02-.758.38-.164.713-.357.96-.587.246-.229.45-.537.45-.919 0-.362-.184-.66-.412-.883s-.535-.411-.882-.571M7.5 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -1,8 +1,7 @@
|
|||
<div>
|
||||
<div id="totpInfo">
|
||||
(% if let Some(TotpInit with { secret, qr_code_svg, steps, digits, algo, uri }) = totp_init %)
|
||||
<div>((qr_code_svg|safe))
|
||||
</div>
|
||||
<div>((qr_code_svg|safe))</div>
|
||||
<code>((uri|safe))</code>
|
||||
|
||||
<h3>TOTP details</h3>
|
||||
|
|
17
server/core/templates/enrol_device.html
Normal file
17
server/core/templates/enrol_device.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
(% extends "user_settings_partial_base.html" %)
|
||||
|
||||
(% block selected_setting_group %)
|
||||
Enrol Another Device
|
||||
(% endblock %)
|
||||
|
||||
(% block settings_window %)
|
||||
<p>You can enrol another device to your account by scanning the QR code or following the link below.</p>
|
||||
|
||||
<div id="intentInfo">
|
||||
<div>((qr_code_svg|safe))</div>
|
||||
<ul>
|
||||
<li>Url: <code>((uri|safe))</code></li>
|
||||
<li>Secret: <code>(( secret ))</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
(% endblock %)
|
|
@ -20,6 +20,8 @@
|
|||
ProfileMenuItems::UserProfile, "person") %)
|
||||
(% call side_menu_item("Credentials", (Urls::UpdateCredentials),
|
||||
ProfileMenuItems::Credentials, "shield-lock") %)
|
||||
(% call side_menu_item("Enrol Device", (Urls::EnrolDevice),
|
||||
ProfileMenuItems::EnrolDevice, "phone-flip") %)
|
||||
</ul>
|
||||
<div id="settings-window" class="flex-grow-1 ps-sm-4 ps-md-5 pt-sm-0 pt-4">
|
||||
<div>
|
||||
|
|
Loading…
Reference in a new issue