1use std::collections::BTreeMap;
6use std::sync::Arc;
7use std::time::Duration;
8
9use compact_jwt::Jws;
10use hashbrown::HashSet;
11use kanidm_proto::internal::UserAuthToken;
12use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
13use nonempty::NonEmpty;
14use tokio::sync::mpsc::UnboundedSender as Sender;
15use uuid::Uuid;
16use webauthn_rs::prelude::{
17 AttestationCaList, AttestedPasskey as AttestedPasskeyV4, AttestedPasskeyAuthentication,
18 CredentialID, Passkey as PasskeyV4, PasskeyAuthentication, RequestChallengeResponse,
19 SecurityKeyAuthentication, Webauthn,
20};
21
22use crate::credential::totp::Totp;
23use crate::credential::{BackupCodes, Credential, CredentialType, Password};
24use crate::idm::account::Account;
25use crate::idm::audit::AuditEvent;
26use crate::idm::delayed::{
27 AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement,
28};
29use crate::idm::AuthState;
30use crate::prelude::*;
31use crate::server::keys::KeyObject;
32use crate::value::{AuthType, Session, SessionState};
33use time::OffsetDateTime;
34
35use super::accountpolicy::ResolvedAccountPolicy;
36
37const BAD_PASSWORD_MSG: &str = "incorrect password";
43const BAD_TOTP_MSG: &str = "incorrect totp";
44const BAD_WEBAUTHN_MSG: &str = "invalid webauthn authentication";
45const BAD_ACCOUNT_POLICY: &str = "the credential no longer meets account policy requirements";
46const BAD_BACKUPCODE_MSG: &str = "invalid backup code";
47const BAD_AUTH_TYPE_MSG: &str = "invalid authentication method in this context";
48const BAD_CREDENTIALS: &str = "invalid credential message";
49const ACCOUNT_EXPIRED: &str = "account expired";
50const PW_BADLIST_MSG: &str = "password is in badlist";
51
52#[derive(Debug, Clone)]
53enum AuthIntent {
54 InitialAuth {
55 privileged: bool,
56 },
57 Reauth {
58 session_id: Uuid,
59 session_expiry: Option<OffsetDateTime>,
60 },
61}
62
63enum CredState {
65 Success { auth_type: AuthType, cred_id: Uuid },
66 Continue(Box<NonEmpty<AuthAllowed>>),
67 Denied(&'static str),
68}
69
70#[derive(Clone, Debug, PartialEq)]
71enum CredVerifyState {
73 Init,
74 Success,
75 Fail,
76}
77
78#[derive(Clone, Debug)]
79struct CredTotp {
81 pw: Password,
82 pw_state: CredVerifyState,
83 totp: BTreeMap<String, Totp>,
84 mfa_state: CredVerifyState,
85}
86
87#[derive(Clone, Debug)]
88struct CredBackupCode {
90 pw: Password,
91 pw_state: CredVerifyState,
92 backup_code: BackupCodes,
93 mfa_state: CredVerifyState,
94}
95
96#[derive(Clone, Debug)]
97struct CredSecurityKey {
99 pw: Password,
100 pw_state: CredVerifyState,
101 chal: RequestChallengeResponse,
102 ska: SecurityKeyAuthentication,
103 mfa_state: CredVerifyState,
104}
105
106#[derive(Clone, Debug)]
107struct CredPasskey {
109 chal: RequestChallengeResponse,
110 wan_state: PasskeyAuthentication,
111 state: CredVerifyState,
112}
113
114#[derive(Clone, Debug)]
115struct CredAttestedPasskey {
117 chal: RequestChallengeResponse,
118 wan_state: AttestedPasskeyAuthentication,
119 state: CredVerifyState,
120}
121
122#[derive(Clone, Debug)]
126enum CredHandler {
127 Anonymous {
128 cred_id: Uuid,
129 },
130 Password {
131 pw: Password,
132 generated: bool,
133 cred_id: Uuid,
134 },
135 PasswordTotp {
136 cmfa: CredTotp,
137 cred_id: Uuid,
138 },
139 PasswordBackupCode {
140 cmfa: CredBackupCode,
141 cred_id: Uuid,
142 },
143 PasswordSecurityKey {
144 cmfa: CredSecurityKey,
145 cred_id: Uuid,
146 },
147 Passkey {
148 c_wan: CredPasskey,
149 cred_ids: BTreeMap<CredentialID, Uuid>,
150 },
151 AttestedPasskey {
152 c_wan: CredAttestedPasskey,
153 att_ca_list: AttestationCaList,
155 creds: BTreeMap<AttestedPasskeyV4, Uuid>,
157 },
158}
159
160impl CredHandler {
161 fn build_from_set_passkey(
166 wan: impl Iterator<Item = (Uuid, PasskeyV4)>,
167 webauthn: &Webauthn,
168 ) -> Option<Self> {
169 let mut pks = Vec::with_capacity(wan.size_hint().0);
170 let mut cred_ids = BTreeMap::default();
171
172 for (uuid, pk) in wan {
173 cred_ids.insert(pk.cred_id().clone(), uuid);
174 pks.push(pk);
175 }
176
177 if pks.is_empty() {
178 debug!("Account does not have any passkeys");
179 return None;
180 };
181
182 webauthn
183 .start_passkey_authentication(&pks)
184 .map(|(chal, wan_state)| CredHandler::Passkey {
185 c_wan: CredPasskey {
186 chal,
187 wan_state,
188 state: CredVerifyState::Init,
189 },
190 cred_ids,
191 })
192 .map_err(|e| {
193 security_info!(
194 ?e,
195 "Unable to create passkey webauthn authentication challenge"
196 );
197 })
199 .ok()
200 }
201
202 fn build_from_single_passkey(
203 cred_id: Uuid,
204 pk: PasskeyV4,
205 webauthn: &Webauthn,
206 ) -> Option<Self> {
207 let cred_ids = btreemap!((pk.cred_id().clone(), cred_id));
208 let pks = vec![pk];
209
210 webauthn
211 .start_passkey_authentication(pks.as_slice())
212 .map(|(chal, wan_state)| CredHandler::Passkey {
213 c_wan: CredPasskey {
214 chal,
215 wan_state,
216 state: CredVerifyState::Init,
217 },
218 cred_ids,
219 })
220 .map_err(|e| {
221 security_info!(
222 ?e,
223 "Unable to create passkey webauthn authentication challenge"
224 );
225 })
227 .ok()
228 }
229
230 fn build_from_set_attested_pk(
231 wan: &BTreeMap<Uuid, (String, AttestedPasskeyV4)>,
232 att_ca_list: &AttestationCaList,
233 webauthn: &Webauthn,
234 ) -> Option<Self> {
235 if wan.is_empty() {
236 debug!("Account does not have any attested passkeys");
237 return None;
238 };
239
240 let pks: Vec<_> = wan.values().map(|(_, k)| k).cloned().collect();
241 let creds: BTreeMap<_, _> = wan.iter().map(|(u, (_, k))| (k.clone(), *u)).collect();
242
243 webauthn
244 .start_attested_passkey_authentication(&pks)
245 .map(|(chal, wan_state)| CredHandler::AttestedPasskey {
246 c_wan: CredAttestedPasskey {
247 chal,
248 wan_state,
249 state: CredVerifyState::Init,
250 },
251 att_ca_list: att_ca_list.clone(),
252 creds,
253 })
254 .map_err(|e| {
255 security_info!(
256 ?e,
257 "Unable to create attested passkey webauthn authentication challenge"
258 );
259 })
261 .ok()
262 }
263
264 fn build_from_single_attested_pk(
265 cred_id: Uuid,
266 pk: &AttestedPasskeyV4,
267 att_ca_list: &AttestationCaList,
268 webauthn: &Webauthn,
269 ) -> Option<Self> {
270 let creds = btreemap!((pk.clone(), cred_id));
271 let pks = vec![pk.clone()];
272
273 webauthn
274 .start_attested_passkey_authentication(pks.as_slice())
275 .map(|(chal, wan_state)| CredHandler::AttestedPasskey {
276 c_wan: CredAttestedPasskey {
277 chal,
278 wan_state,
279 state: CredVerifyState::Init,
280 },
281 att_ca_list: att_ca_list.clone(),
282 creds,
283 })
284 .map_err(|e| {
285 security_info!(
286 ?e,
287 "Unable to create attested passkey webauthn authentication challenge"
288 );
289 })
291 .ok()
292 }
293
294 fn build_from_password_totp(cred: &Credential) -> Option<Self> {
295 match &cred.type_ {
296 CredentialType::PasswordMfa(pw, maybe_totp, _, _) => {
297 if maybe_totp.is_empty() {
298 None
299 } else {
300 let cmfa = CredTotp {
301 pw: pw.clone(),
302 pw_state: CredVerifyState::Init,
303 totp: maybe_totp
304 .iter()
305 .map(|(l, t)| (l.clone(), t.clone()))
306 .collect(),
307 mfa_state: CredVerifyState::Init,
308 };
309
310 Some(CredHandler::PasswordTotp {
311 cmfa,
312 cred_id: cred.uuid,
313 })
314 }
315 }
316 _ => None,
317 }
318 }
319
320 fn build_from_password_backup_code(cred: &Credential) -> Option<Self> {
321 match &cred.type_ {
322 CredentialType::PasswordMfa(pw, _, _, Some(backup_code)) => {
323 let cmfa = CredBackupCode {
324 pw: pw.clone(),
325 pw_state: CredVerifyState::Init,
326 backup_code: backup_code.clone(),
327 mfa_state: CredVerifyState::Init,
328 };
329
330 Some(CredHandler::PasswordBackupCode {
331 cmfa,
332 cred_id: cred.uuid,
333 })
334 }
335 _ => None,
336 }
337 }
338
339 fn build_from_password_security_key(cred: &Credential, webauthn: &Webauthn) -> Option<Self> {
340 match &cred.type_ {
341 CredentialType::PasswordMfa(pw, _, maybe_wan, _) => {
342 if !maybe_wan.is_empty() {
343 let sks: Vec<_> = maybe_wan.values().cloned().collect();
344 let (chal, ska) = webauthn
345 .start_securitykey_authentication(&sks)
346 .map_err(|err| {
347 warn!(?err, "Unable to create webauthn authentication challenge")
348 })
349 .ok()?;
350
351 let cmfa = CredSecurityKey {
352 pw: pw.clone(),
353 pw_state: CredVerifyState::Init,
354 ska,
355 chal,
356 mfa_state: CredVerifyState::Init,
357 };
358
359 Some(CredHandler::PasswordSecurityKey {
360 cmfa,
361 cred_id: cred.uuid,
362 })
363 } else {
364 None
365 }
366 }
367 _ => None,
368 }
369 }
370
371 fn build_from_password_only(cred: &Credential) -> Option<Self> {
372 match &cred.type_ {
373 CredentialType::Password(pw) => Some(CredHandler::Password {
374 pw: pw.clone(),
375 generated: false,
376 cred_id: cred.uuid,
377 }),
378 CredentialType::GeneratedPassword(pw) => Some(CredHandler::Password {
379 pw: pw.clone(),
380 generated: true,
381 cred_id: cred.uuid,
382 }),
383 _ => None,
384 }
385 }
386
387 fn maybe_pw_upgrade(
391 pw: &Password,
392 who: Uuid,
393 cleartext: &str,
394 async_tx: &Sender<DelayedAction>,
395 ) {
396 if pw.requires_upgrade() {
397 if let Err(_e) = async_tx.send(DelayedAction::PwUpgrade(PasswordUpgrade {
398 target_uuid: who,
399 existing_password: cleartext.to_string(),
400 })) {
401 admin_warn!("unable to queue delayed pwupgrade, continuing ... ");
402 };
403 }
404 }
405
406 fn validate_anonymous(cred: &AuthCredential, cred_id: Uuid) -> CredState {
408 match cred {
409 AuthCredential::Anonymous => {
410 security_debug!("Handler::Anonymous -> Result::Success");
412 CredState::Success {
413 auth_type: AuthType::Anonymous,
414 cred_id,
415 }
416 }
417 _ => {
418 security_error!(
419 "Handler::Anonymous -> Result::Denied - invalid cred type for handler"
420 );
421 CredState::Denied(BAD_AUTH_TYPE_MSG)
422 }
423 }
424 }
425
426 fn validate_password(
428 cred: &AuthCredential,
429 cred_id: Uuid,
430 pw: &mut Password,
431 generated: bool,
432 who: Uuid,
433 async_tx: &Sender<DelayedAction>,
434 pw_badlist_set: &HashSet<String>,
435 ) -> CredState {
436 match cred {
437 AuthCredential::Password(cleartext) => {
438 if pw.verify(cleartext.as_str()).unwrap_or(false) {
439 if pw_badlist_set.contains(&cleartext.to_lowercase()) {
440 security_error!("Handler::Password -> Result::Denied - Password found in badlist during login");
441 CredState::Denied(PW_BADLIST_MSG)
442 } else {
443 security_info!("Handler::Password -> Result::Success");
444 Self::maybe_pw_upgrade(pw, who, cleartext.as_str(), async_tx);
445 if generated {
446 CredState::Success {
447 auth_type: AuthType::GeneratedPassword,
448 cred_id,
449 }
450 } else {
451 CredState::Success {
452 auth_type: AuthType::Password,
453 cred_id,
454 }
455 }
456 }
457 } else {
458 security_error!("Handler::Password -> Result::Denied - incorrect password");
459 CredState::Denied(BAD_PASSWORD_MSG)
460 }
461 }
462 _ => {
464 security_error!(
465 "Handler::Password -> Result::Denied - invalid cred type for handler"
466 );
467 CredState::Denied(BAD_AUTH_TYPE_MSG)
468 }
469 }
470 }
471
472 fn validate_password_totp(
476 cred: &AuthCredential,
477 cred_id: Uuid,
478 ts: Duration,
479 pw_mfa: &mut CredTotp,
480 who: Uuid,
481 async_tx: &Sender<DelayedAction>,
482 pw_badlist_set: &HashSet<String>,
483 ) -> CredState {
484 match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
485 (CredVerifyState::Init, CredVerifyState::Init) => {
486 match cred {
488 AuthCredential::Totp(totp_chal) => {
489 if let Some(label) = pw_mfa
493 .totp
494 .iter()
495 .find(|(_, t)| t.verify(*totp_chal, ts))
496 .map(|(l, _)| l)
497 {
498 pw_mfa.mfa_state = CredVerifyState::Success;
499 security_info!(
500 "Handler::PasswordMfa -> Result::Continue - TOTP ({}) OK, password -", label
501 );
502 CredState::Continue(Box::new(NonEmpty {
503 head: AuthAllowed::Password,
504 tail: Vec::with_capacity(0),
505 }))
506 } else {
507 pw_mfa.mfa_state = CredVerifyState::Fail;
508 security_error!(
509 "Handler::PasswordMfa -> Result::Denied - TOTP Fail, password -"
510 );
511 CredState::Denied(BAD_TOTP_MSG)
512 }
513 }
514 _ => {
515 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
516 CredState::Denied(BAD_AUTH_TYPE_MSG)
517 }
518 }
519 }
520 (CredVerifyState::Success, CredVerifyState::Init) => {
521 match cred {
523 AuthCredential::Password(cleartext) => {
524 if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
525 if pw_badlist_set.contains(&cleartext.to_lowercase()) {
526 pw_mfa.pw_state = CredVerifyState::Fail;
527 security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
528 CredState::Denied(PW_BADLIST_MSG)
529 } else {
530 pw_mfa.pw_state = CredVerifyState::Success;
531 security_info!("Handler::PasswordMfa -> Result::Success - TOTP OK, password OK");
532 Self::maybe_pw_upgrade(
533 &pw_mfa.pw,
534 who,
535 cleartext.as_str(),
536 async_tx,
537 );
538 CredState::Success {
539 auth_type: AuthType::PasswordTotp,
540 cred_id,
541 }
542 }
543 } else {
544 pw_mfa.pw_state = CredVerifyState::Fail;
545 security_error!(
546 "Handler::PasswordMfa -> Result::Denied - TOTP OK, password Fail"
547 );
548 CredState::Denied(BAD_PASSWORD_MSG)
549 }
550 }
551 _ => {
552 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
553 CredState::Denied(BAD_AUTH_TYPE_MSG)
554 }
555 }
556 }
557 _ => {
558 security_error!(
559 "Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
560 );
561 CredState::Denied(BAD_AUTH_TYPE_MSG)
562 }
563 }
564 } fn validate_password_security_key(
570 cred: &AuthCredential,
571 cred_id: Uuid,
572 pw_mfa: &mut CredSecurityKey,
573 webauthn: &Webauthn,
574 who: Uuid,
575 async_tx: &Sender<DelayedAction>,
576 pw_badlist_set: &HashSet<String>,
577 ) -> CredState {
578 match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
579 (CredVerifyState::Init, CredVerifyState::Init) => {
580 match cred {
582 AuthCredential::SecurityKey(resp) => {
583 match webauthn.finish_securitykey_authentication(resp, &pw_mfa.ska) {
584 Ok(auth_result) => {
585 pw_mfa.mfa_state = CredVerifyState::Success;
586 if auth_result.needs_update() {
589 if let Err(_e) =
591 async_tx.send(DelayedAction::WebauthnCounterIncrement(
592 WebauthnCounterIncrement {
593 target_uuid: who,
594 auth_result,
595 },
596 ))
597 {
598 admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
599 };
600 };
601 CredState::Continue(Box::new(NonEmpty {
602 head: AuthAllowed::Password,
603 tail: Vec::with_capacity(0),
604 }))
605 }
606 Err(e) => {
607 pw_mfa.mfa_state = CredVerifyState::Fail;
608 security_error!(
610 ?e,
611 "Handler::Webauthn -> Result::Denied - webauthn error"
612 );
613 CredState::Denied(BAD_WEBAUTHN_MSG)
614 }
615 }
616 }
617 _ => {
618 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
619 CredState::Denied(BAD_AUTH_TYPE_MSG)
620 }
621 }
622 }
623 (CredVerifyState::Success, CredVerifyState::Init) => {
624 match cred {
626 AuthCredential::Password(cleartext) => {
627 if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
628 if pw_badlist_set.contains(&cleartext.to_lowercase()) {
629 pw_mfa.pw_state = CredVerifyState::Fail;
630 security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
631 CredState::Denied(PW_BADLIST_MSG)
632 } else {
633 pw_mfa.pw_state = CredVerifyState::Success;
634 security_info!("Handler::PasswordMfa -> Result::Success - SecurityKey OK, password OK");
635 Self::maybe_pw_upgrade(
636 &pw_mfa.pw,
637 who,
638 cleartext.as_str(),
639 async_tx,
640 );
641 CredState::Success {
642 auth_type: AuthType::PasswordSecurityKey,
643 cred_id,
644 }
645 }
646 } else {
647 pw_mfa.pw_state = CredVerifyState::Fail;
648 security_error!("Handler::PasswordMfa -> Result::Denied - SecurityKey OK, password Fail");
649 CredState::Denied(BAD_PASSWORD_MSG)
650 }
651 }
652 _ => {
653 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
654 CredState::Denied(BAD_AUTH_TYPE_MSG)
655 }
656 }
657 }
658 _ => {
659 security_error!(
660 "Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
661 );
662 CredState::Denied(BAD_AUTH_TYPE_MSG)
663 }
664 }
665 }
666
667 fn validate_password_backup_code(
671 cred: &AuthCredential,
672 cred_id: Uuid,
673 pw_mfa: &mut CredBackupCode,
674 who: Uuid,
675 async_tx: &Sender<DelayedAction>,
676 pw_badlist_set: &HashSet<String>,
677 ) -> CredState {
678 match (&pw_mfa.mfa_state, &pw_mfa.pw_state) {
679 (CredVerifyState::Init, CredVerifyState::Init) => {
680 match cred {
682 AuthCredential::BackupCode(code_chal) => {
683 if pw_mfa.backup_code.verify(code_chal) {
684 if let Err(_e) =
685 async_tx.send(DelayedAction::BackupCodeRemoval(BackupCodeRemoval {
686 target_uuid: who,
687 code_to_remove: code_chal.to_string(),
688 }))
689 {
690 admin_warn!(
691 "unable to queue delayed backup code removal, continuing ... "
692 );
693 };
694 pw_mfa.mfa_state = CredVerifyState::Success;
695 security_info!("Handler::PasswordMfa -> Result::Continue - BackupCode OK, password -");
696 CredState::Continue(Box::new(NonEmpty {
697 head: AuthAllowed::Password,
698 tail: Vec::with_capacity(0),
699 }))
700 } else {
701 pw_mfa.mfa_state = CredVerifyState::Fail;
702 security_error!("Handler::PasswordMfa -> Result::Denied - BackupCode Fail, password -");
703 CredState::Denied(BAD_BACKUPCODE_MSG)
704 }
705 }
706 _ => {
707 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
708 CredState::Denied(BAD_AUTH_TYPE_MSG)
709 }
710 }
711 }
712 (CredVerifyState::Success, CredVerifyState::Init) => {
713 match cred {
715 AuthCredential::Password(cleartext) => {
716 if pw_mfa.pw.verify(cleartext.as_str()).unwrap_or(false) {
717 if pw_badlist_set.contains(&cleartext.to_lowercase()) {
718 pw_mfa.pw_state = CredVerifyState::Fail;
719 security_error!("Handler::PasswordMfa -> Result::Denied - Password found in badlist during login");
720 CredState::Denied(PW_BADLIST_MSG)
721 } else {
722 pw_mfa.pw_state = CredVerifyState::Success;
723 security_info!("Handler::PasswordMfa -> Result::Success - BackupCode OK, password OK");
724 Self::maybe_pw_upgrade(
725 &pw_mfa.pw,
726 who,
727 cleartext.as_str(),
728 async_tx,
729 );
730 CredState::Success {
731 auth_type: AuthType::PasswordBackupCode,
732 cred_id,
733 }
734 }
735 } else {
736 pw_mfa.pw_state = CredVerifyState::Fail;
737 security_error!("Handler::PasswordMfa -> Result::Denied - BackupCode OK, password Fail");
738 CredState::Denied(BAD_PASSWORD_MSG)
739 }
740 }
741 _ => {
742 security_error!("Handler::PasswordMfa -> Result::Denied - invalid cred type for handler");
743 CredState::Denied(BAD_AUTH_TYPE_MSG)
744 }
745 }
746 }
747 _ => {
748 security_error!(
749 "Handler::PasswordMfa -> Result::Denied - invalid credential mfa and pw state"
750 );
751 CredState::Denied(BAD_AUTH_TYPE_MSG)
752 }
753 }
754 }
755
756 pub fn validate_passkey(
758 cred: &AuthCredential,
759 cred_ids: &BTreeMap<CredentialID, Uuid>,
760 wan_cred: &mut CredPasskey,
761 webauthn: &Webauthn,
762 who: Uuid,
763 async_tx: &Sender<DelayedAction>,
764 ) -> CredState {
765 if wan_cred.state != CredVerifyState::Init {
766 security_error!("Handler::Webauthn -> Result::Denied - Internal State Already Fail");
767 return CredState::Denied(BAD_WEBAUTHN_MSG);
768 }
769
770 match cred {
771 AuthCredential::Passkey(resp) => {
772 match webauthn.finish_passkey_authentication(resp, &wan_cred.wan_state) {
774 Ok(auth_result) => {
775 if let Some(cred_id) = cred_ids.get(auth_result.cred_id()).copied() {
776 wan_cred.state = CredVerifyState::Success;
777 if auth_result.needs_update() {
780 if let Err(_e) =
782 async_tx.send(DelayedAction::WebauthnCounterIncrement(
783 WebauthnCounterIncrement {
784 target_uuid: who,
785 auth_result,
786 },
787 ))
788 {
789 admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
790 };
791 };
792
793 CredState::Success {
794 auth_type: AuthType::Passkey,
795 cred_id,
796 }
797 } else {
798 wan_cred.state = CredVerifyState::Fail;
799 security_error!("Handler::Webauthn -> Result::Denied - webauthn credential id not found");
801 CredState::Denied(BAD_WEBAUTHN_MSG)
802 }
803 }
804 Err(e) => {
805 wan_cred.state = CredVerifyState::Fail;
806 security_error!(?e, "Handler::Webauthn -> Result::Denied - webauthn error");
808 CredState::Denied(BAD_WEBAUTHN_MSG)
809 }
810 }
811 }
812 _ => {
813 security_error!(
814 "Handler::Webauthn -> Result::Denied - invalid cred type for handler"
815 );
816 CredState::Denied(BAD_AUTH_TYPE_MSG)
817 }
818 }
819 }
820
821 pub fn validate_attested_passkey(
823 cred: &AuthCredential,
824 creds: &BTreeMap<AttestedPasskeyV4, Uuid>,
825 wan_cred: &mut CredAttestedPasskey,
826 webauthn: &Webauthn,
827 who: Uuid,
828 async_tx: &Sender<DelayedAction>,
829 att_ca_list: &AttestationCaList,
830 ) -> CredState {
831 if wan_cred.state != CredVerifyState::Init {
832 security_error!("Handler::Webauthn -> Result::Denied - Internal State Already Fail");
833 return CredState::Denied(BAD_WEBAUTHN_MSG);
834 }
835
836 match cred {
837 AuthCredential::Passkey(resp) => {
838 match webauthn.finish_attested_passkey_authentication(resp, &wan_cred.wan_state) {
840 Ok(auth_result) => {
841 if let Some((apk, cred_id)) = creds.get_key_value(auth_result.cred_id()) {
842 if let Err(webauthn_err) = apk.verify_attestation(att_ca_list) {
845 wan_cred.state = CredVerifyState::Fail;
846 debug!(?webauthn_err);
848 security_error!("Handler::Webauthn -> Result::Denied - webauthn credential fails attestation");
849 return CredState::Denied(BAD_ACCOUNT_POLICY);
850 }
851
852 wan_cred.state = CredVerifyState::Success;
853 if auth_result.needs_update() {
856 if let Err(_e) =
858 async_tx.send(DelayedAction::WebauthnCounterIncrement(
859 WebauthnCounterIncrement {
860 target_uuid: who,
861 auth_result,
862 },
863 ))
864 {
865 admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
866 };
867 };
868
869 CredState::Success {
870 auth_type: AuthType::AttestedPasskey,
871 cred_id: *cred_id,
872 }
873 } else {
874 wan_cred.state = CredVerifyState::Fail;
875 security_error!("Handler::Webauthn -> Result::Denied - webauthn credential id not found");
877 CredState::Denied(BAD_WEBAUTHN_MSG)
878 }
879 }
880 Err(e) => {
881 wan_cred.state = CredVerifyState::Fail;
882 security_error!(?e, "Handler::Webauthn -> Result::Denied - webauthn error");
884 CredState::Denied(BAD_WEBAUTHN_MSG)
885 }
886 }
887 }
888 _ => {
889 security_error!(
890 "Handler::Webauthn -> Result::Denied - invalid cred type for handler"
891 );
892 CredState::Denied(BAD_AUTH_TYPE_MSG)
893 }
894 }
895 }
896
897 #[allow(clippy::too_many_arguments)]
898 pub fn validate(
900 &mut self,
901 cred: &AuthCredential,
902 ts: Duration,
903 who: Uuid,
904 async_tx: &Sender<DelayedAction>,
905 webauthn: &Webauthn,
906 pw_badlist_set: &HashSet<String>,
907 ) -> CredState {
908 match self {
909 CredHandler::Anonymous { cred_id } => Self::validate_anonymous(cred, *cred_id),
910 CredHandler::Password {
911 ref mut pw,
912 generated,
913 cred_id,
914 } => Self::validate_password(
915 cred,
916 *cred_id,
917 pw,
918 *generated,
919 who,
920 async_tx,
921 pw_badlist_set,
922 ),
923 CredHandler::PasswordTotp {
924 ref mut cmfa,
925 cred_id,
926 } => Self::validate_password_totp(
927 cred,
928 *cred_id,
929 ts,
930 cmfa,
931 who,
932 async_tx,
933 pw_badlist_set,
934 ),
935 CredHandler::PasswordBackupCode {
936 ref mut cmfa,
937 cred_id,
938 } => Self::validate_password_backup_code(
939 cred,
940 *cred_id,
941 cmfa,
942 who,
943 async_tx,
944 pw_badlist_set,
945 ),
946 CredHandler::PasswordSecurityKey {
947 ref mut cmfa,
948 cred_id,
949 } => Self::validate_password_security_key(
950 cred,
951 *cred_id,
952 cmfa,
953 webauthn,
954 who,
955 async_tx,
956 pw_badlist_set,
957 ),
958 CredHandler::Passkey {
959 ref mut c_wan,
960 cred_ids,
961 } => Self::validate_passkey(cred, cred_ids, c_wan, webauthn, who, async_tx),
962 CredHandler::AttestedPasskey {
963 ref mut c_wan,
964 ref att_ca_list,
965 creds,
966 } => Self::validate_attested_passkey(
967 cred,
968 creds,
969 c_wan,
970 webauthn,
971 who,
972 async_tx,
973 att_ca_list,
974 ),
975 }
976 }
977
978 pub fn next_auth_allowed(&self) -> Vec<AuthAllowed> {
981 match &self {
982 CredHandler::Anonymous { .. } => vec![AuthAllowed::Anonymous],
983 CredHandler::Password { .. } => vec![AuthAllowed::Password],
984 CredHandler::PasswordTotp { .. } => vec![AuthAllowed::Totp],
985 CredHandler::PasswordBackupCode { .. } => vec![AuthAllowed::BackupCode],
986
987 CredHandler::PasswordSecurityKey { ref cmfa, .. } => {
988 vec![AuthAllowed::SecurityKey(cmfa.chal.clone())]
989 }
990 CredHandler::Passkey { c_wan, .. } => vec![AuthAllowed::Passkey(c_wan.chal.clone())],
991 CredHandler::AttestedPasskey { c_wan, .. } => {
992 vec![AuthAllowed::Passkey(c_wan.chal.clone())]
993 }
994 }
995 }
996
997 fn can_proceed(&self, mech: &AuthMech) -> bool {
999 match (self, mech) {
1000 (CredHandler::Anonymous { .. }, AuthMech::Anonymous)
1001 | (CredHandler::Password { .. }, AuthMech::Password)
1002 | (CredHandler::PasswordTotp { .. }, AuthMech::PasswordTotp)
1003 | (CredHandler::PasswordBackupCode { .. }, AuthMech::PasswordBackupCode)
1004 | (CredHandler::PasswordSecurityKey { .. }, AuthMech::PasswordSecurityKey)
1005 | (CredHandler::Passkey { .. }, AuthMech::Passkey)
1006 | (CredHandler::AttestedPasskey { .. }, AuthMech::Passkey) => true,
1007 (_, _) => false,
1008 }
1009 }
1010
1011 fn allows_mech(&self) -> AuthMech {
1012 match self {
1013 CredHandler::Anonymous { .. } => AuthMech::Anonymous,
1014 CredHandler::Password { .. } => AuthMech::Password,
1015 CredHandler::PasswordTotp { .. } => AuthMech::PasswordTotp,
1016 CredHandler::PasswordBackupCode { .. } => AuthMech::PasswordBackupCode,
1017 CredHandler::PasswordSecurityKey { .. } => AuthMech::PasswordSecurityKey,
1018 CredHandler::Passkey { .. } => AuthMech::Passkey,
1019 CredHandler::AttestedPasskey { .. } => AuthMech::Passkey,
1020 }
1021 }
1022}
1023
1024#[allow(clippy::large_enum_variant)]
1025#[derive(Clone)]
1026enum AuthSessionState {
1032 Init(NonEmpty<CredHandler>),
1033 InProgress(CredHandler),
1039 Success,
1040 Denied(&'static str),
1041}
1042
1043impl AuthSessionState {
1044 fn is_denied(&self) -> Option<&'static str> {
1045 match &self {
1046 AuthSessionState::Denied(x) => Some(x),
1047 _ => None,
1048 }
1049 }
1050}
1051
1052pub(crate) struct AuthSessionData<'a> {
1053 pub(crate) account: Account,
1054 pub(crate) account_policy: ResolvedAccountPolicy,
1055 pub(crate) issue: AuthIssueSession,
1056 pub(crate) webauthn: &'a Webauthn,
1057 pub(crate) ct: Duration,
1058 pub(crate) client_auth_info: ClientAuthInfo,
1059}
1060
1061#[derive(Clone)]
1062pub(crate) struct AuthSession {
1064 account: Account,
1067 account_policy: ResolvedAccountPolicy,
1069
1070 state: AuthSessionState,
1076
1077 issue: AuthIssueSession,
1079
1080 intent: AuthIntent,
1083
1084 source: Source,
1086
1087 key_object: Arc<KeyObject>,
1089}
1090
1091impl AuthSession {
1092 pub fn new(
1096 asd: AuthSessionData<'_>,
1097 privileged: bool,
1098 key_object: Arc<KeyObject>,
1099 ) -> (Option<Self>, AuthState) {
1100 let state = if asd.account.is_within_valid_time(asd.ct) {
1104 if asd.account.is_anonymous() {
1108 AuthSessionState::Init(NonEmpty {
1109 head: CredHandler::Anonymous {
1110 cred_id: asd.account.uuid,
1111 },
1112 tail: Vec::with_capacity(0),
1113 })
1114 } else {
1115 let mut handlers = Vec::with_capacity(4);
1116
1117 if let Some(cred) = &asd.account.primary {
1124 if let Some(ch) = CredHandler::build_from_password_totp(cred) {
1126 handlers.push(ch);
1127 }
1128
1129 if let Some(ch) = CredHandler::build_from_password_backup_code(cred) {
1130 handlers.push(ch);
1131 }
1132
1133 if let Some(ch) =
1134 CredHandler::build_from_password_security_key(cred, asd.webauthn)
1135 {
1136 handlers.push(ch);
1137 }
1138
1139 if handlers.is_empty() {
1140 if let Some(ch) = CredHandler::build_from_password_only(cred) {
1142 handlers.push(ch);
1143 }
1144 }
1145 }
1146
1147 trace!(?handlers);
1148
1149 if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
1151 if let Some(ch) = CredHandler::build_from_set_attested_pk(
1152 &asd.account.attested_passkeys,
1153 att_ca_list,
1154 asd.webauthn,
1155 ) {
1156 handlers.push(ch);
1157 }
1158 } else {
1159 let credential_iter = asd
1160 .account
1161 .passkeys
1162 .iter()
1163 .map(|(u, (_, pk))| (*u, pk.clone()))
1164 .chain(
1165 asd.account
1166 .attested_passkeys
1167 .iter()
1168 .map(|(u, (_, pk))| (*u, pk.into())),
1169 );
1170
1171 if let Some(ch) =
1172 CredHandler::build_from_set_passkey(credential_iter, asd.webauthn)
1173 {
1174 handlers.push(ch);
1175 }
1176 };
1177
1178 if let Some(non_empty_handlers) = NonEmpty::collect(handlers) {
1179 AuthSessionState::Init(non_empty_handlers)
1180 } else {
1181 security_info!("account has no available credentials");
1182 AuthSessionState::Denied("invalid credential state")
1183 }
1184 }
1185 } else {
1186 security_info!("account expired");
1187 AuthSessionState::Denied(ACCOUNT_EXPIRED)
1188 };
1189
1190 if let Some(reason) = state.is_denied() {
1192 (None, AuthState::Denied(reason.to_string()))
1194 } else {
1195 let auth_session = AuthSession {
1197 account: asd.account,
1198 account_policy: asd.account_policy,
1199 state,
1200 issue: asd.issue,
1201 intent: AuthIntent::InitialAuth { privileged },
1202 source: asd.client_auth_info.source,
1203 key_object,
1204 };
1205 let valid_mechs = auth_session.valid_auth_mechs();
1209
1210 security_debug!(?valid_mechs, "Offering auth mechanisms");
1211 let as_state = AuthState::Choose(valid_mechs);
1212 (Some(auth_session), as_state)
1213 }
1214 }
1215
1216 pub(crate) fn new_reauth(
1221 asd: AuthSessionData<'_>,
1222 session_id: Uuid,
1223 session: &Session,
1224 cred_id: Uuid,
1225 key_object: Arc<KeyObject>,
1226 ) -> (Option<Self>, AuthState) {
1227 #[allow(clippy::large_enum_variant)]
1228 enum State {
1230 Expired,
1231 NoMatchingCred,
1232 Proceed(CredHandler),
1233 }
1234
1235 let state = if asd.account.is_within_valid_time(asd.ct) {
1236 let mut cred_handler = None;
1243
1244 match session.type_ {
1245 AuthType::Password
1246 | AuthType::GeneratedPassword
1247 | AuthType::PasswordBackupCode => {
1250 if let Some(primary) = asd.account.primary.as_ref() {
1251 if primary.uuid == cred_id {
1252 cred_handler = CredHandler::build_from_password_only(primary)
1253 }
1254 }
1255 }
1256 AuthType::PasswordTotp => {
1257 if let Some(primary) = asd.account.primary.as_ref() {
1258 if primary.uuid == cred_id {
1259 cred_handler = CredHandler::build_from_password_totp(primary)
1260 }
1261 }
1262 }
1263 AuthType::PasswordSecurityKey => {
1264 if let Some(primary) = asd.account.primary.as_ref() {
1265 if primary.uuid == cred_id {
1266 cred_handler =
1267 CredHandler::build_from_password_security_key(primary, asd.webauthn)
1268 }
1269 }
1270 }
1271 AuthType::Passkey => {
1272 let maybe_pk: Option<PasskeyV4> = asd
1274 .account
1275 .attested_passkeys
1276 .get(&cred_id)
1277 .map(|(_, apk)| apk.into())
1278 .or_else(|| asd.account.passkeys.get(&cred_id).map(|(_, pk)| pk.clone()));
1279
1280 if let Some(pk) = maybe_pk {
1281 if let Some(ch) =
1282 CredHandler::build_from_single_passkey(cred_id, pk, asd.webauthn)
1283 {
1284 debug_assert!(cred_handler.is_none());
1286 cred_handler = Some(ch);
1287 } else {
1288 security_critical!(
1289 "corrupt credentials, unable to start passkey credhandler"
1290 );
1291 }
1292 }
1293 }
1294 AuthType::AttestedPasskey => {
1295 if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
1296 if let Some(pk) = asd
1297 .account
1298 .attested_passkeys
1299 .get(&cred_id)
1300 .map(|(_, pk)| pk)
1301 {
1302 if let Some(ch) = CredHandler::build_from_single_attested_pk(
1303 cred_id,
1304 pk,
1305 att_ca_list,
1306 asd.webauthn,
1307 ) {
1308 debug_assert!(cred_handler.is_none());
1310 cred_handler = Some(ch);
1311 } else {
1312 security_critical!(
1313 "corrupt credentials, unable to start attested passkey credhandler"
1314 );
1315 }
1316 }
1317 }
1318 }
1319 AuthType::Anonymous => {}
1320 }
1321
1322 if let Some(cred_handler) = cred_handler {
1325 State::Proceed(cred_handler)
1326 } else {
1327 State::NoMatchingCred
1328 }
1329 } else {
1330 State::Expired
1331 };
1332
1333 let session_expiry = match session.state {
1334 SessionState::ExpiresAt(odt) => Some(odt),
1335 SessionState::NeverExpires => None,
1336 SessionState::RevokedAt(_) => {
1337 security_error!(
1338 "Invalid State - Should not be possible to trigger re-auth on revoked session."
1339 );
1340 return (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()));
1341 }
1342 };
1343
1344 match state {
1345 State::Proceed(handler) => {
1346 let allow = handler.next_auth_allowed();
1347 let auth_session = AuthSession {
1348 account: asd.account,
1349 account_policy: asd.account_policy,
1350 state: AuthSessionState::InProgress(handler),
1351 issue: asd.issue,
1352 intent: AuthIntent::Reauth {
1353 session_id,
1354 session_expiry,
1355 },
1356 source: asd.client_auth_info.source,
1357 key_object,
1358 };
1359
1360 let as_state = AuthState::Continue(allow);
1361 (Some(auth_session), as_state)
1362 }
1363 State::Expired => {
1364 security_info!("account expired");
1365 (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()))
1366 }
1367 State::NoMatchingCred => {
1368 security_error!("Unable to select a credential for authentication");
1369 (None, AuthState::Denied(BAD_CREDENTIALS.to_string()))
1370 }
1371 }
1372 }
1373
1374 pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
1377 match &self.state {
1378 AuthSessionState::InProgress(CredHandler::Password { cred_id, .. })
1379 | AuthSessionState::InProgress(CredHandler::PasswordTotp { cred_id, .. })
1380 | AuthSessionState::InProgress(CredHandler::PasswordBackupCode { cred_id, .. }) => {
1381 Ok(Some(*cred_id))
1382 }
1383 AuthSessionState::InProgress(CredHandler::Anonymous { .. })
1384 | AuthSessionState::InProgress(CredHandler::PasswordSecurityKey { .. })
1385 | AuthSessionState::InProgress(CredHandler::Passkey { .. })
1386 | AuthSessionState::InProgress(CredHandler::AttestedPasskey { .. }) => Ok(None),
1387
1388 AuthSessionState::Init(_) => {
1389 debug!(
1390 "Request for credential uuid invalid as auth session state not yet initialised"
1391 );
1392 Err(OperationError::AU0001InvalidState)
1393 }
1394 AuthSessionState::Success | AuthSessionState::Denied(_) => {
1395 debug!("Request for credential uuid invalid as auth session state has progressed");
1396 Err(OperationError::AU0001InvalidState)
1397 }
1398 }
1399 }
1400
1401 pub fn start_session(
1405 &mut self,
1406 mech: &AuthMech,
1407 ) -> Result<AuthState, OperationError> {
1410 let (next_state, response) = match &mut self.state {
1416 AuthSessionState::Success
1417 | AuthSessionState::Denied(_)
1418 | AuthSessionState::InProgress(_) => (
1419 None,
1420 Err(OperationError::InvalidAuthState(
1421 "session already finalised!".to_string(),
1422 )),
1423 ),
1424 AuthSessionState::Init(handlers) => {
1425 let mut allowed_handlers: Vec<_> = handlers
1427 .iter()
1428 .filter(|ch| ch.can_proceed(mech))
1429 .cloned()
1430 .collect();
1431
1432 if let Some(allowed_handler) = allowed_handlers.pop() {
1433 let allowed: Vec<_> = allowed_handler.next_auth_allowed();
1434
1435 if allowed.is_empty() {
1436 security_info!("Unable to negotiate credentials");
1437 (
1438 None,
1439 Err(OperationError::InvalidAuthState(
1440 "unable to negotiate credentials".to_string(),
1441 )),
1442 )
1443 } else {
1444 (
1445 Some(AuthSessionState::InProgress(allowed_handler)),
1446 Ok(AuthState::Continue(allowed)),
1447 )
1448 }
1449 } else {
1450 security_error!("Unable to select a credential for authentication");
1451 (
1452 Some(AuthSessionState::Denied(BAD_CREDENTIALS)),
1453 Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())),
1454 )
1455 }
1456 }
1457 };
1458
1459 if let Some(mut next_state) = next_state {
1460 std::mem::swap(&mut self.state, &mut next_state);
1461 };
1462
1463 response
1464 }
1465
1466 pub fn validate_creds(
1470 &mut self,
1471 cred: &AuthCredential,
1472 time: Duration,
1473 async_tx: &Sender<DelayedAction>,
1474 audit_tx: &Sender<AuditEvent>,
1475 webauthn: &Webauthn,
1476 pw_badlist: &HashSet<String>,
1477 ) -> Result<AuthState, OperationError> {
1478 let (next_state, response) = match &mut self.state {
1479 AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => {
1480 return Err(OperationError::InvalidAuthState(
1481 "session already finalised!".to_string(),
1482 ));
1483 }
1484 AuthSessionState::InProgress(ref mut handler) => {
1485 match handler.validate(
1486 cred,
1487 time,
1488 self.account.uuid,
1489 async_tx,
1490 webauthn,
1491 pw_badlist,
1492 ) {
1493 CredState::Success { auth_type, cred_id } => {
1494 let uat = self.issue_uat(auth_type, time, async_tx, cred_id)?;
1496
1497 let jwt = Jws::into_json(&uat).map_err(|e| {
1498 admin_error!(?e, "Failed to serialise into Jws");
1499 OperationError::AU0002JwsSerialisation
1500 })?;
1501
1502 let token = self.key_object.jws_es256_sign(&jwt, time).map_err(|e| {
1504 admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
1505 OperationError::AU0003JwsSignature
1506 })?;
1507
1508 (
1509 Some(AuthSessionState::Success),
1510 Ok(AuthState::Success(Box::new(token), self.issue)),
1511 )
1512 }
1513 CredState::Continue(allowed) => {
1514 security_info!(?allowed, "Request credential continuation");
1515 (None, Ok(AuthState::Continue(allowed.into_iter().collect())))
1516 }
1517 CredState::Denied(reason) => {
1518 if audit_tx
1519 .send(AuditEvent::AuthenticationDenied {
1520 source: self.source.clone().into(),
1521 spn: self.account.spn.clone(),
1522 uuid: self.account.uuid,
1523 time: OffsetDateTime::UNIX_EPOCH + time,
1524 })
1525 .is_err()
1526 {
1527 error!("Unable to submit audit event to queue");
1528 }
1529 security_info!(%reason, "Credentials denied");
1530 (
1531 Some(AuthSessionState::Denied(reason)),
1532 Ok(AuthState::Denied(reason.to_string())),
1533 )
1534 }
1535 }
1536 }
1537 };
1538
1539 if let Some(mut next_state) = next_state {
1540 std::mem::swap(&mut self.state, &mut next_state);
1541 };
1542
1543 response
1556 }
1557
1558 fn issue_uat(
1559 &mut self,
1560 auth_type: AuthType,
1561 time: Duration,
1562 async_tx: &Sender<DelayedAction>,
1563 cred_id: Uuid,
1564 ) -> Result<UserAuthToken, OperationError> {
1565 security_debug!("Successful cred handling");
1566 match self.intent {
1567 AuthIntent::InitialAuth { privileged } => {
1568 let session_id = Uuid::new_v4();
1569 let scope = match auth_type {
1572 AuthType::Anonymous => SessionScope::ReadOnly,
1573 AuthType::GeneratedPassword => SessionScope::ReadWrite,
1574 AuthType::Password
1575 | AuthType::PasswordTotp
1576 | AuthType::PasswordBackupCode
1577 | AuthType::PasswordSecurityKey
1578 | AuthType::Passkey
1579 | AuthType::AttestedPasskey => {
1580 if privileged {
1581 SessionScope::ReadWrite
1582 } else {
1583 SessionScope::PrivilegeCapable
1584 }
1585 }
1586 };
1587
1588 security_info!(
1589 "Issuing {:?} session ({:?}) {} for {} {}",
1590 self.issue,
1591 scope,
1592 session_id,
1593 self.account.spn,
1594 self.account.uuid
1595 );
1596
1597 let uat = self
1598 .account
1599 .to_userauthtoken(session_id, scope, time, &self.account_policy)
1600 .ok_or(OperationError::AU0004UserAuthTokenInvalid)?;
1601
1602 match auth_type {
1608 AuthType::Anonymous => {
1609 }
1611 AuthType::Password
1612 | AuthType::GeneratedPassword
1613 | AuthType::PasswordTotp
1614 | AuthType::PasswordBackupCode
1615 | AuthType::PasswordSecurityKey
1616 | AuthType::Passkey
1617 | AuthType::AttestedPasskey => {
1618 trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
1619 async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
1620 target_uuid: self.account.uuid,
1621 session_id,
1622 cred_id,
1623 label: "Auth Session".to_string(),
1624 expiry: uat.expiry,
1625 issued_at: uat.issued_at,
1626 issued_by: IdentityId::User(self.account.uuid),
1627 scope,
1628 type_: auth_type,
1629 }))
1630 .map_err(|e| {
1631 debug!(?e, "queue failure");
1632 admin_error!("unable to queue failing authentication as the session will not validate ... ");
1633 OperationError::AU0005DelayedProcessFailure
1634 })?;
1635 }
1636 };
1637
1638 Ok(uat)
1639 }
1640 AuthIntent::Reauth {
1641 session_id,
1642 session_expiry,
1643 } => {
1644 let scope = match auth_type {
1647 AuthType::Anonymous | AuthType::GeneratedPassword => {
1648 error!("AuthType used in Reauth is not valid for session re-issuance. Rejecting");
1649 return Err(OperationError::AU0006CredentialMayNotReauthenticate);
1650 }
1651 AuthType::Password
1652 | AuthType::PasswordTotp
1653 | AuthType::PasswordBackupCode
1654 | AuthType::PasswordSecurityKey
1655 | AuthType::Passkey
1656 | AuthType::AttestedPasskey => SessionScope::PrivilegeCapable,
1657 };
1658
1659 let uat = self
1660 .account
1661 .to_reissue_userauthtoken(
1662 session_id,
1663 session_expiry,
1664 scope,
1665 time,
1666 &self.account_policy,
1667 )
1668 .ok_or(OperationError::AU0007UserAuthTokenInvalid)?;
1669
1670 Ok(uat)
1671 }
1672 }
1673 }
1674
1675 pub fn end_session(&mut self, reason: &'static str) -> Result<AuthState, OperationError> {
1677 let mut next_state = AuthSessionState::Denied(reason);
1678 std::mem::swap(&mut self.state, &mut next_state);
1679 Ok(AuthState::Denied(reason.to_string()))
1680 }
1681
1682 fn valid_auth_mechs(&self) -> Vec<AuthMech> {
1683 match &self.state {
1684 AuthSessionState::Success
1685 | AuthSessionState::Denied(_)
1686 | AuthSessionState::InProgress(_) => Vec::with_capacity(0),
1687 AuthSessionState::Init(handlers) => {
1688 handlers.iter().map(|h| h.allows_mech()).collect()
1691 }
1692 }
1693 }
1694}
1695
1696#[cfg(test)]
1697mod tests {
1698 use std::time::Duration;
1699
1700 use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
1701 use hashbrown::HashSet;
1702 use kanidm_proto::internal::{UatPurpose, UserAuthToken};
1703 use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
1704 use tokio::sync::mpsc::unbounded_channel as unbounded;
1705 use webauthn_authenticator_rs::softpasskey::SoftPasskey;
1706 use webauthn_authenticator_rs::WebauthnAuthenticator;
1707 use webauthn_rs::prelude::{RequestChallengeResponse, Webauthn};
1708
1709 use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
1710 use crate::credential::{BackupCodes, Credential};
1711 use crate::idm::account::Account;
1712 use crate::idm::accountpolicy::ResolvedAccountPolicy;
1713 use crate::idm::audit::AuditEvent;
1714 use crate::idm::authsession::{
1715 AuthSession, AuthSessionData, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG,
1716 BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG,
1717 };
1718 use crate::idm::delayed::DelayedAction;
1719 use crate::idm::AuthState;
1720 use crate::migration_data::{BUILTIN_ACCOUNT_ANONYMOUS, BUILTIN_ACCOUNT_TEST_PERSON};
1721 use crate::prelude::*;
1722 use crate::server::keys::KeyObjectInternal;
1723 use crate::utils::readable_password_from_random;
1724 use kanidm_lib_crypto::CryptoPolicy;
1725
1726 fn create_pw_badlist_cache() -> HashSet<String> {
1727 let mut s = HashSet::new();
1728 s.insert("list@no3IBTyqHu$bad".to_lowercase());
1729 s
1730 }
1731
1732 fn create_webauthn() -> webauthn_rs::Webauthn {
1733 webauthn_rs::WebauthnBuilder::new(
1734 "example.com",
1735 &url::Url::parse("https://idm.example.com").unwrap(),
1736 )
1737 .and_then(|builder| builder.build())
1738 .unwrap()
1739 }
1740
1741 #[test]
1742 fn test_idm_authsession_anonymous_auth_mech() {
1743 sketching::test_init();
1744
1745 let webauthn = create_webauthn();
1746
1747 let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
1748
1749 let asd = AuthSessionData {
1750 account: anon_account,
1751 account_policy: ResolvedAccountPolicy::default(),
1752 issue: AuthIssueSession::Token,
1753 webauthn: &webauthn,
1754 ct: duration_from_epoch_now(),
1755 client_auth_info: Source::Internal.into(),
1756 };
1757
1758 let key_object = KeyObjectInternal::new_test();
1759 let (session, state) = AuthSession::new(asd, false, key_object);
1760 if let AuthState::Choose(auth_mechs) = state {
1761 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
1762 } else {
1763 panic!("Invalid auth state")
1764 }
1765
1766 let state = session
1767 .expect("Missing auth session?")
1768 .start_session(&AuthMech::Anonymous)
1769 .expect("Failed to select anonymous mech.");
1770
1771 if let AuthState::Continue(auth_mechs) = state {
1772 assert!(auth_mechs
1773 .iter()
1774 .any(|x| matches!(x, AuthAllowed::Anonymous)));
1775 } else {
1776 panic!("Invalid auth state")
1777 }
1778 }
1779
1780 macro_rules! start_password_session {
1781 (
1782 $audit:expr,
1783 $account:expr,
1784 $webauthn:expr,
1785 $privileged:expr
1786 ) => {{
1787 let asd = AuthSessionData {
1788 account: $account.clone(),
1789 account_policy: ResolvedAccountPolicy::default(),
1790 issue: AuthIssueSession::Token,
1791 webauthn: $webauthn,
1792 ct: duration_from_epoch_now(),
1793 client_auth_info: Source::Internal.into(),
1794 };
1795 let key_object = KeyObjectInternal::new_test();
1796 let (session, state) = AuthSession::new(asd, $privileged, key_object);
1797 let mut session = session.unwrap();
1798
1799 if let AuthState::Choose(auth_mechs) = state {
1800 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Password)));
1801 } else {
1802 panic!();
1803 }
1804
1805 let state = session
1806 .start_session(&AuthMech::Password)
1807 .expect("Failed to select anonymous mech.");
1808
1809 if let AuthState::Continue(auth_mechs) = state {
1810 assert!(auth_mechs
1811 .iter()
1812 .any(|x| matches!(x, AuthAllowed::Password)));
1813 } else {
1814 panic!("Invalid auth state")
1815 }
1816
1817 (session, create_pw_badlist_cache())
1818 }};
1819 }
1820
1821 fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
1822 let webauthn = create_webauthn();
1823 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1825 let p = CryptoPolicy::minimum();
1827 let cred = Credential::new_password_only(&p, "test_password").unwrap();
1828 account.primary = Some(cred);
1829
1830 let (async_tx, mut async_rx) = unbounded();
1831 let (audit_tx, mut audit_rx) = unbounded();
1832
1833 let (mut session, pw_badlist_cache) =
1835 start_password_session!(&mut audit, account, &webauthn, false);
1836
1837 let attempt = AuthCredential::Password("bad_password".to_string());
1838 match session.validate_creds(
1839 &attempt,
1840 Duration::from_secs(0),
1841 &async_tx,
1842 &audit_tx,
1843 &webauthn,
1844 &pw_badlist_cache,
1845 ) {
1846 Ok(AuthState::Denied(_)) => {}
1847 _ => panic!(),
1848 };
1849
1850 match audit_rx.try_recv() {
1851 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1852 _ => panic!("Oh no"),
1853 }
1854
1855 let (mut session, pw_badlist_cache) =
1858 start_password_session!(&mut audit, account, &webauthn, privileged);
1859
1860 let attempt = AuthCredential::Password("test_password".to_string());
1861 let uat: UserAuthToken = match session.validate_creds(
1862 &attempt,
1863 Duration::from_secs(0),
1864 &async_tx,
1865 &audit_tx,
1866 &webauthn,
1867 &pw_badlist_cache,
1868 ) {
1869 Ok(AuthState::Success(jwsc, AuthIssueSession::Token)) => {
1870 let jws_verifier = JwsDangerReleaseWithoutVerify::default();
1871
1872 jws_verifier
1873 .verify(&*jwsc)
1874 .unwrap()
1875 .from_json::<UserAuthToken>()
1876 .unwrap()
1877 }
1878 _ => panic!(),
1879 };
1880
1881 match async_rx.blocking_recv() {
1882 Some(DelayedAction::AuthSessionRecord(_)) => {}
1883 _ => panic!("Oh no"),
1884 }
1885
1886 drop(async_tx);
1887 assert!(async_rx.blocking_recv().is_none());
1888 drop(audit_tx);
1889 assert!(audit_rx.blocking_recv().is_none());
1890
1891 uat
1892 }
1893
1894 #[test]
1895 fn test_idm_authsession_simple_password_mech() {
1896 sketching::test_init();
1897 let uat = start_session_simple_password_mech(false);
1898 match uat.purpose {
1899 UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1900 UatPurpose::ReadWrite { expiry } => {
1901 assert!(expiry.is_none())
1903 }
1904 }
1905 }
1906
1907 #[test]
1908 fn test_idm_authsession_simple_password_mech_priv_shortcut() {
1909 sketching::test_init();
1910 let uat = start_session_simple_password_mech(true);
1911 match uat.purpose {
1912 UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1913 UatPurpose::ReadWrite { expiry } => {
1914 assert!(expiry.is_some())
1916 }
1917 }
1918 }
1919
1920 #[test]
1921 fn test_idm_authsession_simple_password_badlist() {
1922 sketching::test_init();
1923 let webauthn = create_webauthn();
1924 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1926 let p = CryptoPolicy::minimum();
1928 let cred = Credential::new_password_only(&p, "list@no3IBTyqHu$bad").unwrap();
1929 account.primary = Some(cred);
1930
1931 let (async_tx, mut async_rx) = unbounded();
1932 let (audit_tx, mut audit_rx) = unbounded();
1933
1934 let (mut session, pw_badlist_cache) =
1936 start_password_session!(&mut audit, account, &webauthn, false);
1937
1938 let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
1939 match session.validate_creds(
1940 &attempt,
1941 Duration::from_secs(0),
1942 &async_tx,
1943 &audit_tx,
1944 &webauthn,
1945 &pw_badlist_cache,
1946 ) {
1947 Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
1948 _ => panic!(),
1949 };
1950
1951 match audit_rx.try_recv() {
1952 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1953 _ => panic!("Oh no"),
1954 }
1955
1956 drop(async_tx);
1957 assert!(async_rx.blocking_recv().is_none());
1958 drop(audit_tx);
1959 assert!(audit_rx.blocking_recv().is_none());
1960 }
1961
1962 fn start_password_totp_session(
1963 account: &Account,
1964 webauthn: &Webauthn,
1965 ) -> (AuthSession, HashSet<String>) {
1966 let asd = AuthSessionData {
1967 account: account.clone(),
1968 account_policy: ResolvedAccountPolicy::default(),
1969 issue: AuthIssueSession::Token,
1970 webauthn,
1971 ct: duration_from_epoch_now(),
1972 client_auth_info: Source::Internal.into(),
1973 };
1974 let key_object = KeyObjectInternal::new_test();
1975 let (session, state) = AuthSession::new(asd, false, key_object);
1976 let mut session = session.expect("Session was unable to be created.");
1977
1978 if let AuthState::Choose(auth_mechs) = state {
1979 assert!(auth_mechs
1980 .iter()
1981 .any(|x| matches!(x, AuthMech::PasswordTotp)))
1982 } else {
1983 panic!();
1984 }
1985
1986 let state = session
1987 .start_session(&AuthMech::PasswordTotp)
1988 .expect("Failed to select password totp mech.");
1989
1990 if let AuthState::Continue(auth_mechs) = state {
1991 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
1992 AuthAllowed::Totp => true,
1993 _ => acc,
1994 }));
1995 } else {
1996 panic!("Invalid auth state")
1997 }
1998
1999 (session, create_pw_badlist_cache())
2000 }
2001
2002 fn start_password_sk_session(
2003 account: &Account,
2004 webauthn: &Webauthn,
2005 ) -> (AuthSession, RequestChallengeResponse, HashSet<String>) {
2006 let asd = AuthSessionData {
2007 account: account.clone(),
2008 account_policy: ResolvedAccountPolicy::default(),
2009 issue: AuthIssueSession::Token,
2010 webauthn,
2011 ct: duration_from_epoch_now(),
2012 client_auth_info: Source::Internal.into(),
2013 };
2014 let key_object = KeyObjectInternal::new_test();
2015 let (session, state) = AuthSession::new(asd, false, key_object);
2016 let mut session = session.expect("Session was unable to be created.");
2017
2018 if let AuthState::Choose(auth_mechs) = state {
2019 assert!(auth_mechs
2020 .iter()
2021 .any(|x| matches!(x, AuthMech::PasswordSecurityKey)))
2022 } else {
2023 panic!();
2024 }
2025
2026 let state = session
2027 .start_session(&AuthMech::PasswordSecurityKey)
2028 .expect("Failed to select password security key mech.");
2029
2030 let mut rchal = None;
2031
2032 if let AuthState::Continue(auth_mechs) = state {
2033 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2034 AuthAllowed::SecurityKey(chal) => {
2035 rchal = Some(chal.clone());
2036 true
2037 }
2038 _ => acc,
2039 }));
2040 } else {
2041 panic!("Invalid auth state")
2042 }
2043
2044 (session, rchal.unwrap(), create_pw_badlist_cache())
2045 }
2046
2047 fn start_password_bc_session(
2048 account: &Account,
2049 webauthn: &Webauthn,
2050 ) -> (AuthSession, HashSet<String>) {
2051 let asd = AuthSessionData {
2052 account: account.clone(),
2053 account_policy: ResolvedAccountPolicy::default(),
2054 issue: AuthIssueSession::Token,
2055 webauthn,
2056 ct: duration_from_epoch_now(),
2057 client_auth_info: Source::Internal.into(),
2058 };
2059 let key_object = KeyObjectInternal::new_test();
2060 let (session, state) = AuthSession::new(asd, false, key_object);
2061 let mut session = session.expect("Session was unable to be created.");
2062
2063 if let AuthState::Choose(auth_mechs) = state {
2064 assert!(auth_mechs
2065 .iter()
2066 .any(|x| matches!(x, AuthMech::PasswordBackupCode)))
2067 } else {
2068 panic!();
2069 }
2070
2071 let state = session
2072 .start_session(&AuthMech::PasswordBackupCode)
2073 .expect("Failed to select password backup code mech.");
2074
2075 if let AuthState::Continue(auth_mechs) = state {
2076 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2077 AuthAllowed::BackupCode => true,
2078 _ => acc,
2079 }));
2080 } else {
2081 panic!("Invalid auth state")
2082 }
2083
2084 (session, create_pw_badlist_cache())
2085 }
2086
2087 #[test]
2088 fn test_idm_authsession_totp_password_mech() {
2089 sketching::test_init();
2090 let webauthn = create_webauthn();
2091 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2093
2094 let ts = Duration::from_secs(12345);
2096
2097 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2099
2100 let totp_good = totp
2101 .do_totp_duration_from_epoch(&ts)
2102 .expect("failed to perform totp.");
2103 let totp_bad = totp
2104 .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2105 .expect("failed to perform totp.");
2106 assert!(totp_bad != totp_good);
2107
2108 let pw_good = "test_password";
2109 let pw_bad = "bad_password";
2110
2111 let p = CryptoPolicy::minimum();
2112 let cred = Credential::new_password_only(&p, pw_good)
2113 .unwrap()
2114 .append_totp("totp".to_string(), totp);
2115 account.primary = Some(cred);
2117
2118 let (async_tx, mut async_rx) = unbounded();
2119 let (audit_tx, mut audit_rx) = unbounded();
2120
2121 {
2125 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2126
2127 match session.validate_creds(
2128 &AuthCredential::Anonymous,
2129 ts,
2130 &async_tx,
2131 &audit_tx,
2132 &webauthn,
2133 &pw_badlist_cache,
2134 ) {
2135 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2136 _ => panic!(),
2137 };
2138
2139 match audit_rx.try_recv() {
2140 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2141 _ => panic!("Oh no"),
2142 }
2143 }
2144
2145 {
2149 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2150
2151 match session.validate_creds(
2152 &AuthCredential::Password(pw_bad.to_string()),
2153 ts,
2154 &async_tx,
2155 &audit_tx,
2156 &webauthn,
2157 &pw_badlist_cache,
2158 ) {
2159 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2160 _ => panic!(),
2161 };
2162
2163 match audit_rx.try_recv() {
2164 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2165 _ => panic!("Oh no"),
2166 }
2167 }
2168 {
2170 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2171
2172 match session.validate_creds(
2173 &AuthCredential::Totp(totp_bad),
2174 ts,
2175 &async_tx,
2176 &audit_tx,
2177 &webauthn,
2178 &pw_badlist_cache,
2179 ) {
2180 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2181 _ => panic!(),
2182 };
2183
2184 match audit_rx.try_recv() {
2185 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2186 _ => panic!("Oh no"),
2187 }
2188 }
2189
2190 {
2193 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2194
2195 match session.validate_creds(
2196 &AuthCredential::Totp(totp_good),
2197 ts,
2198 &async_tx,
2199 &audit_tx,
2200 &webauthn,
2201 &pw_badlist_cache,
2202 ) {
2203 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2204 _ => panic!(),
2205 };
2206 match session.validate_creds(
2207 &AuthCredential::Password(pw_bad.to_string()),
2208 ts,
2209 &async_tx,
2210 &audit_tx,
2211 &webauthn,
2212 &pw_badlist_cache,
2213 ) {
2214 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2215 _ => panic!(),
2216 };
2217
2218 match audit_rx.try_recv() {
2219 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2220 _ => panic!("Oh no"),
2221 }
2222 }
2223
2224 {
2227 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2228
2229 match session.validate_creds(
2230 &AuthCredential::Totp(totp_good),
2231 ts,
2232 &async_tx,
2233 &audit_tx,
2234 &webauthn,
2235 &pw_badlist_cache,
2236 ) {
2237 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2238 _ => panic!(),
2239 };
2240 match session.validate_creds(
2241 &AuthCredential::Password(pw_good.to_string()),
2242 ts,
2243 &async_tx,
2244 &audit_tx,
2245 &webauthn,
2246 &pw_badlist_cache,
2247 ) {
2248 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2249 _ => panic!(),
2250 };
2251
2252 match async_rx.blocking_recv() {
2253 Some(DelayedAction::AuthSessionRecord(_)) => {}
2254 _ => panic!("Oh no"),
2255 }
2256 }
2257
2258 drop(async_tx);
2259 assert!(async_rx.blocking_recv().is_none());
2260 drop(audit_tx);
2261 assert!(audit_rx.blocking_recv().is_none());
2262 }
2263
2264 #[test]
2265 fn test_idm_authsession_password_mfa_badlist() {
2266 sketching::test_init();
2267 let webauthn = create_webauthn();
2268 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2270
2271 let ts = Duration::from_secs(12345);
2273
2274 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2276
2277 let totp_good = totp
2278 .do_totp_duration_from_epoch(&ts)
2279 .expect("failed to perform totp.");
2280
2281 let pw_badlist = "list@no3IBTyqHu$bad";
2282
2283 let p = CryptoPolicy::minimum();
2284 let cred = Credential::new_password_only(&p, pw_badlist)
2285 .unwrap()
2286 .append_totp("totp".to_string(), totp);
2287 account.primary = Some(cred);
2289
2290 let (async_tx, mut async_rx) = unbounded();
2291 let (audit_tx, mut audit_rx) = unbounded();
2292
2293 {
2300 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2301
2302 match session.validate_creds(
2303 &AuthCredential::Totp(totp_good),
2304 ts,
2305 &async_tx,
2306 &audit_tx,
2307 &webauthn,
2308 &pw_badlist_cache,
2309 ) {
2310 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2311 _ => panic!(),
2312 };
2313 match session.validate_creds(
2314 &AuthCredential::Password(pw_badlist.to_string()),
2315 ts,
2316 &async_tx,
2317 &audit_tx,
2318 &webauthn,
2319 &pw_badlist_cache,
2320 ) {
2321 Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
2322 _ => panic!(),
2323 };
2324
2325 match audit_rx.try_recv() {
2326 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2327 _ => panic!("Oh no"),
2328 }
2329 }
2330
2331 drop(async_tx);
2332 assert!(async_rx.blocking_recv().is_none());
2333 drop(audit_tx);
2334 assert!(audit_rx.blocking_recv().is_none());
2335 }
2336
2337 macro_rules! start_webauthn_only_session {
2338 (
2339 $audit:expr,
2340 $account:expr,
2341 $webauthn:expr
2342 ) => {{
2343 let asd = AuthSessionData {
2344 account: $account.clone(),
2345 account_policy: ResolvedAccountPolicy::default(),
2346 issue: AuthIssueSession::Token,
2347 webauthn: $webauthn,
2348 ct: duration_from_epoch_now(),
2349 client_auth_info: Source::Internal.into(),
2350 };
2351 let key_object = KeyObjectInternal::new_test();
2352 let (session, state) = AuthSession::new(asd, false, key_object);
2353 let mut session = session.unwrap();
2354
2355 if let AuthState::Choose(auth_mechs) = state {
2356 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Passkey)));
2357 } else {
2358 panic!();
2359 }
2360
2361 let state = session
2362 .start_session(&AuthMech::Passkey)
2363 .expect("Failed to select Passkey mech.");
2364
2365 let wan_chal = if let AuthState::Continue(auth_mechs) = state {
2366 assert_eq!(auth_mechs.len(), 1);
2367 auth_mechs
2368 .into_iter()
2369 .fold(None, |_acc, x| match x {
2370 AuthAllowed::Passkey(chal) => Some(chal),
2371 _ => None,
2372 })
2373 .expect("No securitykey challenge found.")
2374 } else {
2375 panic!();
2376 };
2377
2378 (session, wan_chal)
2379 }};
2380 }
2381
2382 fn setup_webauthn_passkey(
2383 name: &str,
2384 ) -> (
2385 webauthn_rs::prelude::Webauthn,
2386 webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2387 webauthn_rs::prelude::Passkey,
2388 ) {
2389 let webauthn = create_webauthn();
2390 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2392
2393 let uuid = Uuid::new_v4();
2394
2395 let (chal, reg_state) = webauthn
2396 .start_passkey_registration(uuid, name, name, None)
2397 .expect("Failed to setup passkey rego challenge");
2398
2399 let r = wa
2400 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2401 .expect("Failed to create soft passkey");
2402
2403 let wan_cred = webauthn
2404 .finish_passkey_registration(&r, ®_state)
2405 .expect("Failed to register soft token");
2406
2407 (webauthn, wa, wan_cred)
2408 }
2409
2410 fn setup_webauthn_securitykey(
2411 name: &str,
2412 ) -> (
2413 webauthn_rs::prelude::Webauthn,
2414 webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2415 webauthn_rs::prelude::SecurityKey,
2416 ) {
2417 let webauthn = create_webauthn();
2418 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2420
2421 let uuid = Uuid::new_v4();
2422
2423 let (chal, reg_state) = webauthn
2424 .start_securitykey_registration(uuid, name, name, None, None, None)
2425 .expect("Failed to setup passkey rego challenge");
2426
2427 let r = wa
2428 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2429 .expect("Failed to create soft securitykey");
2430
2431 let wan_cred = webauthn
2432 .finish_securitykey_registration(&r, ®_state)
2433 .expect("Failed to register soft token");
2434
2435 (webauthn, wa, wan_cred)
2436 }
2437
2438 #[test]
2439 fn test_idm_authsession_webauthn_only_mech() {
2440 sketching::test_init();
2441 let (async_tx, mut async_rx) = unbounded();
2442 let (audit_tx, mut audit_rx) = unbounded();
2443 let ts = duration_from_epoch_now();
2444 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2446
2447 let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
2448
2449 account.passkeys = btreemap![(Uuid::new_v4(), ("soft".to_string(), wan_cred))];
2451
2452 {
2456 let (mut session, _inv_chal) =
2457 start_webauthn_only_session!(&mut audit, account, &webauthn);
2458
2459 match session.validate_creds(
2460 &AuthCredential::Anonymous,
2461 ts,
2462 &async_tx,
2463 &audit_tx,
2464 &webauthn,
2465 &Default::default(),
2466 ) {
2467 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2468 _ => panic!(),
2469 };
2470
2471 match audit_rx.try_recv() {
2472 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2473 _ => panic!("Oh no"),
2474 }
2475 }
2476
2477 {
2479 let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2480
2481 let resp = wa
2482 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2483 .map(Box::new)
2484 .expect("failed to use softtoken to authenticate");
2485
2486 match session.validate_creds(
2487 &AuthCredential::Passkey(resp),
2488 ts,
2489 &async_tx,
2490 &audit_tx,
2491 &webauthn,
2492 &Default::default(),
2493 ) {
2494 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2495 _ => panic!(),
2496 };
2497
2498 match async_rx.blocking_recv() {
2500 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2501 _ => panic!("Oh no"),
2502 }
2503 match async_rx.blocking_recv() {
2504 Some(DelayedAction::AuthSessionRecord(_)) => {}
2505 _ => panic!("Oh no"),
2506 }
2507 }
2508
2509 {
2511 let (_session, inv_chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2512 let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2513
2514 let resp = wa
2515 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2517 .map(Box::new)
2518 .expect("failed to use softtoken to authenticate");
2519
2520 match session.validate_creds(
2521 &AuthCredential::Passkey(resp),
2522 ts,
2523 &async_tx,
2524 &audit_tx,
2525 &webauthn,
2526 &Default::default(),
2527 ) {
2528 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2529 _ => panic!(),
2530 };
2531
2532 match audit_rx.try_recv() {
2533 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2534 _ => panic!("Oh no"),
2535 }
2536 }
2537
2538 {
2540 let mut inv_wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2541 let (chal, reg_state) = webauthn
2542 .start_passkey_registration(account.uuid, &account.name, &account.displayname, None)
2543 .expect("Failed to setup webauthn rego challenge");
2544
2545 let r = inv_wa
2546 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2547 .expect("Failed to create soft token");
2548
2549 let inv_cred = webauthn
2550 .finish_passkey_registration(&r, ®_state)
2551 .expect("Failed to register soft token");
2552
2553 let (chal, _auth_state) = webauthn
2555 .start_passkey_authentication(&vec![inv_cred])
2556 .expect("Failed to generate challenge for in inv softtoken");
2557
2558 let resp = inv_wa
2560 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2561 .map(Box::new)
2562 .expect("Failed to use softtoken for response.");
2563
2564 let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2565 match session.validate_creds(
2569 &AuthCredential::Passkey(resp),
2570 ts,
2571 &async_tx,
2572 &audit_tx,
2573 &webauthn,
2574 &Default::default(),
2575 ) {
2576 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2577 _ => panic!(),
2578 };
2579
2580 match audit_rx.try_recv() {
2581 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2582 _ => panic!("Oh no"),
2583 }
2584 }
2585
2586 drop(async_tx);
2587 assert!(async_rx.blocking_recv().is_none());
2588 drop(audit_tx);
2589 assert!(audit_rx.blocking_recv().is_none());
2590 }
2591
2592 #[test]
2593 fn test_idm_authsession_webauthn_password_mech() {
2594 sketching::test_init();
2595 let (async_tx, mut async_rx) = unbounded();
2596 let (audit_tx, mut audit_rx) = unbounded();
2597 let ts = duration_from_epoch_now();
2598 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2600
2601 let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2602 let pw_good = "test_password";
2603 let pw_bad = "bad_password";
2604
2605 let p = CryptoPolicy::minimum();
2607 let cred = Credential::new_password_only(&p, pw_good)
2608 .unwrap()
2609 .append_securitykey("soft".to_string(), wan_cred)
2610 .unwrap();
2611
2612 account.primary = Some(cred);
2613
2614 {
2616 let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2617
2618 match session.validate_creds(
2619 &AuthCredential::Password(pw_bad.to_string()),
2620 ts,
2621 &async_tx,
2622 &audit_tx,
2623 &webauthn,
2624 &pw_badlist_cache,
2625 ) {
2626 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2627 _ => panic!(),
2628 };
2629
2630 match audit_rx.try_recv() {
2631 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2632 _ => panic!("Oh no"),
2633 }
2634 }
2635
2636 {
2638 let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2639
2640 match session.validate_creds(
2641 &AuthCredential::Totp(0),
2642 ts,
2643 &async_tx,
2644 &audit_tx,
2645 &webauthn,
2646 &pw_badlist_cache,
2647 ) {
2648 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2649 _ => panic!(),
2650 };
2651
2652 match audit_rx.try_recv() {
2653 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2654 _ => panic!("Oh no"),
2655 }
2656 }
2657
2658 {
2662 let (_session, inv_chal, pw_badlist_cache) =
2663 start_password_sk_session(&account, &webauthn);
2664 let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2665
2666 let resp = wa
2667 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2669 .map(Box::new)
2670 .expect("failed to use softtoken to authenticate");
2671
2672 match session.validate_creds(
2673 &AuthCredential::SecurityKey(resp),
2674 ts,
2675 &async_tx,
2676 &audit_tx,
2677 &webauthn,
2678 &pw_badlist_cache,
2679 ) {
2680 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2681 _ => panic!(),
2682 };
2683
2684 match audit_rx.try_recv() {
2685 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2686 _ => panic!("Oh no"),
2687 }
2688 }
2689
2690 {
2692 let (mut session, chal, pw_badlist_cache) =
2693 start_password_sk_session(&account, &webauthn);
2694
2695 let resp = wa
2696 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2697 .map(Box::new)
2698 .expect("failed to use softtoken to authenticate");
2699
2700 match session.validate_creds(
2701 &AuthCredential::SecurityKey(resp),
2702 ts,
2703 &async_tx,
2704 &audit_tx,
2705 &webauthn,
2706 &pw_badlist_cache,
2707 ) {
2708 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2709 _ => panic!(),
2710 };
2711 match session.validate_creds(
2712 &AuthCredential::Password(pw_bad.to_string()),
2713 ts,
2714 &async_tx,
2715 &audit_tx,
2716 &webauthn,
2717 &pw_badlist_cache,
2718 ) {
2719 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2720 _ => panic!(),
2721 };
2722
2723 match audit_rx.try_recv() {
2724 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2725 _ => panic!("Oh no"),
2726 }
2727
2728 match async_rx.blocking_recv() {
2730 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2731 _ => panic!("Oh no"),
2732 }
2733 }
2734
2735 {
2737 let (mut session, chal, pw_badlist_cache) =
2738 start_password_sk_session(&account, &webauthn);
2739
2740 let resp = wa
2741 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2742 .map(Box::new)
2743 .expect("failed to use softtoken to authenticate");
2744
2745 match session.validate_creds(
2746 &AuthCredential::SecurityKey(resp),
2747 ts,
2748 &async_tx,
2749 &audit_tx,
2750 &webauthn,
2751 &pw_badlist_cache,
2752 ) {
2753 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2754 _ => panic!(),
2755 };
2756 match session.validate_creds(
2757 &AuthCredential::Password(pw_good.to_string()),
2758 ts,
2759 &async_tx,
2760 &audit_tx,
2761 &webauthn,
2762 &pw_badlist_cache,
2763 ) {
2764 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2765 _ => panic!(),
2766 };
2767
2768 match async_rx.blocking_recv() {
2770 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2771 _ => panic!("Oh no"),
2772 }
2773 match async_rx.blocking_recv() {
2774 Some(DelayedAction::AuthSessionRecord(_)) => {}
2775 _ => panic!("Oh no"),
2776 }
2777 }
2778
2779 drop(async_tx);
2780 assert!(async_rx.blocking_recv().is_none());
2781 drop(audit_tx);
2782 assert!(audit_rx.blocking_recv().is_none());
2783 }
2784
2785 #[test]
2786 fn test_idm_authsession_webauthn_password_totp_mech() {
2787 sketching::test_init();
2788 let (async_tx, mut async_rx) = unbounded();
2789 let (audit_tx, mut audit_rx) = unbounded();
2790 let ts = duration_from_epoch_now();
2791 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2793
2794 let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2795
2796 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2797 let totp_good = totp
2798 .do_totp_duration_from_epoch(&ts)
2799 .expect("failed to perform totp.");
2800 let totp_bad = totp
2801 .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2802 .expect("failed to perform totp.");
2803 assert!(totp_bad != totp_good);
2804
2805 let pw_good = "test_password";
2806 let pw_bad = "bad_password";
2807
2808 let p = CryptoPolicy::minimum();
2810 let cred = Credential::new_password_only(&p, pw_good)
2811 .unwrap()
2812 .append_securitykey("soft".to_string(), wan_cred)
2813 .unwrap()
2814 .append_totp("totp".to_string(), totp);
2815
2816 account.primary = Some(cred);
2817
2818 {
2820 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2821
2822 match session.validate_creds(
2823 &AuthCredential::Password(pw_bad.to_string()),
2824 ts,
2825 &async_tx,
2826 &audit_tx,
2827 &webauthn,
2828 &pw_badlist_cache,
2829 ) {
2830 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2831 _ => panic!(),
2832 };
2833
2834 match audit_rx.try_recv() {
2835 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2836 _ => panic!("Oh no"),
2837 }
2838 }
2839
2840 {
2842 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2843
2844 match session.validate_creds(
2845 &AuthCredential::Totp(totp_bad),
2846 ts,
2847 &async_tx,
2848 &audit_tx,
2849 &webauthn,
2850 &pw_badlist_cache,
2851 ) {
2852 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2853 _ => panic!(),
2854 };
2855
2856 match audit_rx.try_recv() {
2857 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2858 _ => panic!("Oh no"),
2859 }
2860 }
2861
2862 {
2864 let (_session, inv_chal, pw_badlist_cache) =
2865 start_password_sk_session(&account, &webauthn);
2866 let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2867
2868 let resp = wa
2869 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2871 .map(Box::new)
2872 .expect("failed to use softtoken to authenticate");
2873
2874 match session.validate_creds(
2875 &AuthCredential::SecurityKey(resp),
2876 ts,
2877 &async_tx,
2878 &audit_tx,
2879 &webauthn,
2880 &pw_badlist_cache,
2881 ) {
2882 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2883 _ => panic!(),
2884 };
2885
2886 match audit_rx.try_recv() {
2887 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2888 _ => panic!("Oh no"),
2889 }
2890 }
2891
2892 {
2894 let (mut session, chal, pw_badlist_cache) =
2895 start_password_sk_session(&account, &webauthn);
2896
2897 let resp = wa
2898 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2899 .map(Box::new)
2900 .expect("failed to use softtoken to authenticate");
2901
2902 match session.validate_creds(
2903 &AuthCredential::SecurityKey(resp),
2904 ts,
2905 &async_tx,
2906 &audit_tx,
2907 &webauthn,
2908 &pw_badlist_cache,
2909 ) {
2910 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2911 _ => panic!(),
2912 };
2913 match session.validate_creds(
2914 &AuthCredential::Password(pw_bad.to_string()),
2915 ts,
2916 &async_tx,
2917 &audit_tx,
2918 &webauthn,
2919 &pw_badlist_cache,
2920 ) {
2921 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2922 _ => panic!(),
2923 };
2924
2925 match audit_rx.try_recv() {
2926 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2927 _ => panic!("Oh no"),
2928 }
2929
2930 match async_rx.blocking_recv() {
2932 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2933 _ => panic!("Oh no"),
2934 }
2935 }
2936
2937 {
2939 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2940
2941 match session.validate_creds(
2942 &AuthCredential::Totp(totp_good),
2943 ts,
2944 &async_tx,
2945 &audit_tx,
2946 &webauthn,
2947 &pw_badlist_cache,
2948 ) {
2949 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2950 _ => panic!(),
2951 };
2952 match session.validate_creds(
2953 &AuthCredential::Password(pw_bad.to_string()),
2954 ts,
2955 &async_tx,
2956 &audit_tx,
2957 &webauthn,
2958 &pw_badlist_cache,
2959 ) {
2960 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2961 _ => panic!(),
2962 };
2963
2964 match audit_rx.try_recv() {
2965 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2966 _ => panic!("Oh no"),
2967 }
2968 }
2969
2970 {
2972 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2973
2974 match session.validate_creds(
2975 &AuthCredential::Totp(totp_good),
2976 ts,
2977 &async_tx,
2978 &audit_tx,
2979 &webauthn,
2980 &pw_badlist_cache,
2981 ) {
2982 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2983 _ => panic!(),
2984 };
2985 match session.validate_creds(
2986 &AuthCredential::Password(pw_good.to_string()),
2987 ts,
2988 &async_tx,
2989 &audit_tx,
2990 &webauthn,
2991 &pw_badlist_cache,
2992 ) {
2993 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2994 _ => panic!(),
2995 };
2996
2997 match async_rx.blocking_recv() {
2998 Some(DelayedAction::AuthSessionRecord(_)) => {}
2999 _ => panic!("Oh no"),
3000 }
3001 }
3002
3003 {
3005 let (mut session, chal, pw_badlist_cache) =
3006 start_password_sk_session(&account, &webauthn);
3007
3008 let resp = wa
3009 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
3010 .map(Box::new)
3011 .expect("failed to use softtoken to authenticate");
3012
3013 match session.validate_creds(
3014 &AuthCredential::SecurityKey(resp),
3015 ts,
3016 &async_tx,
3017 &audit_tx,
3018 &webauthn,
3019 &pw_badlist_cache,
3020 ) {
3021 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3022 _ => panic!(),
3023 };
3024 match session.validate_creds(
3025 &AuthCredential::Password(pw_good.to_string()),
3026 ts,
3027 &async_tx,
3028 &audit_tx,
3029 &webauthn,
3030 &pw_badlist_cache,
3031 ) {
3032 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3033 _ => panic!(),
3034 };
3035
3036 match async_rx.blocking_recv() {
3038 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
3039 _ => panic!("Oh no"),
3040 }
3041 match async_rx.blocking_recv() {
3042 Some(DelayedAction::AuthSessionRecord(_)) => {}
3043 _ => panic!("Oh no"),
3044 }
3045 }
3046
3047 drop(async_tx);
3048 assert!(async_rx.blocking_recv().is_none());
3049 drop(audit_tx);
3050 assert!(audit_rx.blocking_recv().is_none());
3051 }
3052
3053 #[test]
3054 fn test_idm_authsession_backup_code_mech() {
3055 sketching::test_init();
3056 let webauthn = create_webauthn();
3057 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3059
3060 let ts = Duration::from_secs(12345);
3062
3063 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
3065
3066 let totp_good = totp
3067 .do_totp_duration_from_epoch(&ts)
3068 .expect("failed to perform totp.");
3069
3070 let pw_good = "test_password";
3071 let pw_bad = "bad_password";
3072
3073 let backup_code_good = readable_password_from_random();
3074 let backup_code_bad = readable_password_from_random();
3075 assert!(backup_code_bad != backup_code_good);
3076 let mut code_set = HashSet::new();
3077 code_set.insert(backup_code_good.clone());
3078
3079 let backup_codes = BackupCodes::new(code_set);
3080
3081 let p = CryptoPolicy::minimum();
3083 let cred = Credential::new_password_only(&p, pw_good)
3084 .unwrap()
3085 .append_totp("totp".to_string(), totp)
3086 .update_backup_code(backup_codes)
3087 .unwrap();
3088
3089 account.primary = Some(cred);
3090
3091 let (async_tx, mut async_rx) = unbounded();
3092 let (audit_tx, mut audit_rx) = unbounded();
3093
3094 {
3099 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3100
3101 match session.validate_creds(
3102 &AuthCredential::Password(pw_bad.to_string()),
3103 ts,
3104 &async_tx,
3105 &audit_tx,
3106 &webauthn,
3107 &pw_badlist_cache,
3108 ) {
3109 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
3110 _ => panic!(),
3111 };
3112
3113 match audit_rx.try_recv() {
3114 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3115 _ => panic!("Oh no"),
3116 }
3117 }
3118 {
3120 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3121
3122 match session.validate_creds(
3123 &AuthCredential::BackupCode(backup_code_bad),
3124 ts,
3125 &async_tx,
3126 &audit_tx,
3127 &webauthn,
3128 &pw_badlist_cache,
3129 ) {
3130 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_BACKUPCODE_MSG),
3131 _ => panic!(),
3132 };
3133
3134 match audit_rx.try_recv() {
3135 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3136 _ => panic!("Oh no"),
3137 }
3138 }
3139 {
3142 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3143
3144 match session.validate_creds(
3145 &AuthCredential::BackupCode(backup_code_good.clone()),
3146 ts,
3147 &async_tx,
3148 &audit_tx,
3149 &webauthn,
3150 &pw_badlist_cache,
3151 ) {
3152 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3153 _ => panic!(),
3154 };
3155 match session.validate_creds(
3156 &AuthCredential::Password(pw_bad.to_string()),
3157 ts,
3158 &async_tx,
3159 &audit_tx,
3160 &webauthn,
3161 &pw_badlist_cache,
3162 ) {
3163 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
3164 _ => panic!(),
3165 };
3166
3167 match audit_rx.try_recv() {
3168 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3169 _ => panic!("Oh no"),
3170 }
3171 }
3172 match async_rx.blocking_recv() {
3174 Some(DelayedAction::BackupCodeRemoval(_)) => {}
3175 _ => panic!("Oh no"),
3176 }
3177
3178 {
3181 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3182
3183 match session.validate_creds(
3184 &AuthCredential::BackupCode(backup_code_good),
3185 ts,
3186 &async_tx,
3187 &audit_tx,
3188 &webauthn,
3189 &pw_badlist_cache,
3190 ) {
3191 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3192 _ => panic!(),
3193 };
3194 match session.validate_creds(
3195 &AuthCredential::Password(pw_good.to_string()),
3196 ts,
3197 &async_tx,
3198 &audit_tx,
3199 &webauthn,
3200 &pw_badlist_cache,
3201 ) {
3202 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3203 _ => panic!(),
3204 };
3205 }
3206 match async_rx.blocking_recv() {
3208 Some(DelayedAction::BackupCodeRemoval(_)) => {}
3209 _ => panic!("Oh no"),
3210 }
3211
3212 match async_rx.blocking_recv() {
3214 Some(DelayedAction::AuthSessionRecord(_)) => {}
3215 _ => panic!("Oh no"),
3216 }
3217
3218 {
3222 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3223
3224 match session.validate_creds(
3225 &AuthCredential::Totp(totp_good),
3226 ts,
3227 &async_tx,
3228 &audit_tx,
3229 &webauthn,
3230 &pw_badlist_cache,
3231 ) {
3232 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3233 _ => panic!(),
3234 };
3235 match session.validate_creds(
3236 &AuthCredential::Password(pw_good.to_string()),
3237 ts,
3238 &async_tx,
3239 &audit_tx,
3240 &webauthn,
3241 &pw_badlist_cache,
3242 ) {
3243 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3244 _ => panic!(),
3245 };
3246 }
3247
3248 match async_rx.blocking_recv() {
3250 Some(DelayedAction::AuthSessionRecord(_)) => {}
3251 _ => panic!("Oh no"),
3252 }
3253
3254 drop(async_tx);
3255 assert!(async_rx.blocking_recv().is_none());
3256 drop(audit_tx);
3257 assert!(audit_rx.blocking_recv().is_none());
3258 }
3259
3260 #[test]
3261 fn test_idm_authsession_multiple_totp_password_mech() {
3262 sketching::test_init();
3265 let webauthn = create_webauthn();
3266 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3268
3269 let ts = Duration::from_secs(12345);
3271
3272 let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
3274 let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
3275
3276 let totp_good_a = totp_a
3277 .do_totp_duration_from_epoch(&ts)
3278 .expect("failed to perform totp.");
3279
3280 let totp_good_b = totp_b
3281 .do_totp_duration_from_epoch(&ts)
3282 .expect("failed to perform totp.");
3283
3284 assert!(totp_good_a != totp_good_b);
3285
3286 let pw_good = "test_password";
3287
3288 let p = CryptoPolicy::minimum();
3289 let cred = Credential::new_password_only(&p, pw_good)
3290 .unwrap()
3291 .append_totp("totp_a".to_string(), totp_a)
3292 .append_totp("totp_b".to_string(), totp_b);
3293 account.primary = Some(cred);
3295
3296 let (async_tx, mut async_rx) = unbounded();
3297 let (audit_tx, mut audit_rx) = unbounded();
3298
3299 {
3301 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3302
3303 match session.validate_creds(
3304 &AuthCredential::Totp(totp_good_a),
3305 ts,
3306 &async_tx,
3307 &audit_tx,
3308 &webauthn,
3309 &pw_badlist_cache,
3310 ) {
3311 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3312 _ => panic!(),
3313 };
3314 match session.validate_creds(
3315 &AuthCredential::Password(pw_good.to_string()),
3316 ts,
3317 &async_tx,
3318 &audit_tx,
3319 &webauthn,
3320 &pw_badlist_cache,
3321 ) {
3322 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3323 _ => panic!(),
3324 };
3325
3326 match async_rx.blocking_recv() {
3327 Some(DelayedAction::AuthSessionRecord(_)) => {}
3328 _ => panic!("Oh no"),
3329 }
3330 }
3331
3332 {
3334 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3335
3336 match session.validate_creds(
3337 &AuthCredential::Totp(totp_good_b),
3338 ts,
3339 &async_tx,
3340 &audit_tx,
3341 &webauthn,
3342 &pw_badlist_cache,
3343 ) {
3344 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3345 _ => panic!(),
3346 };
3347 match session.validate_creds(
3348 &AuthCredential::Password(pw_good.to_string()),
3349 ts,
3350 &async_tx,
3351 &audit_tx,
3352 &webauthn,
3353 &pw_badlist_cache,
3354 ) {
3355 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3356 _ => panic!(),
3357 };
3358
3359 match async_rx.blocking_recv() {
3360 Some(DelayedAction::AuthSessionRecord(_)) => {}
3361 _ => panic!("Oh no"),
3362 }
3363 }
3364
3365 drop(async_tx);
3366 assert!(async_rx.blocking_recv().is_none());
3367 drop(audit_tx);
3368 assert!(audit_rx.blocking_recv().is_none());
3369 }
3370}