diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 3175202a2..e664019d3 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -1807,7 +1807,7 @@ impl KanidmClient { pub async fn idm_account_credential_update_exchange( &self, - intent_token: CUIntentToken, + intent_token: String, ) -> Result<(CUSessionToken, CUStatus), ClientError> { // 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) diff --git a/proto/src/internal/credupdate.rs b/proto/src/internal/credupdate.rs index c695573ac..7a0201bc5 100644 --- a/proto/src/internal/credupdate.rs +++ b/proto/src/internal/credupdate.rs @@ -64,6 +64,8 @@ impl TotpSecret { #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct CUIntentToken { pub token: String, + #[serde(with = "time::serde::timestamp")] + pub expiry_time: time::OffsetDateTime, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs index b27f3afa8..15056d1e6 100644 --- a/server/core/src/actors/v1_write.rs +++ b/server/core/src/actors/v1_write.rs @@ -17,8 +17,8 @@ use kanidmd_lib::{ filter::{Filter, FilterInvalid}, idm::account::DestroySessionTokenEvent, idm::credupdatesession::{ - CredentialUpdateIntentToken, CredentialUpdateSessionToken, InitCredentialUpdateEvent, - InitCredentialUpdateIntentEvent, + CredentialUpdateIntentTokenExchange, CredentialUpdateSessionToken, + InitCredentialUpdateEvent, InitCredentialUpdateIntentEvent, }, idm::event::{GeneratePasswordEvent, RegenerateRadiusSecretEvent, UnixPasswordChangeEvent}, idm::oauth2::{ @@ -669,6 +669,7 @@ impl QueryServerWriteV1 { }) .map(|tok| CUIntentToken { token: tok.intent_id, + expiry_time: tok.expiry_time, }) } @@ -679,14 +680,12 @@ impl QueryServerWriteV1 { )] pub async fn handle_idmcredentialexchangeintent( &self, - intent_token: CUIntentToken, + intent_id: String, eventid: Uuid, ) -> Result<(CUSessionToken, CUStatus), OperationError> { let ct = duration_from_epoch_now(); let mut idms_prox_write = self.idms.proxy_write(ct).await?; - let intent_token = CredentialUpdateIntentToken { - intent_id: intent_token.token, - }; + let intent_token = CredentialUpdateIntentTokenExchange { intent_id }; // TODO: this is throwing a 500 error when a session is already in use, that seems bad? idms_prox_write .exchange_intent_credential_update(intent_token, ct) diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index cbf6783c1..b7a5a8ae6 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1340,7 +1340,7 @@ pub async fn account_user_auth_token_delete( pub async fn credential_update_exchange_intent( State(state): State, Extension(kopid): Extension, - Json(intent_token): Json, + Json(intent_token): Json, ) -> Result, WebError> { state .qe_w_ref diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs index 46f8dbbdc..2e2ac81b3 100644 --- a/server/core/src/https/views/reset.rs +++ b/server/core/src/https/views/reset.rs @@ -19,9 +19,9 @@ use std::fmt::{Display, Formatter}; use uuid::Uuid; use kanidm_proto::internal::{ - CUCredState, CUExtPortal, CUIntentToken, CURegState, CURegWarning, CURequest, CUSessionToken, - CUStatus, CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo, - UserAuthToken, COOKIE_CU_SESSION_TOKEN, + CUCredState, CUExtPortal, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus, + CredentialDetail, OperationError, PasskeyDetail, PasswordFeedback, TotpAlgo, UserAuthToken, + COOKIE_CU_SESSION_TOKEN, }; 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 match state .qe_w_ref - .handle_idmcredentialexchangeintent(CUIntentToken { token }, kopid.eventid) + .handle_idmcredentialexchangeintent(token, kopid.eventid) .await { Ok((cu_session_token, cu_status)) => { diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index cd5572257..b961b2cdb 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -32,12 +32,12 @@ use compact_jwt::jwe::JweBuilder; use super::accountpolicy::ResolvedAccountPolicy; 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. const DEFAULT_INTENT_TTL: Duration = Duration::from_secs(3600); // Default 1 day. const MAXIMUM_INTENT_TTL: Duration = Duration::from_secs(86400); -// Minimum 5 minutes. -const MINIMUM_INTENT_TTL: Duration = Duration::from_secs(300); #[derive(Debug)] pub enum PasswordQuality { @@ -50,6 +50,20 @@ pub enum PasswordQuality { #[derive(Clone, Debug)] pub struct CredentialUpdateIntentToken { pub intent_id: String, + pub expiry_time: OffsetDateTime, +} + +#[derive(Clone, Debug)] +pub struct CredentialUpdateIntentTokenExchange { + pub intent_id: String, +} + +impl From for CredentialUpdateIntentTokenExchange { + fn from(tok: CredentialUpdateIntentToken) -> Self { + CredentialUpdateIntentTokenExchange { + intent_id: tok.intent_id, + } + } } #[derive(Serialize, Deserialize, Debug)] @@ -932,8 +946,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { let mttl = event.max_ttl.unwrap_or(DEFAULT_INTENT_TTL); let clamped_mttl = mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL); debug!(?clamped_mttl, "clamped update intent validity"); + // Absolute expiry of the intent token in epoch seconds 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(); // Mark that we have created an intent token on the user. @@ -984,15 +1002,18 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { e })?; - Ok(CredentialUpdateIntentToken { intent_id }) + Ok(CredentialUpdateIntentToken { + intent_id, + expiry_time, + }) } pub fn exchange_intent_credential_update( &mut self, - token: CredentialUpdateIntentToken, + token: CredentialUpdateIntentTokenExchange, current_time: Duration, ) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> { - let CredentialUpdateIntentToken { intent_id } = token; + let CredentialUpdateIntentTokenExchange { intent_id } = token; /* let entry = self.qs_write.internal_search_uuid(&token.target)?; @@ -2633,24 +2654,24 @@ mod tests { // exchange intent token - invalid - fail // Expired 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))); 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))); // exchange intent token - success 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(); // Session in progress - This will succeed and then block the former success from // committing. 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(); let cur = idms_prox_write.commit_credential_update(&cust_a, ct); diff --git a/server/testkit/tests/proto_v1_test.rs b/server/testkit/tests/proto_v1_test.rs index 33f794cd3..a07061bcb 100644 --- a/server/testkit/tests/proto_v1_test.rs +++ b/server/testkit/tests/proto_v1_test.rs @@ -1069,7 +1069,7 @@ async fn test_server_credential_update_session_pw(rsclient: KanidmClient) { let _ = rsclient.logout().await; // Exchange the intent token let (session_token, _status) = rsclient - .idm_account_credential_update_exchange(intent_token) + .idm_account_credential_update_exchange(intent_token.token) .await .unwrap(); @@ -1144,7 +1144,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) { let _ = rsclient.logout().await; // Exchange the intent token let (session_token, _statu) = rsclient - .idm_account_credential_update_exchange(intent_token) + .idm_account_credential_update_exchange(intent_token.token) .await .unwrap(); @@ -1273,7 +1273,7 @@ async fn setup_demo_account_passkey(rsclient: &KanidmClient) -> WebauthnAuthenti let _ = rsclient.logout().await; // Exchange the intent token let (session_token, _status) = rsclient - .idm_account_credential_update_exchange(intent_token) + .idm_account_credential_update_exchange(intent_token.token) .await .unwrap(); @@ -1359,7 +1359,7 @@ async fn setup_demo_account_password( // Exchange the intent token let (session_token, _status) = rsclient - .idm_account_credential_update_exchange(intent_token) + .idm_account_credential_update_exchange(intent_token.token) .await .expect("Failed to exchange intent token"); @@ -1611,7 +1611,7 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) { let _ = rsclient.logout().await; // Exchange the intent token let (session_token, _status) = rsclient - .idm_account_credential_update_exchange(intent_token) + .idm_account_credential_update_exchange(intent_token.token) .await .unwrap(); diff --git a/server/web_ui/user/src/credential/reset.rs b/server/web_ui/user/src/credential/reset.rs index 698963d1b..e2d3bb4cc 100644 --- a/server/web_ui/user/src/credential/reset.rs +++ b/server/web_ui/user/src/credential/reset.rs @@ -816,7 +816,7 @@ impl CredentialResetApp { } async fn exchange_intent_token(token: String) -> Result { - let request = CUIntentToken { token }; + let request = token; let req_jsvalue = request .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) .expect("Failed to serialise request"); diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 2bbb5b229..0f56dbb63 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -49,7 +49,7 @@ rpassword = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } shellexpand = { workspace = true } -time = { workspace = true, features = ["serde", "std"] } +time = { workspace = true, features = ["serde", "std", "local-offset"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } tokio = { workspace = true, features = ["rt", "macros", "fs", "signal"] } diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index 52a57f9a0..98be76acf 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -21,7 +21,7 @@ use kanidm_proto::scim_v1::{client::ScimSshPublicKeys, ScimEntryGetQuery}; use qrcode::render::unicode; use qrcode::QrCode; use time::format_description::well_known::Rfc3339; -use time::OffsetDateTime; +use time::{OffsetDateTime, UtcOffset}; use uuid::Uuid; use crate::webauthn::get_authenticator; @@ -637,9 +637,7 @@ impl AccountCredential { // The account credential use_reset_token CLI AccountCredential::UseResetToken(aopt) => { let client = aopt.copt.to_unauth_client(); - let cuintent_token = CUIntentToken { - token: aopt.token.clone(), - }; + let cuintent_token = aopt.token.clone(); match client .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) .await { - Ok(cuintent_token) => { + Ok(CUIntentToken { token, expiry_time }) => { let mut url = client.make_url("/ui/reset"); - url.query_pairs_mut() - .append_pair("token", cuintent_token.token.as_str()); + url.query_pairs_mut().append_pair("token", token.as_str()); debug!( "Successfully created credential reset token for {}: {}", - aopts.account_id, cuintent_token.token + aopts.account_id, token ); println!( "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!( "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!(); } diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 31899779e..97d88a6f8 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -464,6 +464,7 @@ pub enum AccountCredential { #[clap(flatten)] copt: CommonOpt, /// Optionally set how many seconds the reset token should be valid for. + /// Default: 3600 seconds ttl: Option, }, }