diff --git a/kanidmd/core/src/config.rs b/kanidmd/core/src/config.rs index ce6b64276..f2c852ec4 100644 --- a/kanidmd/core/src/config.rs +++ b/kanidmd/core/src/config.rs @@ -12,7 +12,6 @@ use std::path::Path; use std::str::FromStr; use kanidm_proto::messages::ConsoleOutputMode; -use rand::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] @@ -125,7 +124,6 @@ pub struct Configuration { pub secure_cookies: bool, pub trust_x_forward_for: bool, pub tls_config: Option, - pub cookie_key: [u8; 32], pub integration_test_config: Option>, pub online_backup: Option, pub domain: String, @@ -171,7 +169,7 @@ impl fmt::Display for Configuration { impl Configuration { pub fn new() -> Self { - let mut c = Configuration { + Configuration { address: String::from("127.0.0.1:8080"), ldapaddress: None, threads: std::thread::available_parallelism() @@ -189,17 +187,13 @@ impl Configuration { secure_cookies: !cfg!(test), trust_x_forward_for: false, tls_config: None, - cookie_key: [0; 32], integration_test_config: None, online_backup: None, domain: "idm.example.com".to_string(), origin: "https://idm.example.com".to_string(), role: ServerRole::WriteReplica, output_mode: ConsoleOutputMode::default(), - }; - let mut rng = StdRng::from_entropy(); - rng.fill(&mut c.cookie_key); - c + } } pub fn update_online_backup(&mut self, cfg: &Option) { diff --git a/kanidmd/core/src/lib.rs b/kanidmd/core/src/lib.rs index 2c623d617..3df88f5bd 100644 --- a/kanidmd/core/src/lib.rs +++ b/kanidmd/core/src/lib.rs @@ -669,6 +669,8 @@ pub async fn create_server_core( } }; + let cookie_key: [u8; 32] = idms.get_cookie_key(); + // Any pre-start tasks here. match &config.integration_test_config { Some(itc) => { @@ -781,8 +783,6 @@ pub async fn create_server_core( // TODO: Remove these when we go to auth bearer! // Copy the max size let _secure_cookies = config.secure_cookies; - // domain will come from the qs now! - let cookie_key: [u8; 32] = config.cookie_key; let maybe_http_acceptor_handle = if config_test { admin_info!("this config rocks! 🪨 "); diff --git a/kanidmd/lib/src/constants/acp.rs b/kanidmd/lib/src/constants/acp.rs index a883f7327..646cd7393 100644 --- a/kanidmd/lib/src/constants/acp.rs +++ b/kanidmd/lib/src/constants/acp.rs @@ -993,6 +993,7 @@ pub const JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &str = r#"{ "domain_uuid", "es256_private_key_der", "fernet_private_key_str", + "cookie_private_key", "name", "uuid" ], @@ -1000,6 +1001,7 @@ pub const JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &str = r#"{ "domain_display_name", "domain_ssid", "es256_private_key_der", + "cookie_private_key", "fernet_private_key_str" ], "acp_modify_presentattr": [ diff --git a/kanidmd/lib/src/constants/schema.rs b/kanidmd/lib/src/constants/schema.rs index 74568d842..628114b1d 100644 --- a/kanidmd/lib/src/constants/schema.rs +++ b/kanidmd/lib/src/constants/schema.rs @@ -967,6 +967,35 @@ pub const JSON_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: &str = r#"{ } }"#; +pub const JSON_SCHEMA_ATTR_PRIVATE_COOKIE_KEY: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "An private cookie hmac key" + ], + "index": [], + "unique": [ + "false" + ], + "multivalue": [ + "false" + ], + "attributename": [ + "private_cookie_key" + ], + "syntax": [ + "PRIVATE_BINARY" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000130" + ] + } +}"#; + pub const JSON_SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE: &str = r#"{ "attrs": { "class": [ @@ -1630,6 +1659,7 @@ pub const JSON_SCHEMA_CLASS_DOMAIN_INFO: &str = r#" "domain_display_name", "fernet_private_key_str", "es256_private_key_der", + "private_cookie_key", "version" ], "uuid": [ diff --git a/kanidmd/lib/src/constants/uuids.rs b/kanidmd/lib/src/constants/uuids.rs index 1d026c08b..52076dc9f 100644 --- a/kanidmd/lib/src/constants/uuids.rs +++ b/kanidmd/lib/src/constants/uuids.rs @@ -224,6 +224,7 @@ pub const UUID_SCHEMA_ATTR_EMAILPRIMARY: Uuid = uuid!("00000000-0000-0000-0000-f pub const UUID_SCHEMA_ATTR_EMAILALTERNATIVE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000127"); pub const UUID_SCHEMA_ATTR_TOTP_IMPORT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000128"); pub const UUID_SCHEMA_ATTR_REPLICATED: Uuid = uuid!("00000000-0000-0000-0000-ffff00000129"); +pub const UUID_SCHEMA_ATTR_PRIVATE_COOKIE_KEY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000130"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/kanidmd/lib/src/entry.rs b/kanidmd/lib/src/entry.rs index 2fd92e074..9a3eb9fbe 100644 --- a/kanidmd/lib/src/entry.rs +++ b/kanidmd/lib/src/entry.rs @@ -451,7 +451,7 @@ impl Entry { vs.into_iter().map(|v| Value::new_secret_str(&v)) ) } - "es256_private_key_der" => { + "es256_private_key_der" | "private_cookie_key" => { valueset::from_value_iter( vs.into_iter().map(|v| Value::new_privatebinary_base64(&v)) ) diff --git a/kanidmd/lib/src/idm/server.rs b/kanidmd/lib/src/idm/server.rs index 3b1595408..b9534c22a 100644 --- a/kanidmd/lib/src/idm/server.rs +++ b/kanidmd/lib/src/idm/server.rs @@ -1,4 +1,5 @@ use std::convert::TryFrom; +use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -81,6 +82,7 @@ pub struct IdmServer { uat_jwt_signer: Arc>, uat_jwt_validator: Arc>, token_enc_key: Arc>, + cookie_key: Arc>, } /// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication. @@ -130,6 +132,7 @@ pub struct IdmServerProxyWriteTransaction<'a> { pw_badlist_cache: CowCellWriteTxn<'a, HashSet>, uat_jwt_signer: CowCellWriteTxn<'a, JwsSigner>, uat_jwt_validator: CowCellWriteTxn<'a, JwsValidator>, + cookie_key: CowCellWriteTxn<'a, [u8; 32]>, pub(crate) token_enc_key: CowCellWriteTxn<'a, Fernet>, pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>, } @@ -156,13 +159,22 @@ impl IdmServer { let (async_tx, async_rx) = unbounded(); // Get the domain name, as the relying party id. - let (rp_id, rp_name, fernet_private_key, es256_private_key, pw_badlist_set, oauth2rs_set) = { + let ( + rp_id, + rp_name, + fernet_private_key, + es256_private_key, + cookie_key, + pw_badlist_set, + oauth2rs_set, + ) = { let mut qs_read = task::block_on(qs.read()); ( qs_read.get_domain_name().to_string(), qs_read.get_domain_display_name().to_string(), qs_read.get_domain_fernet_private_key()?, qs_read.get_domain_es256_private_key()?, + qs_read.get_domain_cookie_key()?, qs_read.get_password_badlist()?, // Add a read/reload of all oauth2 configurations. qs_read.get_oauth2rs_set()?, @@ -220,6 +232,8 @@ impl IdmServer { let uat_jwt_signer = Arc::new(CowCell::new(jwt_signer)); let uat_jwt_validator = Arc::new(CowCell::new(jwt_validator)); + let cookie_key = Arc::new(CowCell::new(cookie_key)); + let oauth2rs = Oauth2ResourceServers::try_from((oauth2rs_set, origin_url)).map_err(|e| { admin_error!("Failed to load oauth2 resource servers - {:?}", e); @@ -240,12 +254,17 @@ impl IdmServer { uat_jwt_signer, uat_jwt_validator, token_enc_key, + cookie_key, oauth2rs: Arc::new(oauth2rs), }, IdmServerDelayed { async_rx }, )) } + pub fn get_cookie_key(&self) -> [u8; 32] { + *self.cookie_key.read().deref() + } + #[cfg(test)] pub fn auth(&self) -> IdmServerAuthTransaction { task::block_on(self.auth_async()) @@ -301,6 +320,7 @@ impl IdmServer { uat_jwt_signer: self.uat_jwt_signer.write(), uat_jwt_validator: self.uat_jwt_validator.write(), token_enc_key: self.token_enc_key.write(), + cookie_key: self.cookie_key.write(), oauth2rs: self.oauth2rs.write(), } } @@ -2203,11 +2223,17 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { *self.uat_jwt_signer = new_signer; *self.uat_jwt_validator = new_validator; })?; + self.qs_write + .get_domain_cookie_key() + .map(|new_cookie_key| { + *self.cookie_key = new_cookie_key; + })?; } // Commit everything. self.oauth2rs.commit(); self.uat_jwt_signer.commit(); self.uat_jwt_validator.commit(); + self.cookie_key.commit(); self.token_enc_key.commit(); self.pw_badlist_cache.commit(); self.cred_update_sessions.commit(); diff --git a/kanidmd/lib/src/plugins/domain.rs b/kanidmd/lib/src/plugins/domain.rs index 948b15d65..f2ca8ef3b 100644 --- a/kanidmd/lib/src/plugins/domain.rs +++ b/kanidmd/lib/src/plugins/domain.rs @@ -8,6 +8,7 @@ use std::iter::once; use compact_jwt::JwsSigner; use kanidm_proto::v1::OperationError; +use rand::prelude::*; use tracing::trace; use crate::event::{CreateEvent, ModifyEvent}; @@ -91,6 +92,7 @@ impl Domain { let v = Value::new_secret_str(&k); e.add_ava("fernet_private_key_str", v); } + if !e.attribute_pres("es256_private_key_der") { security_info!("regenerating domain es256 private key"); let der = JwsSigner::generate_es256() @@ -102,6 +104,16 @@ impl Domain { let v = Value::new_privatebinary(&der); e.add_ava("es256_private_key_der", v); } + + if !e.attribute_pres("private_cookie_key") { + security_info!("regenerating domain cookie key"); + let mut key = [0; 32]; + let mut rng = StdRng::from_entropy(); + rng.fill(&mut key); + let v = Value::new_privatebinary(&key); + e.add_ava("private_cookie_key", v); + } + trace!(?e); Ok(()) } else { diff --git a/kanidmd/lib/src/plugins/protected.rs b/kanidmd/lib/src/plugins/protected.rs index bd63eb1f5..a8573ddfb 100644 --- a/kanidmd/lib/src/plugins/protected.rs +++ b/kanidmd/lib/src/plugins/protected.rs @@ -316,6 +316,10 @@ mod tests { "acp_modify_removedattr", Value::new_iutf8("es256_private_key_der") ), + ( + "acp_modify_removedattr", + Value::new_iutf8("private_cookie_key") + ), ("acp_modify_presentattr", Value::new_iutf8("class")), ("acp_modify_presentattr", Value::new_iutf8("displayname")), ("acp_modify_presentattr", Value::new_iutf8("may")), @@ -335,6 +339,10 @@ mod tests { "acp_modify_presentattr", Value::new_iutf8("es256_private_key_der") ), + ( + "acp_modify_presentattr", + Value::new_iutf8("private_cookie_key") + ), ("acp_create_class", Value::new_iutf8("object")), ("acp_create_class", Value::new_iutf8("person")), ("acp_create_class", Value::new_iutf8("system")), @@ -353,6 +361,7 @@ mod tests { Value::new_iutf8("fernet_private_key_str") ), ("acp_create_attr", Value::new_iutf8("es256_private_key_der")), + ("acp_create_attr", Value::new_iutf8("private_cookie_key")), ("acp_create_attr", Value::new_iutf8("version")) ); pub static ref PRELOAD: Vec = @@ -492,6 +501,7 @@ mod tests { "domain_ssid": ["Example_Wifi"], "fernet_private_key_str": ["ABCD"], "es256_private_key_der" : ["MTIz"], + "private_cookie_key" : ["MTIz"], "version": ["1"] } }"#, @@ -534,6 +544,7 @@ mod tests { "domain_ssid": ["Example_Wifi"], "fernet_private_key_str": ["ABCD"], "es256_private_key_der" : ["MTIz"], + "private_cookie_key" : ["MTIz"], "version": ["1"] } }"#, @@ -566,6 +577,7 @@ mod tests { "domain_ssid": ["Example_Wifi"], "fernet_private_key_str": ["ABCD"], "es256_private_key_der" : ["MTIz"], + "private_cookie_key" : ["MTIz"], "version": ["1"] } }"#, diff --git a/kanidmd/lib/src/server/migrations.rs b/kanidmd/lib/src/server/migrations.rs index 522940d8d..8f4edf48b 100644 --- a/kanidmd/lib/src/server/migrations.rs +++ b/kanidmd/lib/src/server/migrations.rs @@ -364,6 +364,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_CLASS_OAUTH2_RS, JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC, JSON_SCHEMA_CLASS_SYNC_ACCOUNT, + JSON_SCHEMA_ATTR_PRIVATE_COOKIE_KEY, ]; let r = idm_schema diff --git a/kanidmd/lib/src/server/mod.rs b/kanidmd/lib/src/server/mod.rs index 089d585ff..4598b6723 100644 --- a/kanidmd/lib/src/server/mod.rs +++ b/kanidmd/lib/src/server/mod.rs @@ -721,6 +721,27 @@ pub trait QueryServerTransaction<'a> { }) } + fn get_domain_cookie_key(&mut self) -> Result<[u8; 32], OperationError> { + self.internal_search_uuid(UUID_DOMAIN_INFO) + .and_then(|e| { + e.get_ava_single_private_binary("private_cookie_key") + .and_then(|s| { + let mut x = [0; 32]; + if s.len() == x.len() { + x.copy_from_slice(s); + Some(x) + } else { + None + } + }) + .ok_or(OperationError::InvalidEntryState) + }) + .map_err(|e| { + admin_error!(?e, "Error getting domain cookie key"); + e + }) + } + // This is a helper to get password badlist. fn get_password_badlist(&mut self) -> Result, OperationError> { self.internal_search_uuid(UUID_SYSTEM_CONFIG) diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui.js b/kanidmd_web_ui/pkg/kanidmd_web_ui.js index 062a9f3f7..df8767811 100644 --- a/kanidmd_web_ui/pkg/kanidmd_web_ui.js +++ b/kanidmd_web_ui/pkg/kanidmd_web_ui.js @@ -774,6 +774,10 @@ function getImports() { const ret = result; return ret; }; + imports.wbg.__wbg_checked_f0b666100ef39e44 = function(arg0) { + const ret = getObject(arg0).checked; + return ret; + }; imports.wbg.__wbg_setchecked_f1e1f3e62cdca8e7 = function(arg0, arg1) { getObject(arg0).checked = arg1 !== 0; }; @@ -1144,15 +1148,15 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper4732 = function(arg0, arg1, arg2) { + imports.wbg.__wbindgen_closure_wrapper4738 = function(arg0, arg1, arg2) { const ret = makeMutClosure(arg0, arg1, 1095, __wbg_adapter_48); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper5600 = function(arg0, arg1, arg2) { + imports.wbg.__wbindgen_closure_wrapper5605 = function(arg0, arg1, arg2) { const ret = makeMutClosure(arg0, arg1, 1436, __wbg_adapter_51); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper5678 = function(arg0, arg1, arg2) { + imports.wbg.__wbindgen_closure_wrapper5683 = function(arg0, arg1, arg2) { const ret = makeMutClosure(arg0, arg1, 1466, __wbg_adapter_54); return addHeapObject(ret); }; diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm index ca831e92a..e48a2edaa 100644 Binary files a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm and b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm differ diff --git a/kanidmd_web_ui/src/login.rs b/kanidmd_web_ui/src/login.rs index de75adff9..7e9d8e012 100644 --- a/kanidmd_web_ui/src/login.rs +++ b/kanidmd_web_ui/src/login.rs @@ -33,7 +33,7 @@ enum TotpState { } enum LoginState { - Init(bool), + Init { enable: bool, remember_me: bool }, // Select between different cred types, either password (and MFA) or Passkey Select(Vec), // The choices of authentication mechanism. @@ -246,7 +246,10 @@ impl LoginApp { fn view_state(&self, ctx: &Context) -> Html { let inputvalue = self.inputvalue.clone(); match &self.state { - LoginState::Init(enable) => { + LoginState::Init { + enable, + remember_me, + } => { html! { <>
@@ -273,6 +276,18 @@ impl LoginApp { />
+
+ + +
+