mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
20241017 3107 token ttl (#3114)
This commit is contained in:
parent
99a799d72a
commit
5a3e5f1e07
|
@ -1807,7 +1807,7 @@ impl KanidmClient {
|
||||||
|
|
||||||
pub async fn idm_account_credential_update_exchange(
|
pub async fn idm_account_credential_update_exchange(
|
||||||
&self,
|
&self,
|
||||||
intent_token: CUIntentToken,
|
intent_token: String,
|
||||||
) -> Result<(CUSessionToken, CUStatus), ClientError> {
|
) -> Result<(CUSessionToken, CUStatus), ClientError> {
|
||||||
// We don't need to send the UAT with these, which is why we use the different path.
|
// We don't need to send the UAT with these, which is why we use the different path.
|
||||||
self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
|
self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
|
||||||
|
|
|
@ -64,6 +64,8 @@ impl TotpSecret {
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct CUIntentToken {
|
pub struct CUIntentToken {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
|
#[serde(with = "time::serde::timestamp")]
|
||||||
|
pub expiry_time: time::OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||||
|
|
|
@ -17,8 +17,8 @@ use kanidmd_lib::{
|
||||||
filter::{Filter, FilterInvalid},
|
filter::{Filter, FilterInvalid},
|
||||||
idm::account::DestroySessionTokenEvent,
|
idm::account::DestroySessionTokenEvent,
|
||||||
idm::credupdatesession::{
|
idm::credupdatesession::{
|
||||||
CredentialUpdateIntentToken, CredentialUpdateSessionToken, InitCredentialUpdateEvent,
|
CredentialUpdateIntentTokenExchange, CredentialUpdateSessionToken,
|
||||||
InitCredentialUpdateIntentEvent,
|
InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent,
|
||||||
},
|
},
|
||||||
idm::event::{GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent},
|
idm::event::{GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent},
|
||||||
idm::oauth2::{
|
idm::oauth2::{
|
||||||
|
@ -669,6 +669,7 @@ impl QueryServerWriteV1 {
|
||||||
})
|
})
|
||||||
.map(|tok| CUIntentToken {
|
.map(|tok| CUIntentToken {
|
||||||
token: tok.intent_id,
|
token: tok.intent_id,
|
||||||
|
expiry_time: tok.expiry_time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -679,14 +680,12 @@ impl QueryServerWriteV1 {
|
||||||
)]
|
)]
|
||||||
pub async fn handle_idmcredentialexchangeintent(
|
pub async fn handle_idmcredentialexchangeintent(
|
||||||
&self,
|
&self,
|
||||||
intent_token: CUIntentToken,
|
intent_id: String,
|
||||||
eventid: Uuid,
|
eventid: Uuid,
|
||||||
) -> Result<(CUSessionToken, CUStatus), OperationError> {
|
) -> Result<(CUSessionToken, CUStatus), OperationError> {
|
||||||
let ct = duration_from_epoch_now();
|
let ct = duration_from_epoch_now();
|
||||||
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
|
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
|
||||||
let intent_token = CredentialUpdateIntentToken {
|
let intent_token = CredentialUpdateIntentTokenExchange { intent_id };
|
||||||
intent_id: intent_token.token,
|
|
||||||
};
|
|
||||||
// TODO: this is throwing a 500 error when a session is already in use, that seems bad?
|
// TODO: this is throwing a 500 error when a session is already in use, that seems bad?
|
||||||
idms_prox_write
|
idms_prox_write
|
||||||
.exchange_intent_credential_update(intent_token, ct)
|
.exchange_intent_credential_update(intent_token, ct)
|
||||||
|
|
|
@ -1340,7 +1340,7 @@ pub async fn account_user_auth_token_delete(
|
||||||
pub async fn credential_update_exchange_intent(
|
pub async fn credential_update_exchange_intent(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Json(intent_token): Json<CUIntentToken>,
|
Json(intent_token): Json<String>,
|
||||||
) -> Result<Json<(CUSessionToken, CUStatus)>, WebError> {
|
) -> Result<Json<(CUSessionToken, CUStatus)>, WebError> {
|
||||||
state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
|
|
|
@ -19,9 +19,9 @@ use std::fmt::{Display, Formatter};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use kanidm_proto::internal::{
|
use kanidm_proto::internal::{
|
||||||
CUCredState, CUExtPortal, CUIntentToken, CURegState, CURegWarning, CURequest, CUSessionToken,
|
CUCredState, CUExtPortal, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus,
|
||||||
CUStatus, CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo,
|
CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo, UserAuthToken,
|
||||||
UserAuthToken, COOKIE_CU_SESSION_TOKEN,
|
COOKIE_CU_SESSION_TOKEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
|
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
|
||||||
|
@ -690,7 +690,7 @@ pub(crate) async fn view_reset_get(
|
||||||
// We have a reset token and want to create a new session
|
// We have a reset token and want to create a new session
|
||||||
match state
|
match state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_idmcredentialexchangeintent(CUIntentToken { token }, kopid.eventid)
|
.handle_idmcredentialexchangeintent(token, kopid.eventid)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((cu_session_token, cu_status)) => {
|
Ok((cu_session_token, cu_status)) => {
|
||||||
|
|
|
@ -32,12 +32,12 @@ use compact_jwt::jwe::JweBuilder;
|
||||||
use super::accountpolicy::ResolvedAccountPolicy;
|
use super::accountpolicy::ResolvedAccountPolicy;
|
||||||
|
|
||||||
const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
|
const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
|
||||||
|
// Minimum 5 minutes.
|
||||||
|
const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
|
||||||
// Default 1 hour.
|
// Default 1 hour.
|
||||||
const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
|
const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600);
|
||||||
// Default 1 day.
|
// Default 1 day.
|
||||||
const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400);
|
const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400);
|
||||||
// Minimum 5 minutes.
|
|
||||||
const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PasswordQuality {
|
pub enum PasswordQuality {
|
||||||
|
@ -50,6 +50,20 @@ pub enum PasswordQuality {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CredentialUpdateIntentToken {
|
pub struct CredentialUpdateIntentToken {
|
||||||
pub intent_id: String,
|
pub intent_id: String,
|
||||||
|
pub expiry_time: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CredentialUpdateIntentTokenExchange {
|
||||||
|
pub intent_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CredentialUpdateIntentToken> for CredentialUpdateIntentTokenExchange {
|
||||||
|
fn from(tok: CredentialUpdateIntentToken) -> Self {
|
||||||
|
CredentialUpdateIntentTokenExchange {
|
||||||
|
intent_id: tok.intent_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -932,8 +946,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
let mttl = event.max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
|
let mttl = event.max_ttl.unwrap_or(DEFAULT_INTENT_TTL);
|
||||||
let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
|
let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
|
||||||
debug!(?clamped_mttl, "clamped update intent validity");
|
debug!(?clamped_mttl, "clamped update intent validity");
|
||||||
|
// Absolute expiry of the intent token in epoch seconds
|
||||||
let max_ttl = ct + clamped_mttl;
|
let max_ttl = ct + clamped_mttl;
|
||||||
|
|
||||||
|
// Get the expiry of the intent token as an odt.
|
||||||
|
let expiry_time = OffsetDateTime::UNIX_EPOCH + max_ttl;
|
||||||
|
|
||||||
let intent_id = readable_password_from_random();
|
let intent_id = readable_password_from_random();
|
||||||
|
|
||||||
// Mark that we have created an intent token on the user.
|
// Mark that we have created an intent token on the user.
|
||||||
|
@ -984,15 +1002,18 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(CredentialUpdateIntentToken { intent_id })
|
Ok(CredentialUpdateIntentToken {
|
||||||
|
intent_id,
|
||||||
|
expiry_time,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_intent_credential_update(
|
pub fn exchange_intent_credential_update(
|
||||||
&mut self,
|
&mut self,
|
||||||
token: CredentialUpdateIntentToken,
|
token: CredentialUpdateIntentTokenExchange,
|
||||||
current_time: Duration,
|
current_time: Duration,
|
||||||
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
||||||
let CredentialUpdateIntentToken { intent_id } = token;
|
let CredentialUpdateIntentTokenExchange { intent_id } = token;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
let entry = self.qs_write.internal_search_uuid(&token.target)?;
|
let entry = self.qs_write.internal_search_uuid(&token.target)?;
|
||||||
|
@ -2633,24 +2654,24 @@ mod tests {
|
||||||
// exchange intent token - invalid - fail
|
// exchange intent token - invalid - fail
|
||||||
// Expired
|
// Expired
|
||||||
let cur = idms_prox_write
|
let cur = idms_prox_write
|
||||||
.exchange_intent_credential_update(intent_tok.clone(), ct + MINIMUM_INTENT_TTL);
|
.exchange_intent_credential_update(intent_tok.clone().into(), ct + MINIMUM_INTENT_TTL);
|
||||||
|
|
||||||
assert!(matches!(cur, Err(OperationError::SessionExpired)));
|
assert!(matches!(cur, Err(OperationError::SessionExpired)));
|
||||||
|
|
||||||
let cur = idms_prox_write
|
let cur = idms_prox_write
|
||||||
.exchange_intent_credential_update(intent_tok.clone(), ct + MAXIMUM_INTENT_TTL);
|
.exchange_intent_credential_update(intent_tok.clone().into(), ct + MAXIMUM_INTENT_TTL);
|
||||||
|
|
||||||
assert!(matches!(cur, Err(OperationError::SessionExpired)));
|
assert!(matches!(cur, Err(OperationError::SessionExpired)));
|
||||||
|
|
||||||
// exchange intent token - success
|
// exchange intent token - success
|
||||||
let (cust_a, _c_status) = idms_prox_write
|
let (cust_a, _c_status) = idms_prox_write
|
||||||
.exchange_intent_credential_update(intent_tok.clone(), ct)
|
.exchange_intent_credential_update(intent_tok.clone().into(), ct)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Session in progress - This will succeed and then block the former success from
|
// Session in progress - This will succeed and then block the former success from
|
||||||
// committing.
|
// committing.
|
||||||
let (cust_b, _c_status) = idms_prox_write
|
let (cust_b, _c_status) = idms_prox_write
|
||||||
.exchange_intent_credential_update(intent_tok, ct + Duration::from_secs(1))
|
.exchange_intent_credential_update(intent_tok.into(), ct + Duration::from_secs(1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
|
let cur = idms_prox_write.commit_credential_update(&cust_a, ct);
|
||||||
|
|
|
@ -1069,7 +1069,7 @@ async fn test_server_credential_update_session_pw(rsclient: KanidmClient) {
|
||||||
let _ = rsclient.logout().await;
|
let _ = rsclient.logout().await;
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let (session_token, _status) = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token.token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1144,7 +1144,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
|
||||||
let _ = rsclient.logout().await;
|
let _ = rsclient.logout().await;
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let (session_token, _statu) = rsclient
|
let (session_token, _statu) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token.token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1273,7 +1273,7 @@ async fn setup_demo_account_passkey(rsclient: &KanidmClient) -> WebauthnAuthenti
|
||||||
let _ = rsclient.logout().await;
|
let _ = rsclient.logout().await;
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let (session_token, _status) = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token.token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1359,7 +1359,7 @@ async fn setup_demo_account_password(
|
||||||
|
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let (session_token, _status) = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token.token)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to exchange intent token");
|
.expect("Failed to exchange intent token");
|
||||||
|
|
||||||
|
@ -1611,7 +1611,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
|
||||||
let _ = rsclient.logout().await;
|
let _ = rsclient.logout().await;
|
||||||
// Exchange the intent token
|
// Exchange the intent token
|
||||||
let (session_token, _status) = rsclient
|
let (session_token, _status) = rsclient
|
||||||
.idm_account_credential_update_exchange(intent_token)
|
.idm_account_credential_update_exchange(intent_token.token)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -816,7 +816,7 @@ impl CredentialResetApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exchange_intent_token(token: String) -> Result<Msg, FetchError> {
|
async fn exchange_intent_token(token: String) -> Result<Msg, FetchError> {
|
||||||
let request = CUIntentToken { token };
|
let request = token;
|
||||||
let req_jsvalue = request
|
let req_jsvalue = request
|
||||||
.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
|
.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
|
||||||
.expect("Failed to serialise request");
|
.expect("Failed to serialise request");
|
||||||
|
|
|
@ -49,7 +49,7 @@ rpassword = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
shellexpand = { workspace = true }
|
shellexpand = { workspace = true }
|
||||||
time = { workspace = true, features = ["serde", "std"] }
|
time = { workspace = true, features = ["serde", "std", "local-offset"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||||
tokio = { workspace = true, features = ["rt", "macros", "fs", "signal"] }
|
tokio = { workspace = true, features = ["rt", "macros", "fs", "signal"] }
|
||||||
|
|
|
@ -21,7 +21,7 @@ use kanidm_proto::scim_v1::{client::ScimSshPublicKeys, ScimEntryGetQuery};
|
||||||
use qrcode::render::unicode;
|
use qrcode::render::unicode;
|
||||||
use qrcode::QrCode;
|
use qrcode::QrCode;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
use time::OffsetDateTime;
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::webauthn::get_authenticator;
|
use crate::webauthn::get_authenticator;
|
||||||
|
@ -637,9 +637,7 @@ impl AccountCredential {
|
||||||
// The account credential use_reset_token CLI
|
// The account credential use_reset_token CLI
|
||||||
AccountCredential::UseResetToken(aopt) => {
|
AccountCredential::UseResetToken(aopt) => {
|
||||||
let client = aopt.copt.to_unauth_client();
|
let client = aopt.copt.to_unauth_client();
|
||||||
let cuintent_token = CUIntentToken {
|
let cuintent_token = aopt.token.clone();
|
||||||
token: aopt.token.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.idm_account_credential_update_exchange(cuintent_token)
|
.idm_account_credential_update_exchange(cuintent_token)
|
||||||
|
@ -669,14 +667,13 @@ impl AccountCredential {
|
||||||
.idm_person_account_credential_update_intent(aopts.account_id.as_str(), *ttl)
|
.idm_person_account_credential_update_intent(aopts.account_id.as_str(), *ttl)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cuintent_token) => {
|
Ok(CUIntentToken { token, expiry_time }) => {
|
||||||
let mut url = client.make_url("/ui/reset");
|
let mut url = client.make_url("/ui/reset");
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut().append_pair("token", token.as_str());
|
||||||
.append_pair("token", cuintent_token.token.as_str());
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Successfully created credential reset token for {}: {}",
|
"Successfully created credential reset token for {}: {}",
|
||||||
aopts.account_id, cuintent_token.token
|
aopts.account_id, token
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"The person can use one of the following to allow the credential reset"
|
"The person can use one of the following to allow the credential reset"
|
||||||
|
@ -700,7 +697,19 @@ impl AccountCredential {
|
||||||
println!("This link: {}", url.as_str());
|
println!("This link: {}", url.as_str());
|
||||||
println!(
|
println!(
|
||||||
"Or run this command: kanidm person credential use-reset-token {}",
|
"Or run this command: kanidm person credential use-reset-token {}",
|
||||||
cuintent_token.token
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now get the abs time
|
||||||
|
let local_offset =
|
||||||
|
UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||||
|
let expiry_time = expiry_time.to_offset(local_offset);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"This token will expire at: {}",
|
||||||
|
expiry_time
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.expect("Failed to format date time!!!")
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
|
@ -464,6 +464,7 @@ pub enum AccountCredential {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
copt: CommonOpt,
|
copt: CommonOpt,
|
||||||
/// Optionally set how many seconds the reset token should be valid for.
|
/// Optionally set how many seconds the reset token should be valid for.
|
||||||
|
/// Default: 3600 seconds
|
||||||
ttl: Option<u32>,
|
ttl: Option<u32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue