Improve some small behaviours of login and key management (#1383)

This commit is contained in:
Firstyear 2023-02-16 12:58:33 +10:00 committed by GitHub
parent a02337a07a
commit ad07f2a97e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 200 additions and 28 deletions

View file

@ -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<TlsConfiguration>,
pub cookie_key: [u8; 32],
pub integration_test_config: Option<Box<IntegrationTestConfig>>,
pub online_backup: Option<OnlineBackup>,
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<OnlineBackup>) {

View file

@ -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! 🪨 ");

View file

@ -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": [

View file

@ -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": [

View file

@ -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.

View file

@ -451,7 +451,7 @@ impl Entry<EntryInit, EntryNew> {
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))
)

View file

@ -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<CowCell<JwsSigner>>,
uat_jwt_validator: Arc<CowCell<JwsValidator>>,
token_enc_key: Arc<CowCell<Fernet>>,
cookie_key: Arc<CowCell<[u8; 32]>>,
}
/// 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<String>>,
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();

View file

@ -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 {

View file

@ -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<EntryInitNew> =
@ -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"]
}
}"#,

View file

@ -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

View file

@ -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<HashSet<String>, OperationError> {
self.internal_search_uuid(UUID_SYSTEM_CONFIG)

View file

@ -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);
};

View file

@ -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<AuthMech>),
// The choices of authentication mechanism.
@ -246,7 +246,10 @@ impl LoginApp {
fn view_state(&self, ctx: &Context<Self>) -> Html {
let inputvalue = self.inputvalue.clone();
match &self.state {
LoginState::Init(enable) => {
LoginState::Init {
enable,
remember_me,
} => {
html! {
<>
<div class="container">
@ -273,6 +276,18 @@ impl LoginApp {
/>
</div>
<div class="mb-3 form-check form-switch">
<input
type="checkbox"
class="form-check-input"
role="switch"
id="remember_me_check"
disabled={ !enable }
checked={ *remember_me }
/>
<label class="form-check-label" for="remember_me_check">{ "Remember my Username" }</label>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button
type="submit"
@ -564,7 +579,10 @@ impl Component for LoginApp {
// -- clear the bearer to prevent conflict
models::clear_bearer_token();
// Do we have a login hint?
let inputvalue = models::pop_login_hint().unwrap_or_default();
let (inputvalue, remember_me) = models::pop_login_hint()
.map(|user| (user, false))
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default();
#[cfg(debug_assertions)]
{
@ -581,7 +599,10 @@ impl Component for LoginApp {
// Clean any cookies.
// TODO: actually check that it's cleaning the cookies.
let state = LoginState::Init(true);
let state = LoginState::Init {
enable: true,
remember_me,
};
add_body_form_classes!();
@ -603,10 +624,17 @@ impl Component for LoginApp {
true
}
LoginAppMsg::Restart => {
// Clear any leftover input
self.inputvalue = "".to_string();
// Clear any leftover input. Reset to the remembered username if any.
let (inputvalue, remember_me) = models::get_login_remember_me()
.map(|user| (user, true))
.unwrap_or_default();
self.inputvalue = inputvalue;
self.session_id = "".to_string();
self.state = LoginState::Init(true);
self.state = LoginState::Init {
enable: true,
remember_me,
};
true
}
LoginAppMsg::Begin => {
@ -614,13 +642,35 @@ impl Component for LoginApp {
console::debug!(format!("begin -> {:?}", self.inputvalue));
// Disable the button?
let username = self.inputvalue.clone();
// If the remember-me was checked, stash it here.
// If it was false, clear existing data.
let remember_me = if utils::get_inputelement_by_id("remember_me_check")
.map(|element| element.checked())
.unwrap_or(false)
{
models::push_login_remember_me(username.clone());
true
} else {
models::pop_login_remember_me();
false
};
#[cfg(debug_assertions)]
console::debug!(format!("begin remember_me -> {:?}", remember_me));
ctx.link().send_future(async {
match Self::auth_init(username).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
self.state = LoginState::Init(false);
self.state = LoginState::Init {
enable: false,
remember_me,
};
true
}
LoginAppMsg::PasswordSubmit => {

View file

@ -69,6 +69,25 @@ pub fn pop_login_hint() -> Option<String> {
l.ok()
}
pub fn push_login_remember_me(r: String) {
PersistentStorage::set("login_remember_me", r).expect_throw("failed to set login remember me");
}
pub fn get_login_remember_me() -> Option<String> {
let l: Result<String, _> = PersistentStorage::get("login_remember_me");
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::pop_login_remember_me -> {:?}", l).as_str());
l.ok()
}
pub fn pop_login_remember_me() -> Option<String> {
let l: Result<String, _> = PersistentStorage::get("login_remember_me");
#[cfg(debug_assertions)]
console::debug!(format!("login_hint::pop_login_remember_me -> {:?}", l).as_str());
PersistentStorage::delete("login_remember_me");
l.ok()
}
/// Pushes the "cred_update_session" element into the browser's temporary storage
pub fn push_cred_update_session(s: (CUSessionToken, CUStatus)) {
TemporaryStorage::set("cred_update_session", s)

View file

@ -57,11 +57,11 @@ pub fn get_value_from_input_event(e: InputEvent) -> String {
// .and_then(|element| element.dyn_into::<web_sys::HtmlButtonElement>().ok())
// }
// pub fn get_inputelement_by_id(id: &str) -> Option<HtmlInputElement> {
// document()
// .get_element_by_id(id)
// .and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
// }
pub fn get_inputelement_by_id(id: &str) -> Option<HtmlInputElement> {
document()
.get_element_by_id(id)
.and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
}
pub fn get_value_from_element_id(id: &str) -> Option<String> {
document()