use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use compact_jwt::Jws;
use hashbrown::HashSet;
use kanidm_proto::internal::UserAuthToken;
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
use nonempty::NonEmpty;
use tokio::sync::mpsc::UnboundedSender as Sender;
use uuid::Uuid;
use webauthn_rs::prelude::{
AttestationCaList, AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyAuthentication,
CredentialID, Passkey as PasskeyV4, PasskeyAuthentication, RequestChallengeResponse,
SecurityKeyAuthentication, Webauthn,
};
use crate::credential::totp::Totp;
use crate::credential::{BackupCodes, Credential, CredentialType, Password};
use crate::idm::account::Account;
use crate::idm::audit::AuditEvent;
use crate::idm::delayed::{
AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement,
};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyObject;
use crate::value::{AuthType, Session, SessionState};
use time::OffsetDateTime;
use super::accountpolicy::ResolvedAccountPolicy;
const BAD_PASSWORD_MSG: &str = "incorrect password";
const BAD_TOTP_MSG: &str = "incorrect totp";
const BAD_WEBAUTHN_MSG: &str = "invalid webauthn authentication";
const BAD_ACCOUNT_POLICY: &str = "the credential no longer meets account policy requirements";
const BAD_BACKUPCODE_MSG: &str = "invalid backup code";
const BAD_AUTH_TYPE_MSG: &str = "invalid authentication method in this context";
const BAD_CREDENTIALS: &str = "invalid credential message";
const ACCOUNT_EXPIRED: &str = "account expired";
const PW_BADLIST_MSG: &str = "password is in badlist";
#[derive(Debug, Clone)]
enum AuthIntent {
InitialAuth {
privileged: bool,
},
Reauth {
session_id: Uuid,
session_expiry: Option<OffsetDateTime>,
},
}
enum CredState {
Success { auth_type: AuthType, cred_id: Uuid },
Continue(Box<NonEmpty<AuthAllowed>>),
Denied(&'static str),
}
#[derive(Clone, Debug, PartialEq)]
enum CredVerifyState {
Init,
Success,
Fail,
}
#[derive(Clone, Debug)]
struct CredTotp {
pw: Password,
pw_state: CredVerifyState,
totp: BTreeMap<String, Totp>,
mfa_state: CredVerifyState,
}
#[derive(Clone, Debug)]
struct CredBackupCode {
pw: Password,
pw_state: CredVerifyState,
backup_code: BackupCodes,
mfa_state: CredVerifyState,
}
#[derive(Clone, Debug)]
struct CredSecurityKey {
pw: Password,
pw_state: CredVerifyState,
chal: RequestChallengeResponse,
ska: SecurityKeyAuthentication,
mfa_state: CredVerifyState,
}
#[derive(Clone, Debug)]
struct CredPasskey {
chal: RequestChallengeResponse,
wan_state: PasskeyAuthentication,
state: CredVerifyState,
}
#[derive(Clone, Debug)]
struct CredAttestedPasskey {
chal: RequestChallengeResponse,
wan_state: AttestedPasskeyAuthentication,
state: CredVerifyState,
}
#[derive(Clone, Debug)]
enum CredHandler {
Anonymous {
cred_id: Uuid,
},
Password {
pw: Password,
generated: bool,
cred_id: Uuid,
},
PasswordTotp {
cmfa: CredTotp,
cred_id: Uuid,
},
PasswordBackupCode {
cmfa: CredBackupCode,
cred_id: Uuid,
},
PasswordSecurityKey {
cmfa: CredSecurityKey,
cred_id: Uuid,
},
Passkey {
c_wan: CredPasskey,
cred_ids: BTreeMap<CredentialID, Uuid>,
},
AttestedPasskey {
c_wan: CredAttestedPasskey,
att_ca_list: AttestationCaList,
creds: BTreeMap<AttestedPasskeyV4, Uuid>,
},
}
impl CredHandler {
fn build_from_set_passkey(
wan: impl Iterator<Item = (Uuid, PasskeyV4)>,
webauthn: &Webauthn,
) -> Option<Self> {
let mut pks = Vec::with_capacity(wan.size_hint().0);
let mut cred_ids = BTreeMap::default();
for (uuid, pk) in wan {
cred_ids.insert(pk.cred_id().clone(), uuid);
pks.push(pk);
}
if pks.is_empty() {
debug!("Account does not have any passkeys");
return None;
};
webauthn
.start_passkey_authentication(&pks)
.map(|(chal, wan_state)| CredHandler::Passkey {
c_wan: CredPasskey {
chal,
wan_state,
state: CredVerifyState::Init,
},
cred_ids,
})
.map_err(|e| {
security_info!(
?e,
"Unable to create passkey webauthn authentication challenge"
);
})
.ok()
}
fn build_from_single_passkey(
cred_id: Uuid,
pk: PasskeyV4,
webauthn: &Webauthn,
) -> Option<Self> {
let cred_ids = btreemap!((pk.cred_id().clone(), cred_id));
let pks = vec![pk];
webauthn
.start_passkey_authentication(pks.as_slice())
.map(|(chal, wan_state)| CredHandler::Passkey {
c_wan: CredPasskey {
chal,
wan_state,
state: CredVerifyState::Init,
},
cred_ids,
})
.map_err(|e| {
security_info!(
?e,
"Unable to create passkey webauthn authentication challenge"
);
})
.ok()
}
fn build_from_set_attested_pk(
wan: &BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
att_ca_list: &AttestationCaList,
webauthn: &Webauthn,
) -> Option<Self> {
if wan.is_empty() {
debug!("Account does not have any attested passkeys");
return None;
};
let pks: Vec<_> = wan.values().map(|(_, k)| k).cloned().collect();
let creds: BTreeMap<_, _> = wan.iter().map(|(u, (_, k))| (k.clone(), *u)).collect();
webauthn
.start_attested_passkey_authentication(&pks)
.map(|(chal, wan_state)| CredHandler::AttestedPasskey {
c_wan: CredAttestedPasskey {
chal,
wan_state,
state: CredVerifyState::Init,
},
att_ca_list: att_ca_list.clone(),
creds,
})
.map_err(|e| {
security_info!(
?e,
"Unable to create attested passkey webauthn authentication challenge"
);
})
.ok()
}
fn build_from_single_attested_pk(
cred_id: Uuid,
pk: &AttestedPasskeyV4,
att_ca_list: &AttestationCaList,
webauthn: &Webauthn,
) -> Option<Self> {
let creds = btreemap!((pk.clone(), cred_id));
let pks = vec![pk.clone()];
webauthn
.start_attested_passkey_authentication(pks.as_slice())
.map(|(chal, wan_state)| CredHandler::AttestedPasskey {
c_wan: CredAttestedPasskey {
chal,
wan_state,
state: CredVerifyState::Init,
},
att_ca_list: att_ca_list.clone(),
creds,
})
.map_err(|e| {
security_info!(
?e,
"Unable to create attested passkey webauthn authentication challenge"
);
})
.ok()
}
fn build_from_password_totp(cred: &Credential) -> Option<Self> {
match &cred.type_ {
CredentialType::PasswordMfa(pw, maybe_totp, _, _) => {
if maybe_totp.is_empty() {
None
} else {
let cmfa = CredTotp {
pw: pw.clone(),
pw_state: CredVerifyState::Init,
totp: maybe_totp
.iter()
.map(|(l, t)| (l.clone(), t.clone()))
.collect(),
mfa_state: CredVerifyState::Init,
};
Some(CredHandler::PasswordTotp {
cmfa,
cred_id: cred.uuid,
})
}
}
_ => None,
}
}
fn build_from_password_backup_code(cred: &Credential) -> Option<Self> {
match &cred.type_ {
CredentialType::PasswordMfa(pw, _, _, Some(backup_code)) => {
let cmfa = CredBackupCode {
pw: pw.clone(),
pw_state: CredVerifyState::Init,
backup_code: backup_code.clone(),
mfa_state: CredVerifyState::Init,
};
Some(CredHandler::PasswordBackupCode {
cmfa,
cred_id: cred.uuid,
})
}
_ => None,
}
}
fn build_from_password_security_key(cred: &Credential, webauthn: &Webauthn) -> Option<Self> {
match &cred.type_ {
CredentialType::PasswordMfa(pw, _, maybe_wan, _) => {
if !maybe_wan.is_empty() {
let sks: Vec<_> = maybe_wan.values().cloned().collect();
let (chal, ska) = webauthn
.start_securitykey_authentication(&sks)
.map_err(|err| {
warn!(?err, "Unable to create webauthn authentication challenge")
})
.ok()?;
let cmfa = CredSecurityKey {
pw: pw.clone(),
pw_state: CredVerifyState::Init,
ska,
chal,
mfa_state: CredVerifyState::Init,
};
Some(CredHandler::PasswordSecurityKey {
cmfa,
cred_id: cred.uuid,
})
} else {
None
}
}
_ => None,
}
}
fn build_from_password_only(cred: &Credential) -> Option<Self> {
match &cred.type_ {
CredentialType::Password(pw) => Some(CredHandler::Password {
pw: pw.clone(),
generated: false,
cred_id: cred.uuid,
}),
CredentialType::GeneratedPassword(pw) => Some(CredHandler::Password {
pw: pw.clone(),
generated: true,
cred_id: cred.uuid,
}),
_ => None,
}
}
fn maybe_pw_upgrade(
pw: &Password,
who: Uuid,
cleartext: &str,
async_tx: &Sender<DelayedAction>,
) {
if pw.requires_upgrade() {
if let Err(_e) = async_tx.send(DelayedAction::PwUpgrade(PasswordUpgrade {
target_uuid: who,
existing_password: cleartext.to_string(),
})) {
admin_warn!("unable to queue delayed pwupgrade, continuing ... ");
};
}
}
fn validate_anonymous(cred: &AuthCredential, cred_id: Uuid) -> CredState {
match cred {
AuthCredential::Anonymous => {
security_debug!("Handler::Anonymous -> Result::Success");
CredState::Success {
auth_type: AuthType::Anonymous,
cred_id,
}
}
_ => {
security_error!(
"Handler::Anonymous -> Result::Denied - invalid cred type for handler"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
fn validate_password(
cred: &AuthCredential,
cred_id: Uuid,
pw: &mut Password,
generated: bool,
who: Uuid,
async_tx: &Sender<DelayedAction>,
pw_badlist_set: &HashSet<String>,
) -> CredState {
match cred {
AuthCredential::Password(cleartext) => {
if pw.verify(cleartext.as_str()).unwrap_or(false) {
if pw_badlist_set.contains(&cleartext.to_lowercase()) {
security_error!("Handler::Password -> Result::Denied - Password found in badlist during login");
CredState::Denied(PW_BADLIST_MSG)
} else {
security_info!("Handler::Password -> Result::Success");
Self::maybe_pw_upgrade(pw, who, cleartext.as_str(), async_tx);
if generated {
CredState::Success {
auth_type: AuthType::GeneratedPassword,
cred_id,
}
} else {
CredState::Success {
auth_type: AuthType::Password,
cred_id,
}
}
}
} else {
security_error!("Handler::Password -> Result::Denied - incorrect password");
CredState::Denied(BAD_PASSWORD_MSG)
}
}
_ => {
security_error!(
"Handler::Password -> Result::Denied - invalid cred type for handler"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
fn validate_password_totp(
cred: &AuthCredential,
cred_id: Uuid,
ts: Duration,
pw_mfa: &mut CredTotp,
who: Uuid,
async_tx: &Sender<DelayedAction>,
pw_badlist_set: &HashSet<String>,
) -> CredState {
match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
(CredVerifyState::Init, CredVerifyState::Init) => {
match cred {
AuthCredential::Totp(totp_chal) => {
if let Some(label) = pw_mfa
.totp
.iter()
.find(|(_, t)| t.verify(*totp_chal, ts))
.map(|(l, _)| l)
{
pw_mfa.mfa_state = CredVerifyState::Success;
security_info!(
"Handler::PasswordMfa -> Result::Continue - TOTP ({}) OK, password -", label
);
CredState::Continue(Box::new(NonEmpty {
head: AuthAllowed::Password,
tail: Vec::with_capacity(0),
}))
} else {
pw_mfa.mfa_state = CredVerifyState::Fail;
security_error!(
"Handler::PasswordMfa -> Result::Denied - TOTP Fail, password -"
);
CredState::Denied(BAD_TOTP_MSG)
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
(CredVerifyState::Success, CredVerifyState::Init) => {
match cred {
AuthCredential::Password(cleartext) => {
if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
if pw_badlist_set.contains(&cleartext.to_lowercase()) {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
CredState::Denied(PW_BADLIST_MSG)
} else {
pw_mfa.pw_state = CredVerifyState::Success;
security_info!("Handler::PasswordMfa -> Result::Success - TOTP OK, password OK");
Self::maybe_pw_upgrade(
&pw_mfa.pw,
who,
cleartext.as_str(),
async_tx,
);
CredState::Success {
auth_type: AuthType::PasswordTotp,
cred_id,
}
}
} else {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!(
"Handler::PasswordMfa -> Result::Denied - TOTP OK, password Fail"
);
CredState::Denied(BAD_PASSWORD_MSG)
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
_ => {
security_error!(
"Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
} fn validate_password_security_key(
cred: &AuthCredential,
cred_id: Uuid,
pw_mfa: &mut CredSecurityKey,
webauthn: &Webauthn,
who: Uuid,
async_tx: &Sender<DelayedAction>,
pw_badlist_set: &HashSet<String>,
) -> CredState {
match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
(CredVerifyState::Init, CredVerifyState::Init) => {
match cred {
AuthCredential::SecurityKey(resp) => {
match webauthn.finish_securitykey_authentication(resp, &pw_mfa.ska) {
Ok(auth_result) => {
pw_mfa.mfa_state = CredVerifyState::Success;
if auth_result.needs_update() {
if let Err(_e) =
async_tx.send(DelayedAction::WebauthnCounterIncrement(
WebauthnCounterIncrement {
target_uuid: who,
auth_result,
},
))
{
admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
};
};
CredState::Continue(Box::new(NonEmpty {
head: AuthAllowed::Password,
tail: Vec::with_capacity(0),
}))
}
Err(e) => {
pw_mfa.mfa_state = CredVerifyState::Fail;
security_error!(
?e,
"Handler::Webauthn -> Result::Denied - webauthn error"
);
CredState::Denied(BAD_WEBAUTHN_MSG)
}
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
(CredVerifyState::Success, CredVerifyState::Init) => {
match cred {
AuthCredential::Password(cleartext) => {
if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
if pw_badlist_set.contains(&cleartext.to_lowercase()) {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
CredState::Denied(PW_BADLIST_MSG)
} else {
pw_mfa.pw_state = CredVerifyState::Success;
security_info!("Handler::PasswordMfa -> Result::Success - SecurityKey OK, password OK");
Self::maybe_pw_upgrade(
&pw_mfa.pw,
who,
cleartext.as_str(),
async_tx,
);
CredState::Success {
auth_type: AuthType::PasswordSecurityKey,
cred_id,
}
}
} else {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - SecurityKey OK, password Fail");
CredState::Denied(BAD_PASSWORD_MSG)
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
_ => {
security_error!(
"Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
fn validate_password_backup_code(
cred: &AuthCredential,
cred_id: Uuid,
pw_mfa: &mut CredBackupCode,
who: Uuid,
async_tx: &Sender<DelayedAction>,
pw_badlist_set: &HashSet<String>,
) -> CredState {
match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
(CredVerifyState::Init, CredVerifyState::Init) => {
match cred {
AuthCredential::BackupCode(code_chal) => {
if pw_mfa.backup_code.verify(code_chal) {
if let Err(_e) =
async_tx.send(DelayedAction::BackupCodeRemoval(BackupCodeRemoval {
target_uuid: who,
code_to_remove: code_chal.to_string(),
}))
{
admin_warn!(
"unable to queue delayed backup code removal, continuing ... "
);
};
pw_mfa.mfa_state = CredVerifyState::Success;
security_info!("Handler::PasswordMfa -> Result::Continue - BackupCode OK, password -");
CredState::Continue(Box::new(NonEmpty {
head: AuthAllowed::Password,
tail: Vec::with_capacity(0),
}))
} else {
pw_mfa.mfa_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - BackupCode Fail, password -");
CredState::Denied(BAD_BACKUPCODE_MSG)
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
(CredVerifyState::Success, CredVerifyState::Init) => {
match cred {
AuthCredential::Password(cleartext) => {
if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
if pw_badlist_set.contains(&cleartext.to_lowercase()) {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
CredState::Denied(PW_BADLIST_MSG)
} else {
pw_mfa.pw_state = CredVerifyState::Success;
security_info!("Handler::PasswordMfa -> Result::Success - BackupCode OK, password OK");
Self::maybe_pw_upgrade(
&pw_mfa.pw,
who,
cleartext.as_str(),
async_tx,
);
CredState::Success {
auth_type: AuthType::PasswordBackupCode,
cred_id,
}
}
} else {
pw_mfa.pw_state = CredVerifyState::Fail;
security_error!("Handler::PasswordMfa -> Result::Denied - BackupCode OK, password Fail");
CredState::Denied(BAD_PASSWORD_MSG)
}
}
_ => {
security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
_ => {
security_error!(
"Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
pub fn validate_passkey(
cred: &AuthCredential,
cred_ids: &BTreeMap<CredentialID, Uuid>,
wan_cred: &mut CredPasskey,
webauthn: &Webauthn,
who: Uuid,
async_tx: &Sender<DelayedAction>,
) -> CredState {
if wan_cred.state != CredVerifyState::Init {
security_error!("Handler::Webauthn -> Result::Denied - Internal State Already Fail");
return CredState::Denied(BAD_WEBAUTHN_MSG);
}
match cred {
AuthCredential::Passkey(resp) => {
match webauthn.finish_passkey_authentication(resp, &wan_cred.wan_state) {
Ok(auth_result) => {
if let Some(cred_id) = cred_ids.get(auth_result.cred_id()).copied() {
wan_cred.state = CredVerifyState::Success;
if auth_result.needs_update() {
if let Err(_e) =
async_tx.send(DelayedAction::WebauthnCounterIncrement(
WebauthnCounterIncrement {
target_uuid: who,
auth_result,
},
))
{
admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
};
};
CredState::Success {
auth_type: AuthType::Passkey,
cred_id,
}
} else {
wan_cred.state = CredVerifyState::Fail;
security_error!("Handler::Webauthn -> Result::Denied - webauthn credential id not found");
CredState::Denied(BAD_WEBAUTHN_MSG)
}
}
Err(e) => {
wan_cred.state = CredVerifyState::Fail;
security_error!(?e, "Handler::Webauthn -> Result::Denied - webauthn error");
CredState::Denied(BAD_WEBAUTHN_MSG)
}
}
}
_ => {
security_error!(
"Handler::Webauthn -> Result::Denied - invalid cred type for handler"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
pub fn validate_attested_passkey(
cred: &AuthCredential,
creds: &BTreeMap<AttestedPasskeyV4, Uuid>,
wan_cred: &mut CredAttestedPasskey,
webauthn: &Webauthn,
who: Uuid,
async_tx: &Sender<DelayedAction>,
att_ca_list: &AttestationCaList,
) -> CredState {
if wan_cred.state != CredVerifyState::Init {
security_error!("Handler::Webauthn -> Result::Denied - Internal State Already Fail");
return CredState::Denied(BAD_WEBAUTHN_MSG);
}
match cred {
AuthCredential::Passkey(resp) => {
match webauthn.finish_attested_passkey_authentication(resp, &wan_cred.wan_state) {
Ok(auth_result) => {
if let Some((apk, cred_id)) = creds.get_key_value(auth_result.cred_id()) {
if let Err(webauthn_err) = apk.verify_attestation(att_ca_list) {
wan_cred.state = CredVerifyState::Fail;
debug!(?webauthn_err);
security_error!("Handler::Webauthn -> Result::Denied - webauthn credential fails attestation");
return CredState::Denied(BAD_ACCOUNT_POLICY);
}
wan_cred.state = CredVerifyState::Success;
if auth_result.needs_update() {
if let Err(_e) =
async_tx.send(DelayedAction::WebauthnCounterIncrement(
WebauthnCounterIncrement {
target_uuid: who,
auth_result,
},
))
{
admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
};
};
CredState::Success {
auth_type: AuthType::AttestedPasskey,
cred_id: *cred_id,
}
} else {
wan_cred.state = CredVerifyState::Fail;
security_error!("Handler::Webauthn -> Result::Denied - webauthn credential id not found");
CredState::Denied(BAD_WEBAUTHN_MSG)
}
}
Err(e) => {
wan_cred.state = CredVerifyState::Fail;
security_error!(?e, "Handler::Webauthn -> Result::Denied - webauthn error");
CredState::Denied(BAD_WEBAUTHN_MSG)
}
}
}
_ => {
security_error!(
"Handler::Webauthn -> Result::Denied - invalid cred type for handler"
);
CredState::Denied(BAD_AUTH_TYPE_MSG)
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn validate(
&mut self,
cred: &AuthCredential,
ts: Duration,
who: Uuid,
async_tx: &Sender<DelayedAction>,
webauthn: &Webauthn,
pw_badlist_set: &HashSet<String>,
) -> CredState {
match self {
CredHandler::Anonymous { cred_id } => Self::validate_anonymous(cred, *cred_id),
CredHandler::Password {
ref mut pw,
generated,
cred_id,
} => Self::validate_password(
cred,
*cred_id,
pw,
*generated,
who,
async_tx,
pw_badlist_set,
),
CredHandler::PasswordTotp {
ref mut cmfa,
cred_id,
} => Self::validate_password_totp(
cred,
*cred_id,
ts,
cmfa,
who,
async_tx,
pw_badlist_set,
),
CredHandler::PasswordBackupCode {
ref mut cmfa,
cred_id,
} => Self::validate_password_backup_code(
cred,
*cred_id,
cmfa,
who,
async_tx,
pw_badlist_set,
),
CredHandler::PasswordSecurityKey {
ref mut cmfa,
cred_id,
} => Self::validate_password_security_key(
cred,
*cred_id,
cmfa,
webauthn,
who,
async_tx,
pw_badlist_set,
),
CredHandler::Passkey {
ref mut c_wan,
cred_ids,
} => Self::validate_passkey(cred, cred_ids, c_wan, webauthn, who, async_tx),
CredHandler::AttestedPasskey {
ref mut c_wan,
ref att_ca_list,
creds,
} => Self::validate_attested_passkey(
cred,
creds,
c_wan,
webauthn,
who,
async_tx,
att_ca_list,
),
}
}
pub fn next_auth_allowed(&self) -> Vec<AuthAllowed> {
match &self {
CredHandler::Anonymous { .. } => vec![AuthAllowed::Anonymous],
CredHandler::Password { .. } => vec![AuthAllowed::Password],
CredHandler::PasswordTotp { .. } => vec![AuthAllowed::Totp],
CredHandler::PasswordBackupCode { .. } => vec![AuthAllowed::BackupCode],
CredHandler::PasswordSecurityKey { ref cmfa, .. } => {
vec![AuthAllowed::SecurityKey(cmfa.chal.clone())]
}
CredHandler::Passkey { c_wan, .. } => vec![AuthAllowed::Passkey(c_wan.chal.clone())],
CredHandler::AttestedPasskey { c_wan, .. } => {
vec![AuthAllowed::Passkey(c_wan.chal.clone())]
}
}
}
fn can_proceed(&self, mech: &AuthMech) -> bool {
match (self, mech) {
(CredHandler::Anonymous { .. }, AuthMech::Anonymous)
| (CredHandler::Password { .. }, AuthMech::Password)
| (CredHandler::PasswordTotp { .. }, AuthMech::PasswordTotp)
| (CredHandler::PasswordBackupCode { .. }, AuthMech::PasswordBackupCode)
| (CredHandler::PasswordSecurityKey { .. }, AuthMech::PasswordSecurityKey)
| (CredHandler::Passkey { .. }, AuthMech::Passkey)
| (CredHandler::AttestedPasskey { .. }, AuthMech::Passkey) => true,
(_, _) => false,
}
}
fn allows_mech(&self) -> AuthMech {
match self {
CredHandler::Anonymous { .. } => AuthMech::Anonymous,
CredHandler::Password { .. } => AuthMech::Password,
CredHandler::PasswordTotp { .. } => AuthMech::PasswordTotp,
CredHandler::PasswordBackupCode { .. } => AuthMech::PasswordBackupCode,
CredHandler::PasswordSecurityKey { .. } => AuthMech::PasswordSecurityKey,
CredHandler::Passkey { .. } => AuthMech::Passkey,
CredHandler::AttestedPasskey { .. } => AuthMech::Passkey,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
enum AuthSessionState {
Init(NonEmpty<CredHandler>),
InProgress(CredHandler),
Success,
Denied(&'static str),
}
impl AuthSessionState {
fn is_denied(&self) -> Option<&'static str> {
match &self {
AuthSessionState::Denied(x) => Some(x),
_ => None,
}
}
}
pub(crate) struct AuthSessionData<'a> {
pub(crate) account: Account,
pub(crate) account_policy: ResolvedAccountPolicy,
pub(crate) issue: AuthIssueSession,
pub(crate) webauthn: &'a Webauthn,
pub(crate) ct: Duration,
pub(crate) client_auth_info: ClientAuthInfo,
}
#[derive(Clone)]
pub(crate) struct AuthSession {
account: Account,
account_policy: ResolvedAccountPolicy,
state: AuthSessionState,
issue: AuthIssueSession,
intent: AuthIntent,
source: Source,
key_object: Arc<KeyObject>,
}
impl AuthSession {
pub fn new(
asd: AuthSessionData<'_>,
privileged: bool,
key_object: Arc<KeyObject>,
) -> (Option<Self>, AuthState) {
let state = if asd.account.is_within_valid_time(asd.ct) {
if asd.account.is_anonymous() {
AuthSessionState::Init(NonEmpty {
head: CredHandler::Anonymous {
cred_id: asd.account.uuid,
},
tail: Vec::with_capacity(0),
})
} else {
let mut handlers = Vec::with_capacity(4);
if let Some(cred) = &asd.account.primary {
if let Some(ch) = CredHandler::build_from_password_totp(cred) {
handlers.push(ch);
}
if let Some(ch) = CredHandler::build_from_password_backup_code(cred) {
handlers.push(ch);
}
if let Some(ch) =
CredHandler::build_from_password_security_key(cred, asd.webauthn)
{
handlers.push(ch);
}
if handlers.is_empty() {
if let Some(ch) = CredHandler::build_from_password_only(cred) {
handlers.push(ch);
}
}
}
trace!(?handlers);
if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
if let Some(ch) = CredHandler::build_from_set_attested_pk(
&asd.account.attested_passkeys,
att_ca_list,
asd.webauthn,
) {
handlers.push(ch);
}
} else {
let credential_iter = asd
.account
.passkeys
.iter()
.map(|(u, (_, pk))| (*u, pk.clone()))
.chain(
asd.account
.attested_passkeys
.iter()
.map(|(u, (_, pk))| (*u, pk.into())),
);
if let Some(ch) =
CredHandler::build_from_set_passkey(credential_iter, asd.webauthn)
{
handlers.push(ch);
}
};
if let Some(non_empty_handlers) = NonEmpty::collect(handlers) {
AuthSessionState::Init(non_empty_handlers)
} else {
security_info!("account has no available credentials");
AuthSessionState::Denied("invalid credential state")
}
}
} else {
security_info!("account expired");
AuthSessionState::Denied(ACCOUNT_EXPIRED)
};
if let Some(reason) = state.is_denied() {
(None, AuthState::Denied(reason.to_string()))
} else {
let auth_session = AuthSession {
account: asd.account,
account_policy: asd.account_policy,
state,
issue: asd.issue,
intent: AuthIntent::InitialAuth { privileged },
source: asd.client_auth_info.source,
key_object,
};
let valid_mechs = auth_session.valid_auth_mechs();
security_debug!(?valid_mechs, "Offering auth mechanisms");
let as_state = AuthState::Choose(valid_mechs);
(Some(auth_session), as_state)
}
}
pub(crate) fn new_reauth(
asd: AuthSessionData<'_>,
session_id: Uuid,
session: &Session,
cred_id: Uuid,
key_object: Arc<KeyObject>,
) -> (Option<Self>, AuthState) {
enum State {
Expired,
NoMatchingCred,
Proceed(CredHandler),
}
let state = if asd.account.is_within_valid_time(asd.ct) {
let mut cred_handler = None;
match session.type_ {
AuthType::Password
| AuthType::GeneratedPassword
| AuthType::PasswordBackupCode => {
if let Some(primary) = asd.account.primary.as_ref() {
if primary.uuid == cred_id {
cred_handler = CredHandler::build_from_password_only(primary)
}
}
}
AuthType::PasswordTotp => {
if let Some(primary) = asd.account.primary.as_ref() {
if primary.uuid == cred_id {
cred_handler = CredHandler::build_from_password_totp(primary)
}
}
}
AuthType::PasswordSecurityKey => {
if let Some(primary) = asd.account.primary.as_ref() {
if primary.uuid == cred_id {
cred_handler =
CredHandler::build_from_password_security_key(primary, asd.webauthn)
}
}
}
AuthType::Passkey => {
let maybe_pk: Option<PasskeyV4> = asd
.account
.attested_passkeys
.get(&cred_id)
.map(|(_, apk)| apk.into())
.or_else(|| asd.account.passkeys.get(&cred_id).map(|(_, pk)| pk.clone()));
if let Some(pk) = maybe_pk {
if let Some(ch) =
CredHandler::build_from_single_passkey(cred_id, pk, asd.webauthn)
{
debug_assert!(cred_handler.is_none());
cred_handler = Some(ch);
} else {
security_critical!(
"corrupt credentials, unable to start passkey credhandler"
);
}
}
}
AuthType::AttestedPasskey => {
if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
if let Some(pk) = asd
.account
.attested_passkeys
.get(&cred_id)
.map(|(_, pk)| pk)
{
if let Some(ch) = CredHandler::build_from_single_attested_pk(
cred_id,
pk,
att_ca_list,
asd.webauthn,
) {
debug_assert!(cred_handler.is_none());
cred_handler = Some(ch);
} else {
security_critical!(
"corrupt credentials, unable to start attested passkey credhandler"
);
}
}
}
}
AuthType::Anonymous => {}
}
if let Some(cred_handler) = cred_handler {
State::Proceed(cred_handler)
} else {
State::NoMatchingCred
}
} else {
State::Expired
};
let session_expiry = match session.state {
SessionState::ExpiresAt(odt) => Some(odt),
SessionState::NeverExpires => None,
SessionState::RevokedAt(_) => {
security_error!(
"Invalid State - Should not be possible to trigger re-auth on revoked session."
);
return (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()));
}
};
match state {
State::Proceed(handler) => {
let allow = handler.next_auth_allowed();
let auth_session = AuthSession {
account: asd.account,
account_policy: asd.account_policy,
state: AuthSessionState::InProgress(handler),
issue: asd.issue,
intent: AuthIntent::Reauth {
session_id,
session_expiry,
},
source: asd.client_auth_info.source,
key_object,
};
let as_state = AuthState::Continue(allow);
(Some(auth_session), as_state)
}
State::Expired => {
security_info!("account expired");
(None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()))
}
State::NoMatchingCred => {
security_error!("Unable to select a credential for authentication");
(None, AuthState::Denied(BAD_CREDENTIALS.to_string()))
}
}
}
pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
match &self.state {
AuthSessionState::InProgress(CredHandler::Password { cred_id, .. })
| AuthSessionState::InProgress(CredHandler::PasswordTotp { cred_id, .. })
| AuthSessionState::InProgress(CredHandler::PasswordBackupCode { cred_id, .. }) => {
Ok(Some(*cred_id))
}
AuthSessionState::InProgress(CredHandler::Anonymous { .. })
| AuthSessionState::InProgress(CredHandler::PasswordSecurityKey { .. })
| AuthSessionState::InProgress(CredHandler::Passkey { .. })
| AuthSessionState::InProgress(CredHandler::AttestedPasskey { .. }) => Ok(None),
AuthSessionState::Init(_) => {
debug!(
"Request for credential uuid invalid as auth session state not yet initialised"
);
Err(OperationError::AU0001InvalidState)
}
AuthSessionState::Success | AuthSessionState::Denied(_) => {
debug!("Request for credential uuid invalid as auth session state has progressed");
Err(OperationError::AU0001InvalidState)
}
}
}
pub fn start_session(
&mut self,
mech: &AuthMech,
) -> Result<AuthState, OperationError> {
let (next_state, response) = match &mut self.state {
AuthSessionState::Success
| AuthSessionState::Denied(_)
| AuthSessionState::InProgress(_) => (
None,
Err(OperationError::InvalidAuthState(
"session already finalised!".to_string(),
)),
),
AuthSessionState::Init(handlers) => {
let mut allowed_handlers: Vec<_> = handlers
.iter()
.filter(|ch| ch.can_proceed(mech))
.cloned()
.collect();
if let Some(allowed_handler) = allowed_handlers.pop() {
let allowed: Vec<_> = allowed_handler.next_auth_allowed();
if allowed.is_empty() {
security_info!("Unable to negotiate credentials");
(
None,
Err(OperationError::InvalidAuthState(
"unable to negotiate credentials".to_string(),
)),
)
} else {
(
Some(AuthSessionState::InProgress(allowed_handler)),
Ok(AuthState::Continue(allowed)),
)
}
} else {
security_error!("Unable to select a credential for authentication");
(
Some(AuthSessionState::Denied(BAD_CREDENTIALS)),
Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())),
)
}
}
};
if let Some(mut next_state) = next_state {
std::mem::swap(&mut self.state, &mut next_state);
};
response
}
pub fn validate_creds(
&mut self,
cred: &AuthCredential,
time: Duration,
async_tx: &Sender<DelayedAction>,
audit_tx: &Sender<AuditEvent>,
webauthn: &Webauthn,
pw_badlist: &HashSet<String>,
) -> Result<AuthState, OperationError> {
let (next_state, response) = match &mut self.state {
AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => {
return Err(OperationError::InvalidAuthState(
"session already finalised!".to_string(),
));
}
AuthSessionState::InProgress(ref mut handler) => {
match handler.validate(
cred,
time,
self.account.uuid,
async_tx,
webauthn,
pw_badlist,
) {
CredState::Success { auth_type, cred_id } => {
let uat = self.issue_uat(auth_type, time, async_tx, cred_id)?;
let jwt = Jws::into_json(&uat).map_err(|e| {
admin_error!(?e, "Failed to serialise into Jws");
OperationError::AU0002JwsSerialisation
})?;
let token = self.key_object.jws_es256_sign(&jwt, time).map_err(|e| {
admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
OperationError::AU0003JwsSignature
})?;
(
Some(AuthSessionState::Success),
Ok(AuthState::Success(Box::new(token), self.issue)),
)
}
CredState::Continue(allowed) => {
security_info!(?allowed, "Request credential continuation");
(None, Ok(AuthState::Continue(allowed.into_iter().collect())))
}
CredState::Denied(reason) => {
if audit_tx
.send(AuditEvent::AuthenticationDenied {
source: self.source.clone().into(),
spn: self.account.spn.clone(),
uuid: self.account.uuid,
time: OffsetDateTime::UNIX_EPOCH + time,
})
.is_err()
{
error!("Unable to submit audit event to queue");
}
security_info!(%reason, "Credentials denied");
(
Some(AuthSessionState::Denied(reason)),
Ok(AuthState::Denied(reason.to_string())),
)
}
}
}
};
if let Some(mut next_state) = next_state {
std::mem::swap(&mut self.state, &mut next_state);
};
response
}
fn issue_uat(
&mut self,
auth_type: AuthType,
time: Duration,
async_tx: &Sender<DelayedAction>,
cred_id: Uuid,
) -> Result<UserAuthToken, OperationError> {
security_debug!("Successful cred handling");
match self.intent {
AuthIntent::InitialAuth { privileged } => {
let session_id = Uuid::new_v4();
let scope = match auth_type {
AuthType::Anonymous => SessionScope::ReadOnly,
AuthType::GeneratedPassword => SessionScope::ReadWrite,
AuthType::Password
| AuthType::PasswordTotp
| AuthType::PasswordBackupCode
| AuthType::PasswordSecurityKey
| AuthType::Passkey
| AuthType::AttestedPasskey => {
if privileged {
SessionScope::ReadWrite
} else {
SessionScope::PrivilegeCapable
}
}
};
security_info!(
"Issuing {:?} session ({:?}) {} for {} {}",
self.issue,
scope,
session_id,
self.account.spn,
self.account.uuid
);
let uat = self
.account
.to_userauthtoken(session_id, scope, time, &self.account_policy)
.ok_or(OperationError::AU0004UserAuthTokenInvalid)?;
match auth_type {
AuthType::Anonymous => {
}
AuthType::Password
| AuthType::GeneratedPassword
| AuthType::PasswordTotp
| AuthType::PasswordBackupCode
| AuthType::PasswordSecurityKey
| AuthType::Passkey
| AuthType::AttestedPasskey => {
trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
target_uuid: self.account.uuid,
session_id,
cred_id,
label: "Auth Session".to_string(),
expiry: uat.expiry,
issued_at: uat.issued_at,
issued_by: IdentityId::User(self.account.uuid),
scope,
type_: auth_type,
}))
.map_err(|e| {
debug!(?e, "queue failure");
admin_error!("unable to queue failing authentication as the session will not validate ... ");
OperationError::AU0005DelayedProcessFailure
})?;
}
};
Ok(uat)
}
AuthIntent::Reauth {
session_id,
session_expiry,
} => {
let scope = match auth_type {
AuthType::Anonymous | AuthType::GeneratedPassword => {
error!("AuthType used in Reauth is not valid for session re-issuance. Rejecting");
return Err(OperationError::AU0006CredentialMayNotReauthenticate);
}
AuthType::Password
| AuthType::PasswordTotp
| AuthType::PasswordBackupCode
| AuthType::PasswordSecurityKey
| AuthType::Passkey
| AuthType::AttestedPasskey => SessionScope::PrivilegeCapable,
};
let uat = self
.account
.to_reissue_userauthtoken(
session_id,
session_expiry,
scope,
time,
&self.account_policy,
)
.ok_or(OperationError::AU0007UserAuthTokenInvalid)?;
Ok(uat)
}
}
}
pub fn end_session(&mut self, reason: &'static str) -> Result<AuthState, OperationError> {
let mut next_state = AuthSessionState::Denied(reason);
std::mem::swap(&mut self.state, &mut next_state);
Ok(AuthState::Denied(reason.to_string()))
}
fn valid_auth_mechs(&self) -> Vec<AuthMech> {
match &self.state {
AuthSessionState::Success
| AuthSessionState::Denied(_)
| AuthSessionState::InProgress(_) => Vec::with_capacity(0),
AuthSessionState::Init(handlers) => {
handlers.iter().map(|h| h.allows_mech()).collect()
}
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
use hashbrown::HashSet;
use kanidm_proto::internal::{UatPurpose, UserAuthToken};
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
use tokio::sync::mpsc::unbounded_channel as unbounded;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
use webauthn_rs::prelude::{RequestChallengeResponse, Webauthn};
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::credential::{BackupCodes, Credential};
use crate::idm::account::Account;
use crate::idm::accountpolicy::ResolvedAccountPolicy;
use crate::idm::audit::AuditEvent;
use crate::idm::authsession::{
AuthSession, AuthSessionData, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG,
BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG,
};
use crate::idm::delayed::DelayedAction;
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyObjectInternal;
use crate::utils::readable_password_from_random;
use kanidm_lib_crypto::CryptoPolicy;
fn create_pw_badlist_cache() -> HashSet<String> {
let mut s = HashSet::new();
s.insert("list@no3IBTyqHu$bad".to_lowercase());
s
}
fn create_webauthn() -> webauthn_rs::Webauthn {
webauthn_rs::WebauthnBuilder::new(
"example.com",
&url::Url::parse("https://idm.example.com").unwrap(),
)
.and_then(|builder| builder.build())
.unwrap()
}
#[test]
fn test_idm_authsession_anonymous_auth_mech() {
sketching::test_init();
let webauthn = create_webauthn();
let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into();
let asd = AuthSessionData {
account: anon_account,
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn: &webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
} else {
panic!("Invalid auth state")
}
let state = session
.expect("Missing auth session?")
.start_session(&AuthMech::Anonymous)
.expect("Failed to select anonymous mech.");
if let AuthState::Continue(auth_mechs) = state {
assert!(auth_mechs
.iter()
.any(|x| matches!(x, AuthAllowed::Anonymous)));
} else {
panic!("Invalid auth state")
}
}
macro_rules! start_password_session {
(
$audit:expr,
$account:expr,
$webauthn:expr,
$privileged:expr
) => {{
let asd = AuthSessionData {
account: $account.clone(),
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn: $webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, $privileged, key_object);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Password)));
} else {
panic!();
}
let state = session
.start_session(&AuthMech::Password)
.expect("Failed to select anonymous mech.");
if let AuthState::Continue(auth_mechs) = state {
assert!(auth_mechs
.iter()
.any(|x| matches!(x, AuthAllowed::Password)));
} else {
panic!("Invalid auth state")
}
(session, create_pw_badlist_cache())
}};
}
fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let (mut session, pw_badlist_cache) =
start_password_session!(&mut audit, account, &webauthn, false);
let attempt = AuthCredential::Password("bad_password".to_string());
match session.validate_creds(
&attempt,
Duration::from_secs(0),
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(_)) => {}
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
let (mut session, pw_badlist_cache) =
start_password_session!(&mut audit, account, &webauthn, privileged);
let attempt = AuthCredential::Password("test_password".to_string());
let uat: UserAuthToken = match session.validate_creds(
&attempt,
Duration::from_secs(0),
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(jwsc, AuthIssueSession::Token)) => {
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
jws_verifier
.verify(&*jwsc)
.unwrap()
.from_json::<UserAuthToken>()
.unwrap()
}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
uat
}
#[test]
fn test_idm_authsession_simple_password_mech() {
sketching::test_init();
let uat = start_session_simple_password_mech(false);
match uat.purpose {
UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
UatPurpose::ReadWrite { expiry } => {
assert!(expiry.is_none())
}
}
}
#[test]
fn test_idm_authsession_simple_password_mech_priv_shortcut() {
sketching::test_init();
let uat = start_session_simple_password_mech(true);
match uat.purpose {
UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
UatPurpose::ReadWrite { expiry } => {
assert!(expiry.is_some())
}
}
}
#[test]
fn test_idm_authsession_simple_password_badlist() {
sketching::test_init();
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "list@no3IBTyqHu$bad").unwrap();
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let (mut session, pw_badlist_cache) =
start_password_session!(&mut audit, account, &webauthn, false);
let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
match session.validate_creds(
&attempt,
Duration::from_secs(0),
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
fn start_password_totp_session(
account: &Account,
webauthn: &Webauthn,
) -> (AuthSession, HashSet<String>) {
let asd = AuthSessionData {
account: account.clone(),
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.expect("Session was unable to be created.");
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs
.iter()
.any(|x| matches!(x, AuthMech::PasswordTotp)))
} else {
panic!();
}
let state = session
.start_session(&AuthMech::PasswordTotp)
.expect("Failed to select password totp mech.");
if let AuthState::Continue(auth_mechs) = state {
assert!(auth_mechs.iter().fold(false, |acc, x| match x {
AuthAllowed::Totp => true,
_ => acc,
}));
} else {
panic!("Invalid auth state")
}
(session, create_pw_badlist_cache())
}
fn start_password_sk_session(
account: &Account,
webauthn: &Webauthn,
) -> (AuthSession, RequestChallengeResponse, HashSet<String>) {
let asd = AuthSessionData {
account: account.clone(),
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.expect("Session was unable to be created.");
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs
.iter()
.any(|x| matches!(x, AuthMech::PasswordSecurityKey)))
} else {
panic!();
}
let state = session
.start_session(&AuthMech::PasswordSecurityKey)
.expect("Failed to select password security key mech.");
let mut rchal = None;
if let AuthState::Continue(auth_mechs) = state {
assert!(auth_mechs.iter().fold(false, |acc, x| match x {
AuthAllowed::SecurityKey(chal) => {
rchal = Some(chal.clone());
true
}
_ => acc,
}));
} else {
panic!("Invalid auth state")
}
(session, rchal.unwrap(), create_pw_badlist_cache())
}
fn start_password_bc_session(
account: &Account,
webauthn: &Webauthn,
) -> (AuthSession, HashSet<String>) {
let asd = AuthSessionData {
account: account.clone(),
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.expect("Session was unable to be created.");
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs
.iter()
.any(|x| matches!(x, AuthMech::PasswordBackupCode)))
} else {
panic!();
}
let state = session
.start_session(&AuthMech::PasswordBackupCode)
.expect("Failed to select password backup code mech.");
if let AuthState::Continue(auth_mechs) = state {
assert!(auth_mechs.iter().fold(false, |acc, x| match x {
AuthAllowed::BackupCode => true,
_ => acc,
}));
} else {
panic!("Invalid auth state")
}
(session, create_pw_badlist_cache())
}
#[test]
fn test_idm_authsession_totp_password_mech() {
sketching::test_init();
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let ts = Duration::from_secs(12345);
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good = totp
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
let totp_bad = totp
.do_totp_duration_from_epoch(&Duration::from_secs(1234567))
.expect("failed to perform totp.");
assert!(totp_bad != totp_good);
let pw_good = "test_password";
let pw_bad = "bad_password";
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_good)
.unwrap()
.append_totp("totp".to_string(), totp);
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Anonymous,
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_bad),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
#[test]
fn test_idm_authsession_password_mfa_badlist() {
sketching::test_init();
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let ts = Duration::from_secs(12345);
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good = totp
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
let pw_badlist = "list@no3IBTyqHu$bad";
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_badlist)
.unwrap()
.append_totp("totp".to_string(), totp);
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_badlist.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
macro_rules! start_webauthn_only_session {
(
$audit:expr,
$account:expr,
$webauthn:expr
) => {{
let asd = AuthSessionData {
account: $account.clone(),
account_policy: ResolvedAccountPolicy::default(),
issue: AuthIssueSession::Token,
webauthn: $webauthn,
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Passkey)));
} else {
panic!();
}
let state = session
.start_session(&AuthMech::Passkey)
.expect("Failed to select Passkey mech.");
let wan_chal = if let AuthState::Continue(auth_mechs) = state {
assert_eq!(auth_mechs.len(), 1);
auth_mechs
.into_iter()
.fold(None, |_acc, x| match x {
AuthAllowed::Passkey(chal) => Some(chal),
_ => None,
})
.expect("No securitykey challenge found.")
} else {
panic!();
};
(session, wan_chal)
}};
}
fn setup_webauthn_passkey(
name: &str,
) -> (
webauthn_rs::prelude::Webauthn,
webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
webauthn_rs::prelude::Passkey,
) {
let webauthn = create_webauthn();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let uuid = Uuid::new_v4();
let (chal, reg_state) = webauthn
.start_passkey_registration(uuid, name, name, None)
.expect("Failed to setup passkey rego challenge");
let r = wa
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
.expect("Failed to create soft passkey");
let wan_cred = webauthn
.finish_passkey_registration(&r, ®_state)
.expect("Failed to register soft token");
(webauthn, wa, wan_cred)
}
fn setup_webauthn_securitykey(
name: &str,
) -> (
webauthn_rs::prelude::Webauthn,
webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
webauthn_rs::prelude::SecurityKey,
) {
let webauthn = create_webauthn();
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let uuid = Uuid::new_v4();
let (chal, reg_state) = webauthn
.start_securitykey_registration(uuid, name, name, None, None, None)
.expect("Failed to setup passkey rego challenge");
let r = wa
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
.expect("Failed to create soft securitykey");
let wan_cred = webauthn
.finish_securitykey_registration(&r, ®_state)
.expect("Failed to register soft token");
(webauthn, wa, wan_cred)
}
#[test]
fn test_idm_authsession_webauthn_only_mech() {
sketching::test_init();
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
account.passkeys = btreemap![(Uuid::new_v4(), ("soft".to_string(), wan_cred))];
{
let (mut session, _inv_chal) =
start_webauthn_only_session!(&mut audit, account, &webauthn);
match session.validate_creds(
&AuthCredential::Anonymous,
ts,
&async_tx,
&audit_tx,
&webauthn,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::Passkey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&Default::default(),
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
{
let (_session, inv_chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::Passkey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let mut inv_wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
let (chal, reg_state) = webauthn
.start_passkey_registration(account.uuid, &account.name, &account.displayname, None)
.expect("Failed to setup webauthn rego challenge");
let r = inv_wa
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
.expect("Failed to create soft token");
let inv_cred = webauthn
.finish_passkey_registration(&r, ®_state)
.expect("Failed to register soft token");
let (chal, _auth_state) = webauthn
.start_passkey_authentication(&vec![inv_cred])
.expect("Failed to generate challenge for in inv softtoken");
let resp = inv_wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("Failed to use softtoken for response.");
let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
match session.validate_creds(
&AuthCredential::Passkey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
#[test]
fn test_idm_authsession_webauthn_password_mech() {
sketching::test_init();
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
let pw_good = "test_password";
let pw_bad = "bad_password";
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_good)
.unwrap()
.append_securitykey("soft".to_string(), wan_cred)
.unwrap();
account.primary = Some(cred);
{
let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(0),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (_session, inv_chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
#[test]
fn test_idm_authsession_webauthn_password_totp_mech() {
sketching::test_init();
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good = totp
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
let totp_bad = totp
.do_totp_duration_from_epoch(&Duration::from_secs(1234567))
.expect("failed to perform totp.");
assert!(totp_bad != totp_good);
let pw_good = "test_password";
let pw_bad = "bad_password";
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_good)
.unwrap()
.append_securitykey("soft".to_string(), wan_cred)
.unwrap()
.append_totp("totp".to_string(), totp);
account.primary = Some(cred);
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_bad),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (_session, inv_chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, chal, pw_badlist_cache) =
start_password_sk_session(&account, &webauthn);
let resp = wa
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
.map(Box::new)
.expect("failed to use softtoken to authenticate");
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
#[test]
fn test_idm_authsession_backup_code_mech() {
sketching::test_init();
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let ts = Duration::from_secs(12345);
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good = totp
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
let pw_good = "test_password";
let pw_bad = "bad_password";
let backup_code_good = readable_password_from_random();
let backup_code_bad = readable_password_from_random();
assert!(backup_code_bad != backup_code_good);
let mut code_set = HashSet::new();
code_set.insert(backup_code_good.clone());
let backup_codes = BackupCodes::new(code_set);
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_good)
.unwrap()
.append_totp("totp".to_string(), totp)
.update_backup_code(backup_codes)
.unwrap();
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
{
let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_bad),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_BACKUPCODE_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_good.clone()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
_ => panic!(),
};
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
}
match async_rx.blocking_recv() {
Some(DelayedAction::BackupCodeRemoval(_)) => {}
_ => panic!("Oh no"),
}
{
let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
}
match async_rx.blocking_recv() {
Some(DelayedAction::BackupCodeRemoval(_)) => {}
_ => panic!("Oh no"),
}
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
}
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
#[test]
fn test_idm_authsession_multiple_totp_password_mech() {
sketching::test_init();
let webauthn = create_webauthn();
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let ts = Duration::from_secs(12345);
let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good_a = totp_a
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
let totp_good_b = totp_b
.do_totp_duration_from_epoch(&ts)
.expect("failed to perform totp.");
assert!(totp_good_a != totp_good_b);
let pw_good = "test_password";
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, pw_good)
.unwrap()
.append_totp("totp_a".to_string(), totp_a)
.append_totp("totp_b".to_string(), totp_b);
account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good_a),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
{
let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
match session.validate_creds(
&AuthCredential::Totp(totp_good_b),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
_ => panic!(),
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
ts,
&async_tx,
&audit_tx,
&webauthn,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
match async_rx.blocking_recv() {
Some(DelayedAction::AuthSessionRecord(_)) => {}
_ => panic!("Oh no"),
}
}
drop(async_tx);
assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
}
}