20241017 3107 token ttl (#3114)

This commit is contained in:
Firstyear 2024-10-18 13:28:52 +10:00 committed by GitHub
parent 99a799d72a
commit 5a3e5f1e07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 69 additions and 37 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"] }

View file

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

View file

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