use crate::prelude::*;
use crate::credential::softlock::CredSoftLock;
use crate::idm::account::Account;
use crate::idm::authsession::{AuthSession, AuthSessionData};
use crate::idm::event::AuthResult;
use crate::idm::server::IdmServerAuthTransaction;
use crate::idm::AuthState;
use crate::utils::uuid_from_duration;
use std::sync::Arc;
use tokio::sync::Mutex;
use kanidm_proto::v1::AuthIssueSession;
use super::server::CredSoftLockMutex;
impl<'a> IdmServerAuthTransaction<'a> {
pub async fn reauth_init(
&mut self,
ident: Identity,
issue: AuthIssueSession,
ct: Duration,
client_auth_info: ClientAuthInfo,
) -> Result<AuthResult, OperationError> {
let Some(entry) = ident.get_user_entry() else {
error!("Ident is not a user and has no entry associated. Unable to proceed.");
return Err(OperationError::InvalidState);
};
let (account, account_policy) =
Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;
security_info!(
username = %account.name,
issue = ?issue,
uuid = %account.uuid,
"Initiating Re-Authentication Session",
);
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&ident.session_id))
.ok_or_else(|| {
error!("Ident session is not present in entry. Perhaps replication is delayed?");
OperationError::InvalidState
})?;
match session.scope {
SessionScope::PrivilegeCapable => {
}
SessionScope::ReadOnly | SessionScope::ReadWrite | SessionScope::Synchronise => {
error!("Session scope is not PrivilegeCapable and can not be used in re-auth.");
return Err(OperationError::InvalidState);
}
};
let session_cred_id = session.cred_id;
let sessionid = uuid_from_duration(ct, self.sid);
let _session_ticket = self.session_ticket.acquire().await;
let maybe_slock = account
.primary_cred_uuid_and_policy()
.and_then(|(cred_uuid, policy)| {
if cred_uuid == session_cred_id {
let mut softlock_write = self.softlocks.write();
let slock_ref: CredSoftLockMutex =
if let Some(slock_ref) = softlock_write.get(&cred_uuid) {
slock_ref.clone()
} else {
let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
softlock_write.insert(cred_uuid, slock.clone());
slock
};
softlock_write.commit();
Some(slock_ref)
} else {
None
}
});
let is_valid = if let Some(slock_ref) = maybe_slock {
let mut slock = slock_ref.lock().await;
slock.apply_time_step(ct);
slock.is_valid()
} else {
true
};
if !is_valid {
warn!(
"Credential {:?} is currently softlocked, unable to proceed",
session_cred_id
);
return Ok(AuthResult {
sessionid: ident.get_session_id(),
state: AuthState::Denied("Credential is temporarily locked".to_string()),
});
}
let asd: AuthSessionData = AuthSessionData {
account,
account_policy,
issue,
webauthn: self.webauthn,
ct,
client_auth_info,
};
let domain_keys = self.qs_read.get_domain_key_object_handle()?;
let (auth_session, state) =
AuthSession::new_reauth(asd, ident.session_id, session, session_cred_id, domain_keys);
match auth_session {
Some(auth_session) => {
let mut session_write = self.sessions.write();
if session_write.contains_key(&sessionid) {
Err(OperationError::InvalidSessionState)
} else {
session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
debug_assert!(session_write.get(&sessionid).is_some());
Ok(())
}?;
session_write.commit();
}
None => {
security_info!("Authentication Session Unable to begin");
}
};
Ok(AuthResult { sessionid, state })
}
}
#[cfg(test)]
mod tests {
use crate::credential::totp::Totp;
use crate::idm::audit::AuditEvent;
use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus};
use crate::idm::delayed::DelayedAction;
use crate::idm::event::{AuthEvent, AuthResult};
use crate::idm::server::IdmServerTransaction;
use crate::idm::AuthState;
use crate::prelude::*;
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
use compact_jwt::JwsCompact;
use uuid::uuid;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
async fn setup_testaccount(idms: &IdmServer, ct: Duration) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson")),
(Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
(Attribute::Description, Value::new_utf8s("testperson")),
(Attribute::DisplayName, Value::new_utf8s("testperson"))
);
let cr = idms_prox_write.qs_write.internal_create(vec![e2]);
assert!(cr.is_ok());
assert!(idms_prox_write.commit().is_ok());
}
async fn setup_testaccount_passkey(
idms: &IdmServer,
ct: Duration,
) -> WebauthnAuthenticator<SoftPasskey> {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let (cust, _c_status) = idms_prox_write
.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
)
.expect("Failed to begin credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
let cutxn = idms.cred_update_transaction().await.unwrap();
let origin = cutxn.get_origin().clone();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let c_status = cutxn
.credential_passkey_init(&cust, ct)
.expect("Failed to initiate passkey registration");
let passkey_chal = match c_status.mfaregstate() {
MfaRegStateStatus::Passkey(c) => Some(c),
_ => None,
}
.expect("Unable to access passkey challenge, invalid state");
let passkey_resp = wa
.do_registration(origin.clone(), passkey_chal.clone())
.expect("Failed to create soft passkey");
let label = "softtoken".to_string();
let c_status = cutxn
.credential_passkey_finish(&cust, ct, label, &passkey_resp)
.expect("Failed to initiate passkey registration");
assert!(c_status.can_commit());
drop(cutxn);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
idms_prox_write
.commit_credential_update(&cust, ct)
.expect("Failed to commit credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
wa
}
async fn setup_testaccount_password_totp(idms: &IdmServer, ct: Duration) -> (String, Totp) {
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let (cust, _c_status) = idms_prox_write
.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
)
.expect("Failed to begin credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
let cutxn = idms.cred_update_transaction().await.unwrap();
let pw = crate::utils::password_from_random();
let _c_status = cutxn
.credential_primary_set_password(&cust, ct, &pw)
.expect("Failed to update the primary cred password");
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
let totp_token: Totp = match c_status.mfaregstate() {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.clone().try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate(), MfaRegStateStatus::None));
assert!(c_status.can_commit());
drop(cutxn);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
idms_prox_write
.commit_credential_update(&cust, ct)
.expect("Failed to commit credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
(pw, totp_token)
}
async fn auth_passkey(
idms: &IdmServer,
ct: Duration,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
idms_delayed: &mut IdmServerDelayed,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let origin = idms_auth.get_origin().clone();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
trace!(?state);
let rcr = match state {
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Passkey(rcr)) => rcr,
_ => unreachable!(),
},
_ => unreachable!(),
};
trace!(?rcr);
let resp = wa
.do_authentication(origin, rcr)
.expect("failed to use softtoken to authenticate");
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
let r3 = idms_auth
.auth(&passkey_step, ct, Source::Internal.into())
.await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
Some(*token)
}
_ => None,
}
}
async fn auth_password_totp(
idms: &IdmServer,
ct: Duration,
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth
.auth(&auth_init, ct, Source::Internal.into())
.await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
let r2 = idms_auth
.auth(&auth_begin, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let totp = token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth
.auth(&totp_step, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
Some(*token)
}
_ => None,
}
}
async fn token_to_ident(
idms: &IdmServer,
ct: Duration,
client_auth_info: ClientAuthInfo,
) -> Identity {
let mut idms_prox_read = idms.proxy_read().await.unwrap();
idms_prox_read
.validate_client_auth_info_to_ident(client_auth_info, ct)
.expect("Invalid UAT")
}
async fn reauth_passkey(
idms: &IdmServer,
ct: Duration,
ident: &Identity,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
idms_delayed: &mut IdmServerDelayed,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let origin = idms_auth.get_origin().clone();
let auth_allowed = idms_auth
.reauth_init(
ident.clone(),
AuthIssueSession::Token,
ct,
Source::Internal.into(),
)
.await
.expect("Failed to start reauth.");
let AuthResult { sessionid, state } = auth_allowed;
trace!(?state);
let rcr = match state {
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Passkey(rcr)) => rcr,
_ => return None,
},
_ => unreachable!(),
};
trace!(?rcr);
let resp = wa
.do_authentication(origin, rcr)
.expect("failed to use softtoken to authenticate");
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
let r3 = idms_auth
.auth(&passkey_step, ct, Source::Internal.into())
.await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
Some(*token)
}
_ => unreachable!(),
}
}
async fn reauth_password_totp(
idms: &IdmServer,
ct: Duration,
ident: &Identity,
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await.unwrap();
let auth_allowed = idms_auth
.reauth_init(
ident.clone(),
AuthIssueSession::Token,
ct,
Source::Internal.into(),
)
.await
.expect("Failed to start reauth.");
let AuthResult { sessionid, state } = auth_allowed;
trace!(?state);
match state {
AuthState::Denied(reason) => {
trace!("{}", reason);
return None;
}
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Totp) => {}
_ => return None,
},
_ => unreachable!(),
};
let totp = token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth
.auth(&totp_step, ct, Source::Internal.into())
.await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(*token)
}
_ => None,
}
}
#[idm_test]
async fn test_idm_reauth_passkey(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
let ct = duration_from_epoch_now();
setup_testaccount(idms, ct).await;
let mut passkey = setup_testaccount_passkey(idms, ct).await;
let token = auth_passkey(idms, ct, &mut passkey, idms_delayed)
.await
.expect("failed to authenticate with passkey");
let ident = token_to_ident(idms, ct, token.clone().into()).await;
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
let session = ident.get_session().expect("Unable to access sessions");
assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
let token = reauth_passkey(idms, ct, &ident, &mut passkey, idms_delayed)
.await
.expect("Failed to get new session token");
let ident = token_to_ident(idms, ct, token.clone().into()).await;
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
}
#[idm_test(audit = 1)]
async fn test_idm_reauth_softlocked_pw(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
) {
let ct = duration_from_epoch_now();
setup_testaccount(idms, ct).await;
let (pw, totp) = setup_testaccount_password_totp(idms, ct).await;
let token = auth_password_totp(idms, ct, &pw, &totp, idms_delayed)
.await
.expect("failed to authenticate with passkey");
let ident = token_to_ident(idms, ct, token.into()).await;
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
let session = ident.get_session().expect("Unable to access sessions");
assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
assert!(reauth_password_totp(
idms,
ct,
&ident,
"absolutely-wrong-password",
&totp,
idms_delayed
)
.await
.is_none());
match idms_audit.audit_rx().try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
assert!(
reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)
.await
.is_none()
);
}
}