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 enum State {
1229 Expired,
1230 NoMatchingCred,
1231 Proceed(CredHandler),
1232 }
1233
1234 let state = if asd.account.is_within_valid_time(asd.ct) {
1235 let mut cred_handler = None;
1242
1243 match session.type_ {
1244 AuthType::Password
1245 | AuthType::GeneratedPassword
1246 | AuthType::PasswordBackupCode => {
1249 if let Some(primary) = asd.account.primary.as_ref() {
1250 if primary.uuid == cred_id {
1251 cred_handler = CredHandler::build_from_password_only(primary)
1252 }
1253 }
1254 }
1255 AuthType::PasswordTotp => {
1256 if let Some(primary) = asd.account.primary.as_ref() {
1257 if primary.uuid == cred_id {
1258 cred_handler = CredHandler::build_from_password_totp(primary)
1259 }
1260 }
1261 }
1262 AuthType::PasswordSecurityKey => {
1263 if let Some(primary) = asd.account.primary.as_ref() {
1264 if primary.uuid == cred_id {
1265 cred_handler =
1266 CredHandler::build_from_password_security_key(primary, asd.webauthn)
1267 }
1268 }
1269 }
1270 AuthType::Passkey => {
1271 let maybe_pk: Option<PasskeyV4> = asd
1273 .account
1274 .attested_passkeys
1275 .get(&cred_id)
1276 .map(|(_, apk)| apk.into())
1277 .or_else(|| asd.account.passkeys.get(&cred_id).map(|(_, pk)| pk.clone()));
1278
1279 if let Some(pk) = maybe_pk {
1280 if let Some(ch) =
1281 CredHandler::build_from_single_passkey(cred_id, pk, asd.webauthn)
1282 {
1283 debug_assert!(cred_handler.is_none());
1285 cred_handler = Some(ch);
1286 } else {
1287 security_critical!(
1288 "corrupt credentials, unable to start passkey credhandler"
1289 );
1290 }
1291 }
1292 }
1293 AuthType::AttestedPasskey => {
1294 if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
1295 if let Some(pk) = asd
1296 .account
1297 .attested_passkeys
1298 .get(&cred_id)
1299 .map(|(_, pk)| pk)
1300 {
1301 if let Some(ch) = CredHandler::build_from_single_attested_pk(
1302 cred_id,
1303 pk,
1304 att_ca_list,
1305 asd.webauthn,
1306 ) {
1307 debug_assert!(cred_handler.is_none());
1309 cred_handler = Some(ch);
1310 } else {
1311 security_critical!(
1312 "corrupt credentials, unable to start attested passkey credhandler"
1313 );
1314 }
1315 }
1316 }
1317 }
1318 AuthType::Anonymous => {}
1319 }
1320
1321 if let Some(cred_handler) = cred_handler {
1324 State::Proceed(cred_handler)
1325 } else {
1326 State::NoMatchingCred
1327 }
1328 } else {
1329 State::Expired
1330 };
1331
1332 let session_expiry = match session.state {
1333 SessionState::ExpiresAt(odt) => Some(odt),
1334 SessionState::NeverExpires => None,
1335 SessionState::RevokedAt(_) => {
1336 security_error!(
1337 "Invalid State - Should not be possible to trigger re-auth on revoked session."
1338 );
1339 return (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()));
1340 }
1341 };
1342
1343 match state {
1344 State::Proceed(handler) => {
1345 let allow = handler.next_auth_allowed();
1346 let auth_session = AuthSession {
1347 account: asd.account,
1348 account_policy: asd.account_policy,
1349 state: AuthSessionState::InProgress(handler),
1350 issue: asd.issue,
1351 intent: AuthIntent::Reauth {
1352 session_id,
1353 session_expiry,
1354 },
1355 source: asd.client_auth_info.source,
1356 key_object,
1357 };
1358
1359 let as_state = AuthState::Continue(allow);
1360 (Some(auth_session), as_state)
1361 }
1362 State::Expired => {
1363 security_info!("account expired");
1364 (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()))
1365 }
1366 State::NoMatchingCred => {
1367 security_error!("Unable to select a credential for authentication");
1368 (None, AuthState::Denied(BAD_CREDENTIALS.to_string()))
1369 }
1370 }
1371 }
1372
1373 pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
1376 match &self.state {
1377 AuthSessionState::InProgress(CredHandler::Password { cred_id, .. })
1378 | AuthSessionState::InProgress(CredHandler::PasswordTotp { cred_id, .. })
1379 | AuthSessionState::InProgress(CredHandler::PasswordBackupCode { cred_id, .. }) => {
1380 Ok(Some(*cred_id))
1381 }
1382 AuthSessionState::InProgress(CredHandler::Anonymous { .. })
1383 | AuthSessionState::InProgress(CredHandler::PasswordSecurityKey { .. })
1384 | AuthSessionState::InProgress(CredHandler::Passkey { .. })
1385 | AuthSessionState::InProgress(CredHandler::AttestedPasskey { .. }) => Ok(None),
1386
1387 AuthSessionState::Init(_) => {
1388 debug!(
1389 "Request for credential uuid invalid as auth session state not yet initialised"
1390 );
1391 Err(OperationError::AU0001InvalidState)
1392 }
1393 AuthSessionState::Success | AuthSessionState::Denied(_) => {
1394 debug!("Request for credential uuid invalid as auth session state has progressed");
1395 Err(OperationError::AU0001InvalidState)
1396 }
1397 }
1398 }
1399
1400 pub fn start_session(
1404 &mut self,
1405 mech: &AuthMech,
1406 ) -> Result<AuthState, OperationError> {
1409 let (next_state, response) = match &mut self.state {
1415 AuthSessionState::Success
1416 | AuthSessionState::Denied(_)
1417 | AuthSessionState::InProgress(_) => (
1418 None,
1419 Err(OperationError::InvalidAuthState(
1420 "session already finalised!".to_string(),
1421 )),
1422 ),
1423 AuthSessionState::Init(handlers) => {
1424 let mut allowed_handlers: Vec<_> = handlers
1426 .iter()
1427 .filter(|ch| ch.can_proceed(mech))
1428 .cloned()
1429 .collect();
1430
1431 if let Some(allowed_handler) = allowed_handlers.pop() {
1432 let allowed: Vec<_> = allowed_handler.next_auth_allowed();
1433
1434 if allowed.is_empty() {
1435 security_info!("Unable to negotiate credentials");
1436 (
1437 None,
1438 Err(OperationError::InvalidAuthState(
1439 "unable to negotiate credentials".to_string(),
1440 )),
1441 )
1442 } else {
1443 (
1444 Some(AuthSessionState::InProgress(allowed_handler)),
1445 Ok(AuthState::Continue(allowed)),
1446 )
1447 }
1448 } else {
1449 security_error!("Unable to select a credential for authentication");
1450 (
1451 Some(AuthSessionState::Denied(BAD_CREDENTIALS)),
1452 Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())),
1453 )
1454 }
1455 }
1456 };
1457
1458 if let Some(mut next_state) = next_state {
1459 std::mem::swap(&mut self.state, &mut next_state);
1460 };
1461
1462 response
1463 }
1464
1465 pub fn validate_creds(
1469 &mut self,
1470 cred: &AuthCredential,
1471 time: Duration,
1472 async_tx: &Sender<DelayedAction>,
1473 audit_tx: &Sender<AuditEvent>,
1474 webauthn: &Webauthn,
1475 pw_badlist: &HashSet<String>,
1476 ) -> Result<AuthState, OperationError> {
1477 let (next_state, response) = match &mut self.state {
1478 AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => {
1479 return Err(OperationError::InvalidAuthState(
1480 "session already finalised!".to_string(),
1481 ));
1482 }
1483 AuthSessionState::InProgress(ref mut handler) => {
1484 match handler.validate(
1485 cred,
1486 time,
1487 self.account.uuid,
1488 async_tx,
1489 webauthn,
1490 pw_badlist,
1491 ) {
1492 CredState::Success { auth_type, cred_id } => {
1493 let uat = self.issue_uat(auth_type, time, async_tx, cred_id)?;
1495
1496 let jwt = Jws::into_json(&uat).map_err(|e| {
1497 admin_error!(?e, "Failed to serialise into Jws");
1498 OperationError::AU0002JwsSerialisation
1499 })?;
1500
1501 let token = self.key_object.jws_es256_sign(&jwt, time).map_err(|e| {
1503 admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
1504 OperationError::AU0003JwsSignature
1505 })?;
1506
1507 (
1508 Some(AuthSessionState::Success),
1509 Ok(AuthState::Success(Box::new(token), self.issue)),
1510 )
1511 }
1512 CredState::Continue(allowed) => {
1513 security_info!(?allowed, "Request credential continuation");
1514 (None, Ok(AuthState::Continue(allowed.into_iter().collect())))
1515 }
1516 CredState::Denied(reason) => {
1517 if audit_tx
1518 .send(AuditEvent::AuthenticationDenied {
1519 source: self.source.clone().into(),
1520 spn: self.account.spn.clone(),
1521 uuid: self.account.uuid,
1522 time: OffsetDateTime::UNIX_EPOCH + time,
1523 })
1524 .is_err()
1525 {
1526 error!("Unable to submit audit event to queue");
1527 }
1528 security_info!(%reason, "Credentials denied");
1529 (
1530 Some(AuthSessionState::Denied(reason)),
1531 Ok(AuthState::Denied(reason.to_string())),
1532 )
1533 }
1534 }
1535 }
1536 };
1537
1538 if let Some(mut next_state) = next_state {
1539 std::mem::swap(&mut self.state, &mut next_state);
1540 };
1541
1542 response
1555 }
1556
1557 fn issue_uat(
1558 &mut self,
1559 auth_type: AuthType,
1560 time: Duration,
1561 async_tx: &Sender<DelayedAction>,
1562 cred_id: Uuid,
1563 ) -> Result<UserAuthToken, OperationError> {
1564 security_debug!("Successful cred handling");
1565 match self.intent {
1566 AuthIntent::InitialAuth { privileged } => {
1567 let session_id = Uuid::new_v4();
1568 let scope = match auth_type {
1571 AuthType::Anonymous => SessionScope::ReadOnly,
1572 AuthType::GeneratedPassword => SessionScope::ReadWrite,
1573 AuthType::Password
1574 | AuthType::PasswordTotp
1575 | AuthType::PasswordBackupCode
1576 | AuthType::PasswordSecurityKey
1577 | AuthType::Passkey
1578 | AuthType::AttestedPasskey => {
1579 if privileged {
1580 SessionScope::ReadWrite
1581 } else {
1582 SessionScope::PrivilegeCapable
1583 }
1584 }
1585 };
1586
1587 security_info!(
1588 "Issuing {:?} session ({:?}) {} for {} {}",
1589 self.issue,
1590 scope,
1591 session_id,
1592 self.account.spn,
1593 self.account.uuid
1594 );
1595
1596 let uat = self
1597 .account
1598 .to_userauthtoken(session_id, scope, time, &self.account_policy)
1599 .ok_or(OperationError::AU0004UserAuthTokenInvalid)?;
1600
1601 match auth_type {
1607 AuthType::Anonymous => {
1608 }
1610 AuthType::Password
1611 | AuthType::GeneratedPassword
1612 | AuthType::PasswordTotp
1613 | AuthType::PasswordBackupCode
1614 | AuthType::PasswordSecurityKey
1615 | AuthType::Passkey
1616 | AuthType::AttestedPasskey => {
1617 trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
1618 async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
1619 target_uuid: self.account.uuid,
1620 session_id,
1621 cred_id,
1622 label: "Auth Session".to_string(),
1623 expiry: uat.expiry,
1624 issued_at: uat.issued_at,
1625 issued_by: IdentityId::User(self.account.uuid),
1626 scope,
1627 type_: auth_type,
1628 }))
1629 .map_err(|e| {
1630 debug!(?e, "queue failure");
1631 admin_error!("unable to queue failing authentication as the session will not validate ... ");
1632 OperationError::AU0005DelayedProcessFailure
1633 })?;
1634 }
1635 };
1636
1637 Ok(uat)
1638 }
1639 AuthIntent::Reauth {
1640 session_id,
1641 session_expiry,
1642 } => {
1643 let scope = match auth_type {
1646 AuthType::Anonymous | AuthType::GeneratedPassword => {
1647 error!("AuthType used in Reauth is not valid for session re-issuance. Rejecting");
1648 return Err(OperationError::AU0006CredentialMayNotReauthenticate);
1649 }
1650 AuthType::Password
1651 | AuthType::PasswordTotp
1652 | AuthType::PasswordBackupCode
1653 | AuthType::PasswordSecurityKey
1654 | AuthType::Passkey
1655 | AuthType::AttestedPasskey => SessionScope::PrivilegeCapable,
1656 };
1657
1658 let uat = self
1659 .account
1660 .to_reissue_userauthtoken(
1661 session_id,
1662 session_expiry,
1663 scope,
1664 time,
1665 &self.account_policy,
1666 )
1667 .ok_or(OperationError::AU0007UserAuthTokenInvalid)?;
1668
1669 Ok(uat)
1670 }
1671 }
1672 }
1673
1674 pub fn end_session(&mut self, reason: &'static str) -> Result<AuthState, OperationError> {
1676 let mut next_state = AuthSessionState::Denied(reason);
1677 std::mem::swap(&mut self.state, &mut next_state);
1678 Ok(AuthState::Denied(reason.to_string()))
1679 }
1680
1681 fn valid_auth_mechs(&self) -> Vec<AuthMech> {
1682 match &self.state {
1683 AuthSessionState::Success
1684 | AuthSessionState::Denied(_)
1685 | AuthSessionState::InProgress(_) => Vec::with_capacity(0),
1686 AuthSessionState::Init(handlers) => {
1687 handlers.iter().map(|h| h.allows_mech()).collect()
1690 }
1691 }
1692 }
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697 use std::time::Duration;
1698
1699 use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
1700 use hashbrown::HashSet;
1701 use kanidm_proto::internal::{UatPurpose, UserAuthToken};
1702 use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
1703 use tokio::sync::mpsc::unbounded_channel as unbounded;
1704 use webauthn_authenticator_rs::softpasskey::SoftPasskey;
1705 use webauthn_authenticator_rs::WebauthnAuthenticator;
1706 use webauthn_rs::prelude::{RequestChallengeResponse, Webauthn};
1707
1708 use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
1709 use crate::credential::{BackupCodes, Credential};
1710 use crate::idm::account::Account;
1711 use crate::idm::accountpolicy::ResolvedAccountPolicy;
1712 use crate::idm::audit::AuditEvent;
1713 use crate::idm::authsession::{
1714 AuthSession, AuthSessionData, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG,
1715 BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG,
1716 };
1717 use crate::idm::delayed::DelayedAction;
1718 use crate::idm::AuthState;
1719 use crate::migration_data::{BUILTIN_ACCOUNT_ANONYMOUS, BUILTIN_ACCOUNT_TEST_PERSON};
1720 use crate::prelude::*;
1721 use crate::server::keys::KeyObjectInternal;
1722 use crate::utils::readable_password_from_random;
1723 use kanidm_lib_crypto::CryptoPolicy;
1724
1725 fn create_pw_badlist_cache() -> HashSet<String> {
1726 let mut s = HashSet::new();
1727 s.insert("list@no3IBTyqHu$bad".to_lowercase());
1728 s
1729 }
1730
1731 fn create_webauthn() -> webauthn_rs::Webauthn {
1732 webauthn_rs::WebauthnBuilder::new(
1733 "example.com",
1734 &url::Url::parse("https://idm.example.com").unwrap(),
1735 )
1736 .and_then(|builder| builder.build())
1737 .unwrap()
1738 }
1739
1740 #[test]
1741 fn test_idm_authsession_anonymous_auth_mech() {
1742 sketching::test_init();
1743
1744 let webauthn = create_webauthn();
1745
1746 let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
1747
1748 let asd = AuthSessionData {
1749 account: anon_account,
1750 account_policy: ResolvedAccountPolicy::default(),
1751 issue: AuthIssueSession::Token,
1752 webauthn: &webauthn,
1753 ct: duration_from_epoch_now(),
1754 client_auth_info: Source::Internal.into(),
1755 };
1756
1757 let key_object = KeyObjectInternal::new_test();
1758 let (session, state) = AuthSession::new(asd, false, key_object);
1759 if let AuthState::Choose(auth_mechs) = state {
1760 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
1761 } else {
1762 panic!("Invalid auth state")
1763 }
1764
1765 let state = session
1766 .expect("Missing auth session?")
1767 .start_session(&AuthMech::Anonymous)
1768 .expect("Failed to select anonymous mech.");
1769
1770 if let AuthState::Continue(auth_mechs) = state {
1771 assert!(auth_mechs
1772 .iter()
1773 .any(|x| matches!(x, AuthAllowed::Anonymous)));
1774 } else {
1775 panic!("Invalid auth state")
1776 }
1777 }
1778
1779 macro_rules! start_password_session {
1780 (
1781 $audit:expr,
1782 $account:expr,
1783 $webauthn:expr,
1784 $privileged:expr
1785 ) => {{
1786 let asd = AuthSessionData {
1787 account: $account.clone(),
1788 account_policy: ResolvedAccountPolicy::default(),
1789 issue: AuthIssueSession::Token,
1790 webauthn: $webauthn,
1791 ct: duration_from_epoch_now(),
1792 client_auth_info: Source::Internal.into(),
1793 };
1794 let key_object = KeyObjectInternal::new_test();
1795 let (session, state) = AuthSession::new(asd, $privileged, key_object);
1796 let mut session = session.unwrap();
1797
1798 if let AuthState::Choose(auth_mechs) = state {
1799 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Password)));
1800 } else {
1801 panic!();
1802 }
1803
1804 let state = session
1805 .start_session(&AuthMech::Password)
1806 .expect("Failed to select anonymous mech.");
1807
1808 if let AuthState::Continue(auth_mechs) = state {
1809 assert!(auth_mechs
1810 .iter()
1811 .any(|x| matches!(x, AuthAllowed::Password)));
1812 } else {
1813 panic!("Invalid auth state")
1814 }
1815
1816 (session, create_pw_badlist_cache())
1817 }};
1818 }
1819
1820 fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
1821 let webauthn = create_webauthn();
1822 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1824 let p = CryptoPolicy::minimum();
1826 let cred = Credential::new_password_only(&p, "test_password").unwrap();
1827 account.primary = Some(cred);
1828
1829 let (async_tx, mut async_rx) = unbounded();
1830 let (audit_tx, mut audit_rx) = unbounded();
1831
1832 let (mut session, pw_badlist_cache) =
1834 start_password_session!(&mut audit, account, &webauthn, false);
1835
1836 let attempt = AuthCredential::Password("bad_password".to_string());
1837 match session.validate_creds(
1838 &attempt,
1839 Duration::from_secs(0),
1840 &async_tx,
1841 &audit_tx,
1842 &webauthn,
1843 &pw_badlist_cache,
1844 ) {
1845 Ok(AuthState::Denied(_)) => {}
1846 _ => panic!(),
1847 };
1848
1849 match audit_rx.try_recv() {
1850 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1851 _ => panic!("Oh no"),
1852 }
1853
1854 let (mut session, pw_badlist_cache) =
1857 start_password_session!(&mut audit, account, &webauthn, privileged);
1858
1859 let attempt = AuthCredential::Password("test_password".to_string());
1860 let uat: UserAuthToken = match session.validate_creds(
1861 &attempt,
1862 Duration::from_secs(0),
1863 &async_tx,
1864 &audit_tx,
1865 &webauthn,
1866 &pw_badlist_cache,
1867 ) {
1868 Ok(AuthState::Success(jwsc, AuthIssueSession::Token)) => {
1869 let jws_verifier = JwsDangerReleaseWithoutVerify::default();
1870
1871 jws_verifier
1872 .verify(&*jwsc)
1873 .unwrap()
1874 .from_json::<UserAuthToken>()
1875 .unwrap()
1876 }
1877 _ => panic!(),
1878 };
1879
1880 match async_rx.blocking_recv() {
1881 Some(DelayedAction::AuthSessionRecord(_)) => {}
1882 _ => panic!("Oh no"),
1883 }
1884
1885 drop(async_tx);
1886 assert!(async_rx.blocking_recv().is_none());
1887 drop(audit_tx);
1888 assert!(audit_rx.blocking_recv().is_none());
1889
1890 uat
1891 }
1892
1893 #[test]
1894 fn test_idm_authsession_simple_password_mech() {
1895 sketching::test_init();
1896 let uat = start_session_simple_password_mech(false);
1897 match uat.purpose {
1898 UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1899 UatPurpose::ReadWrite { expiry } => {
1900 assert!(expiry.is_none())
1902 }
1903 }
1904 }
1905
1906 #[test]
1907 fn test_idm_authsession_simple_password_mech_priv_shortcut() {
1908 sketching::test_init();
1909 let uat = start_session_simple_password_mech(true);
1910 match uat.purpose {
1911 UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1912 UatPurpose::ReadWrite { expiry } => {
1913 assert!(expiry.is_some())
1915 }
1916 }
1917 }
1918
1919 #[test]
1920 fn test_idm_authsession_simple_password_badlist() {
1921 sketching::test_init();
1922 let webauthn = create_webauthn();
1923 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1925 let p = CryptoPolicy::minimum();
1927 let cred = Credential::new_password_only(&p, "list@no3IBTyqHu$bad").unwrap();
1928 account.primary = Some(cred);
1929
1930 let (async_tx, mut async_rx) = unbounded();
1931 let (audit_tx, mut audit_rx) = unbounded();
1932
1933 let (mut session, pw_badlist_cache) =
1935 start_password_session!(&mut audit, account, &webauthn, false);
1936
1937 let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
1938 match session.validate_creds(
1939 &attempt,
1940 Duration::from_secs(0),
1941 &async_tx,
1942 &audit_tx,
1943 &webauthn,
1944 &pw_badlist_cache,
1945 ) {
1946 Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
1947 _ => panic!(),
1948 };
1949
1950 match audit_rx.try_recv() {
1951 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1952 _ => panic!("Oh no"),
1953 }
1954
1955 drop(async_tx);
1956 assert!(async_rx.blocking_recv().is_none());
1957 drop(audit_tx);
1958 assert!(audit_rx.blocking_recv().is_none());
1959 }
1960
1961 fn start_password_totp_session(
1962 account: &Account,
1963 webauthn: &Webauthn,
1964 ) -> (AuthSession, HashSet<String>) {
1965 let asd = AuthSessionData {
1966 account: account.clone(),
1967 account_policy: ResolvedAccountPolicy::default(),
1968 issue: AuthIssueSession::Token,
1969 webauthn,
1970 ct: duration_from_epoch_now(),
1971 client_auth_info: Source::Internal.into(),
1972 };
1973 let key_object = KeyObjectInternal::new_test();
1974 let (session, state) = AuthSession::new(asd, false, key_object);
1975 let mut session = session.expect("Session was unable to be created.");
1976
1977 if let AuthState::Choose(auth_mechs) = state {
1978 assert!(auth_mechs
1979 .iter()
1980 .any(|x| matches!(x, AuthMech::PasswordTotp)))
1981 } else {
1982 panic!();
1983 }
1984
1985 let state = session
1986 .start_session(&AuthMech::PasswordTotp)
1987 .expect("Failed to select password totp mech.");
1988
1989 if let AuthState::Continue(auth_mechs) = state {
1990 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
1991 AuthAllowed::Totp => true,
1992 _ => acc,
1993 }));
1994 } else {
1995 panic!("Invalid auth state")
1996 }
1997
1998 (session, create_pw_badlist_cache())
1999 }
2000
2001 fn start_password_sk_session(
2002 account: &Account,
2003 webauthn: &Webauthn,
2004 ) -> (AuthSession, RequestChallengeResponse, HashSet<String>) {
2005 let asd = AuthSessionData {
2006 account: account.clone(),
2007 account_policy: ResolvedAccountPolicy::default(),
2008 issue: AuthIssueSession::Token,
2009 webauthn,
2010 ct: duration_from_epoch_now(),
2011 client_auth_info: Source::Internal.into(),
2012 };
2013 let key_object = KeyObjectInternal::new_test();
2014 let (session, state) = AuthSession::new(asd, false, key_object);
2015 let mut session = session.expect("Session was unable to be created.");
2016
2017 if let AuthState::Choose(auth_mechs) = state {
2018 assert!(auth_mechs
2019 .iter()
2020 .any(|x| matches!(x, AuthMech::PasswordSecurityKey)))
2021 } else {
2022 panic!();
2023 }
2024
2025 let state = session
2026 .start_session(&AuthMech::PasswordSecurityKey)
2027 .expect("Failed to select password security key mech.");
2028
2029 let mut rchal = None;
2030
2031 if let AuthState::Continue(auth_mechs) = state {
2032 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2033 AuthAllowed::SecurityKey(chal) => {
2034 rchal = Some(chal.clone());
2035 true
2036 }
2037 _ => acc,
2038 }));
2039 } else {
2040 panic!("Invalid auth state")
2041 }
2042
2043 (session, rchal.unwrap(), create_pw_badlist_cache())
2044 }
2045
2046 fn start_password_bc_session(
2047 account: &Account,
2048 webauthn: &Webauthn,
2049 ) -> (AuthSession, HashSet<String>) {
2050 let asd = AuthSessionData {
2051 account: account.clone(),
2052 account_policy: ResolvedAccountPolicy::default(),
2053 issue: AuthIssueSession::Token,
2054 webauthn,
2055 ct: duration_from_epoch_now(),
2056 client_auth_info: Source::Internal.into(),
2057 };
2058 let key_object = KeyObjectInternal::new_test();
2059 let (session, state) = AuthSession::new(asd, false, key_object);
2060 let mut session = session.expect("Session was unable to be created.");
2061
2062 if let AuthState::Choose(auth_mechs) = state {
2063 assert!(auth_mechs
2064 .iter()
2065 .any(|x| matches!(x, AuthMech::PasswordBackupCode)))
2066 } else {
2067 panic!();
2068 }
2069
2070 let state = session
2071 .start_session(&AuthMech::PasswordBackupCode)
2072 .expect("Failed to select password backup code mech.");
2073
2074 if let AuthState::Continue(auth_mechs) = state {
2075 assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2076 AuthAllowed::BackupCode => true,
2077 _ => acc,
2078 }));
2079 } else {
2080 panic!("Invalid auth state")
2081 }
2082
2083 (session, create_pw_badlist_cache())
2084 }
2085
2086 #[test]
2087 fn test_idm_authsession_totp_password_mech() {
2088 sketching::test_init();
2089 let webauthn = create_webauthn();
2090 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2092
2093 let ts = Duration::from_secs(12345);
2095
2096 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2098
2099 let totp_good = totp
2100 .do_totp_duration_from_epoch(&ts)
2101 .expect("failed to perform totp.");
2102 let totp_bad = totp
2103 .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2104 .expect("failed to perform totp.");
2105 assert!(totp_bad != totp_good);
2106
2107 let pw_good = "test_password";
2108 let pw_bad = "bad_password";
2109
2110 let p = CryptoPolicy::minimum();
2111 let cred = Credential::new_password_only(&p, pw_good)
2112 .unwrap()
2113 .append_totp("totp".to_string(), totp);
2114 account.primary = Some(cred);
2116
2117 let (async_tx, mut async_rx) = unbounded();
2118 let (audit_tx, mut audit_rx) = unbounded();
2119
2120 {
2124 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2125
2126 match session.validate_creds(
2127 &AuthCredential::Anonymous,
2128 ts,
2129 &async_tx,
2130 &audit_tx,
2131 &webauthn,
2132 &pw_badlist_cache,
2133 ) {
2134 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2135 _ => panic!(),
2136 };
2137
2138 match audit_rx.try_recv() {
2139 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2140 _ => panic!("Oh no"),
2141 }
2142 }
2143
2144 {
2148 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2149
2150 match session.validate_creds(
2151 &AuthCredential::Password(pw_bad.to_string()),
2152 ts,
2153 &async_tx,
2154 &audit_tx,
2155 &webauthn,
2156 &pw_badlist_cache,
2157 ) {
2158 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2159 _ => panic!(),
2160 };
2161
2162 match audit_rx.try_recv() {
2163 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2164 _ => panic!("Oh no"),
2165 }
2166 }
2167 {
2169 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2170
2171 match session.validate_creds(
2172 &AuthCredential::Totp(totp_bad),
2173 ts,
2174 &async_tx,
2175 &audit_tx,
2176 &webauthn,
2177 &pw_badlist_cache,
2178 ) {
2179 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2180 _ => panic!(),
2181 };
2182
2183 match audit_rx.try_recv() {
2184 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2185 _ => panic!("Oh no"),
2186 }
2187 }
2188
2189 {
2192 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2193
2194 match session.validate_creds(
2195 &AuthCredential::Totp(totp_good),
2196 ts,
2197 &async_tx,
2198 &audit_tx,
2199 &webauthn,
2200 &pw_badlist_cache,
2201 ) {
2202 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2203 _ => panic!(),
2204 };
2205 match session.validate_creds(
2206 &AuthCredential::Password(pw_bad.to_string()),
2207 ts,
2208 &async_tx,
2209 &audit_tx,
2210 &webauthn,
2211 &pw_badlist_cache,
2212 ) {
2213 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2214 _ => panic!(),
2215 };
2216
2217 match audit_rx.try_recv() {
2218 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2219 _ => panic!("Oh no"),
2220 }
2221 }
2222
2223 {
2226 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2227
2228 match session.validate_creds(
2229 &AuthCredential::Totp(totp_good),
2230 ts,
2231 &async_tx,
2232 &audit_tx,
2233 &webauthn,
2234 &pw_badlist_cache,
2235 ) {
2236 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2237 _ => panic!(),
2238 };
2239 match session.validate_creds(
2240 &AuthCredential::Password(pw_good.to_string()),
2241 ts,
2242 &async_tx,
2243 &audit_tx,
2244 &webauthn,
2245 &pw_badlist_cache,
2246 ) {
2247 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2248 _ => panic!(),
2249 };
2250
2251 match async_rx.blocking_recv() {
2252 Some(DelayedAction::AuthSessionRecord(_)) => {}
2253 _ => panic!("Oh no"),
2254 }
2255 }
2256
2257 drop(async_tx);
2258 assert!(async_rx.blocking_recv().is_none());
2259 drop(audit_tx);
2260 assert!(audit_rx.blocking_recv().is_none());
2261 }
2262
2263 #[test]
2264 fn test_idm_authsession_password_mfa_badlist() {
2265 sketching::test_init();
2266 let webauthn = create_webauthn();
2267 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2269
2270 let ts = Duration::from_secs(12345);
2272
2273 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2275
2276 let totp_good = totp
2277 .do_totp_duration_from_epoch(&ts)
2278 .expect("failed to perform totp.");
2279
2280 let pw_badlist = "list@no3IBTyqHu$bad";
2281
2282 let p = CryptoPolicy::minimum();
2283 let cred = Credential::new_password_only(&p, pw_badlist)
2284 .unwrap()
2285 .append_totp("totp".to_string(), totp);
2286 account.primary = Some(cred);
2288
2289 let (async_tx, mut async_rx) = unbounded();
2290 let (audit_tx, mut audit_rx) = unbounded();
2291
2292 {
2299 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2300
2301 match session.validate_creds(
2302 &AuthCredential::Totp(totp_good),
2303 ts,
2304 &async_tx,
2305 &audit_tx,
2306 &webauthn,
2307 &pw_badlist_cache,
2308 ) {
2309 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2310 _ => panic!(),
2311 };
2312 match session.validate_creds(
2313 &AuthCredential::Password(pw_badlist.to_string()),
2314 ts,
2315 &async_tx,
2316 &audit_tx,
2317 &webauthn,
2318 &pw_badlist_cache,
2319 ) {
2320 Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
2321 _ => panic!(),
2322 };
2323
2324 match audit_rx.try_recv() {
2325 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2326 _ => panic!("Oh no"),
2327 }
2328 }
2329
2330 drop(async_tx);
2331 assert!(async_rx.blocking_recv().is_none());
2332 drop(audit_tx);
2333 assert!(audit_rx.blocking_recv().is_none());
2334 }
2335
2336 macro_rules! start_webauthn_only_session {
2337 (
2338 $audit:expr,
2339 $account:expr,
2340 $webauthn:expr
2341 ) => {{
2342 let asd = AuthSessionData {
2343 account: $account.clone(),
2344 account_policy: ResolvedAccountPolicy::default(),
2345 issue: AuthIssueSession::Token,
2346 webauthn: $webauthn,
2347 ct: duration_from_epoch_now(),
2348 client_auth_info: Source::Internal.into(),
2349 };
2350 let key_object = KeyObjectInternal::new_test();
2351 let (session, state) = AuthSession::new(asd, false, key_object);
2352 let mut session = session.unwrap();
2353
2354 if let AuthState::Choose(auth_mechs) = state {
2355 assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Passkey)));
2356 } else {
2357 panic!();
2358 }
2359
2360 let state = session
2361 .start_session(&AuthMech::Passkey)
2362 .expect("Failed to select Passkey mech.");
2363
2364 let wan_chal = if let AuthState::Continue(auth_mechs) = state {
2365 assert_eq!(auth_mechs.len(), 1);
2366 auth_mechs
2367 .into_iter()
2368 .fold(None, |_acc, x| match x {
2369 AuthAllowed::Passkey(chal) => Some(chal),
2370 _ => None,
2371 })
2372 .expect("No securitykey challenge found.")
2373 } else {
2374 panic!();
2375 };
2376
2377 (session, wan_chal)
2378 }};
2379 }
2380
2381 fn setup_webauthn_passkey(
2382 name: &str,
2383 ) -> (
2384 webauthn_rs::prelude::Webauthn,
2385 webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2386 webauthn_rs::prelude::Passkey,
2387 ) {
2388 let webauthn = create_webauthn();
2389 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2391
2392 let uuid = Uuid::new_v4();
2393
2394 let (chal, reg_state) = webauthn
2395 .start_passkey_registration(uuid, name, name, None)
2396 .expect("Failed to setup passkey rego challenge");
2397
2398 let r = wa
2399 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2400 .expect("Failed to create soft passkey");
2401
2402 let wan_cred = webauthn
2403 .finish_passkey_registration(&r, ®_state)
2404 .expect("Failed to register soft token");
2405
2406 (webauthn, wa, wan_cred)
2407 }
2408
2409 fn setup_webauthn_securitykey(
2410 name: &str,
2411 ) -> (
2412 webauthn_rs::prelude::Webauthn,
2413 webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2414 webauthn_rs::prelude::SecurityKey,
2415 ) {
2416 let webauthn = create_webauthn();
2417 let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2419
2420 let uuid = Uuid::new_v4();
2421
2422 let (chal, reg_state) = webauthn
2423 .start_securitykey_registration(uuid, name, name, None, None, None)
2424 .expect("Failed to setup passkey rego challenge");
2425
2426 let r = wa
2427 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2428 .expect("Failed to create soft securitykey");
2429
2430 let wan_cred = webauthn
2431 .finish_securitykey_registration(&r, ®_state)
2432 .expect("Failed to register soft token");
2433
2434 (webauthn, wa, wan_cred)
2435 }
2436
2437 #[test]
2438 fn test_idm_authsession_webauthn_only_mech() {
2439 sketching::test_init();
2440 let (async_tx, mut async_rx) = unbounded();
2441 let (audit_tx, mut audit_rx) = unbounded();
2442 let ts = duration_from_epoch_now();
2443 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2445
2446 let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
2447
2448 account.passkeys = btreemap![(Uuid::new_v4(), ("soft".to_string(), wan_cred))];
2450
2451 {
2455 let (mut session, _inv_chal) =
2456 start_webauthn_only_session!(&mut audit, account, &webauthn);
2457
2458 match session.validate_creds(
2459 &AuthCredential::Anonymous,
2460 ts,
2461 &async_tx,
2462 &audit_tx,
2463 &webauthn,
2464 &Default::default(),
2465 ) {
2466 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2467 _ => panic!(),
2468 };
2469
2470 match audit_rx.try_recv() {
2471 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2472 _ => panic!("Oh no"),
2473 }
2474 }
2475
2476 {
2478 let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2479
2480 let resp = wa
2481 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2482 .map(Box::new)
2483 .expect("failed to use softtoken to authenticate");
2484
2485 match session.validate_creds(
2486 &AuthCredential::Passkey(resp),
2487 ts,
2488 &async_tx,
2489 &audit_tx,
2490 &webauthn,
2491 &Default::default(),
2492 ) {
2493 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2494 _ => panic!(),
2495 };
2496
2497 match async_rx.blocking_recv() {
2499 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2500 _ => panic!("Oh no"),
2501 }
2502 match async_rx.blocking_recv() {
2503 Some(DelayedAction::AuthSessionRecord(_)) => {}
2504 _ => panic!("Oh no"),
2505 }
2506 }
2507
2508 {
2510 let (_session, inv_chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2511 let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2512
2513 let resp = wa
2514 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2516 .map(Box::new)
2517 .expect("failed to use softtoken to authenticate");
2518
2519 match session.validate_creds(
2520 &AuthCredential::Passkey(resp),
2521 ts,
2522 &async_tx,
2523 &audit_tx,
2524 &webauthn,
2525 &Default::default(),
2526 ) {
2527 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2528 _ => panic!(),
2529 };
2530
2531 match audit_rx.try_recv() {
2532 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2533 _ => panic!("Oh no"),
2534 }
2535 }
2536
2537 {
2539 let mut inv_wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2540 let (chal, reg_state) = webauthn
2541 .start_passkey_registration(account.uuid, &account.name, &account.displayname, None)
2542 .expect("Failed to setup webauthn rego challenge");
2543
2544 let r = inv_wa
2545 .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2546 .expect("Failed to create soft token");
2547
2548 let inv_cred = webauthn
2549 .finish_passkey_registration(&r, ®_state)
2550 .expect("Failed to register soft token");
2551
2552 let (chal, _auth_state) = webauthn
2554 .start_passkey_authentication(&vec![inv_cred])
2555 .expect("Failed to generate challenge for in inv softtoken");
2556
2557 let resp = inv_wa
2559 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2560 .map(Box::new)
2561 .expect("Failed to use softtoken for response.");
2562
2563 let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2564 match session.validate_creds(
2568 &AuthCredential::Passkey(resp),
2569 ts,
2570 &async_tx,
2571 &audit_tx,
2572 &webauthn,
2573 &Default::default(),
2574 ) {
2575 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2576 _ => panic!(),
2577 };
2578
2579 match audit_rx.try_recv() {
2580 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2581 _ => panic!("Oh no"),
2582 }
2583 }
2584
2585 drop(async_tx);
2586 assert!(async_rx.blocking_recv().is_none());
2587 drop(audit_tx);
2588 assert!(audit_rx.blocking_recv().is_none());
2589 }
2590
2591 #[test]
2592 fn test_idm_authsession_webauthn_password_mech() {
2593 sketching::test_init();
2594 let (async_tx, mut async_rx) = unbounded();
2595 let (audit_tx, mut audit_rx) = unbounded();
2596 let ts = duration_from_epoch_now();
2597 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2599
2600 let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2601 let pw_good = "test_password";
2602 let pw_bad = "bad_password";
2603
2604 let p = CryptoPolicy::minimum();
2606 let cred = Credential::new_password_only(&p, pw_good)
2607 .unwrap()
2608 .append_securitykey("soft".to_string(), wan_cred)
2609 .unwrap();
2610
2611 account.primary = Some(cred);
2612
2613 {
2615 let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2616
2617 match session.validate_creds(
2618 &AuthCredential::Password(pw_bad.to_string()),
2619 ts,
2620 &async_tx,
2621 &audit_tx,
2622 &webauthn,
2623 &pw_badlist_cache,
2624 ) {
2625 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2626 _ => panic!(),
2627 };
2628
2629 match audit_rx.try_recv() {
2630 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2631 _ => panic!("Oh no"),
2632 }
2633 }
2634
2635 {
2637 let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2638
2639 match session.validate_creds(
2640 &AuthCredential::Totp(0),
2641 ts,
2642 &async_tx,
2643 &audit_tx,
2644 &webauthn,
2645 &pw_badlist_cache,
2646 ) {
2647 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2648 _ => panic!(),
2649 };
2650
2651 match audit_rx.try_recv() {
2652 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2653 _ => panic!("Oh no"),
2654 }
2655 }
2656
2657 {
2661 let (_session, inv_chal, pw_badlist_cache) =
2662 start_password_sk_session(&account, &webauthn);
2663 let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2664
2665 let resp = wa
2666 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2668 .map(Box::new)
2669 .expect("failed to use softtoken to authenticate");
2670
2671 match session.validate_creds(
2672 &AuthCredential::SecurityKey(resp),
2673 ts,
2674 &async_tx,
2675 &audit_tx,
2676 &webauthn,
2677 &pw_badlist_cache,
2678 ) {
2679 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2680 _ => panic!(),
2681 };
2682
2683 match audit_rx.try_recv() {
2684 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2685 _ => panic!("Oh no"),
2686 }
2687 }
2688
2689 {
2691 let (mut session, chal, pw_badlist_cache) =
2692 start_password_sk_session(&account, &webauthn);
2693
2694 let resp = wa
2695 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2696 .map(Box::new)
2697 .expect("failed to use softtoken to authenticate");
2698
2699 match session.validate_creds(
2700 &AuthCredential::SecurityKey(resp),
2701 ts,
2702 &async_tx,
2703 &audit_tx,
2704 &webauthn,
2705 &pw_badlist_cache,
2706 ) {
2707 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2708 _ => panic!(),
2709 };
2710 match session.validate_creds(
2711 &AuthCredential::Password(pw_bad.to_string()),
2712 ts,
2713 &async_tx,
2714 &audit_tx,
2715 &webauthn,
2716 &pw_badlist_cache,
2717 ) {
2718 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2719 _ => panic!(),
2720 };
2721
2722 match audit_rx.try_recv() {
2723 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2724 _ => panic!("Oh no"),
2725 }
2726
2727 match async_rx.blocking_recv() {
2729 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2730 _ => panic!("Oh no"),
2731 }
2732 }
2733
2734 {
2736 let (mut session, chal, pw_badlist_cache) =
2737 start_password_sk_session(&account, &webauthn);
2738
2739 let resp = wa
2740 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2741 .map(Box::new)
2742 .expect("failed to use softtoken to authenticate");
2743
2744 match session.validate_creds(
2745 &AuthCredential::SecurityKey(resp),
2746 ts,
2747 &async_tx,
2748 &audit_tx,
2749 &webauthn,
2750 &pw_badlist_cache,
2751 ) {
2752 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2753 _ => panic!(),
2754 };
2755 match session.validate_creds(
2756 &AuthCredential::Password(pw_good.to_string()),
2757 ts,
2758 &async_tx,
2759 &audit_tx,
2760 &webauthn,
2761 &pw_badlist_cache,
2762 ) {
2763 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2764 _ => panic!(),
2765 };
2766
2767 match async_rx.blocking_recv() {
2769 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2770 _ => panic!("Oh no"),
2771 }
2772 match async_rx.blocking_recv() {
2773 Some(DelayedAction::AuthSessionRecord(_)) => {}
2774 _ => panic!("Oh no"),
2775 }
2776 }
2777
2778 drop(async_tx);
2779 assert!(async_rx.blocking_recv().is_none());
2780 drop(audit_tx);
2781 assert!(audit_rx.blocking_recv().is_none());
2782 }
2783
2784 #[test]
2785 fn test_idm_authsession_webauthn_password_totp_mech() {
2786 sketching::test_init();
2787 let (async_tx, mut async_rx) = unbounded();
2788 let (audit_tx, mut audit_rx) = unbounded();
2789 let ts = duration_from_epoch_now();
2790 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2792
2793 let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2794
2795 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2796 let totp_good = totp
2797 .do_totp_duration_from_epoch(&ts)
2798 .expect("failed to perform totp.");
2799 let totp_bad = totp
2800 .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2801 .expect("failed to perform totp.");
2802 assert!(totp_bad != totp_good);
2803
2804 let pw_good = "test_password";
2805 let pw_bad = "bad_password";
2806
2807 let p = CryptoPolicy::minimum();
2809 let cred = Credential::new_password_only(&p, pw_good)
2810 .unwrap()
2811 .append_securitykey("soft".to_string(), wan_cred)
2812 .unwrap()
2813 .append_totp("totp".to_string(), totp);
2814
2815 account.primary = Some(cred);
2816
2817 {
2819 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2820
2821 match session.validate_creds(
2822 &AuthCredential::Password(pw_bad.to_string()),
2823 ts,
2824 &async_tx,
2825 &audit_tx,
2826 &webauthn,
2827 &pw_badlist_cache,
2828 ) {
2829 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2830 _ => panic!(),
2831 };
2832
2833 match audit_rx.try_recv() {
2834 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2835 _ => panic!("Oh no"),
2836 }
2837 }
2838
2839 {
2841 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2842
2843 match session.validate_creds(
2844 &AuthCredential::Totp(totp_bad),
2845 ts,
2846 &async_tx,
2847 &audit_tx,
2848 &webauthn,
2849 &pw_badlist_cache,
2850 ) {
2851 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2852 _ => panic!(),
2853 };
2854
2855 match audit_rx.try_recv() {
2856 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2857 _ => panic!("Oh no"),
2858 }
2859 }
2860
2861 {
2863 let (_session, inv_chal, pw_badlist_cache) =
2864 start_password_sk_session(&account, &webauthn);
2865 let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2866
2867 let resp = wa
2868 .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2870 .map(Box::new)
2871 .expect("failed to use softtoken to authenticate");
2872
2873 match session.validate_creds(
2874 &AuthCredential::SecurityKey(resp),
2875 ts,
2876 &async_tx,
2877 &audit_tx,
2878 &webauthn,
2879 &pw_badlist_cache,
2880 ) {
2881 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2882 _ => panic!(),
2883 };
2884
2885 match audit_rx.try_recv() {
2886 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2887 _ => panic!("Oh no"),
2888 }
2889 }
2890
2891 {
2893 let (mut session, chal, pw_badlist_cache) =
2894 start_password_sk_session(&account, &webauthn);
2895
2896 let resp = wa
2897 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2898 .map(Box::new)
2899 .expect("failed to use softtoken to authenticate");
2900
2901 match session.validate_creds(
2902 &AuthCredential::SecurityKey(resp),
2903 ts,
2904 &async_tx,
2905 &audit_tx,
2906 &webauthn,
2907 &pw_badlist_cache,
2908 ) {
2909 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2910 _ => panic!(),
2911 };
2912 match session.validate_creds(
2913 &AuthCredential::Password(pw_bad.to_string()),
2914 ts,
2915 &async_tx,
2916 &audit_tx,
2917 &webauthn,
2918 &pw_badlist_cache,
2919 ) {
2920 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2921 _ => panic!(),
2922 };
2923
2924 match audit_rx.try_recv() {
2925 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2926 _ => panic!("Oh no"),
2927 }
2928
2929 match async_rx.blocking_recv() {
2931 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2932 _ => panic!("Oh no"),
2933 }
2934 }
2935
2936 {
2938 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2939
2940 match session.validate_creds(
2941 &AuthCredential::Totp(totp_good),
2942 ts,
2943 &async_tx,
2944 &audit_tx,
2945 &webauthn,
2946 &pw_badlist_cache,
2947 ) {
2948 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2949 _ => panic!(),
2950 };
2951 match session.validate_creds(
2952 &AuthCredential::Password(pw_bad.to_string()),
2953 ts,
2954 &async_tx,
2955 &audit_tx,
2956 &webauthn,
2957 &pw_badlist_cache,
2958 ) {
2959 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2960 _ => panic!(),
2961 };
2962
2963 match audit_rx.try_recv() {
2964 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2965 _ => panic!("Oh no"),
2966 }
2967 }
2968
2969 {
2971 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2972
2973 match session.validate_creds(
2974 &AuthCredential::Totp(totp_good),
2975 ts,
2976 &async_tx,
2977 &audit_tx,
2978 &webauthn,
2979 &pw_badlist_cache,
2980 ) {
2981 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2982 _ => panic!(),
2983 };
2984 match session.validate_creds(
2985 &AuthCredential::Password(pw_good.to_string()),
2986 ts,
2987 &async_tx,
2988 &audit_tx,
2989 &webauthn,
2990 &pw_badlist_cache,
2991 ) {
2992 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2993 _ => panic!(),
2994 };
2995
2996 match async_rx.blocking_recv() {
2997 Some(DelayedAction::AuthSessionRecord(_)) => {}
2998 _ => panic!("Oh no"),
2999 }
3000 }
3001
3002 {
3004 let (mut session, chal, pw_badlist_cache) =
3005 start_password_sk_session(&account, &webauthn);
3006
3007 let resp = wa
3008 .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
3009 .map(Box::new)
3010 .expect("failed to use softtoken to authenticate");
3011
3012 match session.validate_creds(
3013 &AuthCredential::SecurityKey(resp),
3014 ts,
3015 &async_tx,
3016 &audit_tx,
3017 &webauthn,
3018 &pw_badlist_cache,
3019 ) {
3020 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3021 _ => panic!(),
3022 };
3023 match session.validate_creds(
3024 &AuthCredential::Password(pw_good.to_string()),
3025 ts,
3026 &async_tx,
3027 &audit_tx,
3028 &webauthn,
3029 &pw_badlist_cache,
3030 ) {
3031 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3032 _ => panic!(),
3033 };
3034
3035 match async_rx.blocking_recv() {
3037 Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
3038 _ => panic!("Oh no"),
3039 }
3040 match async_rx.blocking_recv() {
3041 Some(DelayedAction::AuthSessionRecord(_)) => {}
3042 _ => panic!("Oh no"),
3043 }
3044 }
3045
3046 drop(async_tx);
3047 assert!(async_rx.blocking_recv().is_none());
3048 drop(audit_tx);
3049 assert!(audit_rx.blocking_recv().is_none());
3050 }
3051
3052 #[test]
3053 fn test_idm_authsession_backup_code_mech() {
3054 sketching::test_init();
3055 let webauthn = create_webauthn();
3056 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3058
3059 let ts = Duration::from_secs(12345);
3061
3062 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
3064
3065 let totp_good = totp
3066 .do_totp_duration_from_epoch(&ts)
3067 .expect("failed to perform totp.");
3068
3069 let pw_good = "test_password";
3070 let pw_bad = "bad_password";
3071
3072 let backup_code_good = readable_password_from_random();
3073 let backup_code_bad = readable_password_from_random();
3074 assert!(backup_code_bad != backup_code_good);
3075 let mut code_set = HashSet::new();
3076 code_set.insert(backup_code_good.clone());
3077
3078 let backup_codes = BackupCodes::new(code_set);
3079
3080 let p = CryptoPolicy::minimum();
3082 let cred = Credential::new_password_only(&p, pw_good)
3083 .unwrap()
3084 .append_totp("totp".to_string(), totp)
3085 .update_backup_code(backup_codes)
3086 .unwrap();
3087
3088 account.primary = Some(cred);
3089
3090 let (async_tx, mut async_rx) = unbounded();
3091 let (audit_tx, mut audit_rx) = unbounded();
3092
3093 {
3098 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3099
3100 match session.validate_creds(
3101 &AuthCredential::Password(pw_bad.to_string()),
3102 ts,
3103 &async_tx,
3104 &audit_tx,
3105 &webauthn,
3106 &pw_badlist_cache,
3107 ) {
3108 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
3109 _ => panic!(),
3110 };
3111
3112 match audit_rx.try_recv() {
3113 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3114 _ => panic!("Oh no"),
3115 }
3116 }
3117 {
3119 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3120
3121 match session.validate_creds(
3122 &AuthCredential::BackupCode(backup_code_bad),
3123 ts,
3124 &async_tx,
3125 &audit_tx,
3126 &webauthn,
3127 &pw_badlist_cache,
3128 ) {
3129 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_BACKUPCODE_MSG),
3130 _ => panic!(),
3131 };
3132
3133 match audit_rx.try_recv() {
3134 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3135 _ => panic!("Oh no"),
3136 }
3137 }
3138 {
3141 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3142
3143 match session.validate_creds(
3144 &AuthCredential::BackupCode(backup_code_good.clone()),
3145 ts,
3146 &async_tx,
3147 &audit_tx,
3148 &webauthn,
3149 &pw_badlist_cache,
3150 ) {
3151 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3152 _ => panic!(),
3153 };
3154 match session.validate_creds(
3155 &AuthCredential::Password(pw_bad.to_string()),
3156 ts,
3157 &async_tx,
3158 &audit_tx,
3159 &webauthn,
3160 &pw_badlist_cache,
3161 ) {
3162 Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
3163 _ => panic!(),
3164 };
3165
3166 match audit_rx.try_recv() {
3167 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3168 _ => panic!("Oh no"),
3169 }
3170 }
3171 match async_rx.blocking_recv() {
3173 Some(DelayedAction::BackupCodeRemoval(_)) => {}
3174 _ => panic!("Oh no"),
3175 }
3176
3177 {
3180 let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3181
3182 match session.validate_creds(
3183 &AuthCredential::BackupCode(backup_code_good),
3184 ts,
3185 &async_tx,
3186 &audit_tx,
3187 &webauthn,
3188 &pw_badlist_cache,
3189 ) {
3190 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3191 _ => panic!(),
3192 };
3193 match session.validate_creds(
3194 &AuthCredential::Password(pw_good.to_string()),
3195 ts,
3196 &async_tx,
3197 &audit_tx,
3198 &webauthn,
3199 &pw_badlist_cache,
3200 ) {
3201 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3202 _ => panic!(),
3203 };
3204 }
3205 match async_rx.blocking_recv() {
3207 Some(DelayedAction::BackupCodeRemoval(_)) => {}
3208 _ => panic!("Oh no"),
3209 }
3210
3211 match async_rx.blocking_recv() {
3213 Some(DelayedAction::AuthSessionRecord(_)) => {}
3214 _ => panic!("Oh no"),
3215 }
3216
3217 {
3221 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3222
3223 match session.validate_creds(
3224 &AuthCredential::Totp(totp_good),
3225 ts,
3226 &async_tx,
3227 &audit_tx,
3228 &webauthn,
3229 &pw_badlist_cache,
3230 ) {
3231 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3232 _ => panic!(),
3233 };
3234 match session.validate_creds(
3235 &AuthCredential::Password(pw_good.to_string()),
3236 ts,
3237 &async_tx,
3238 &audit_tx,
3239 &webauthn,
3240 &pw_badlist_cache,
3241 ) {
3242 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3243 _ => panic!(),
3244 };
3245 }
3246
3247 match async_rx.blocking_recv() {
3249 Some(DelayedAction::AuthSessionRecord(_)) => {}
3250 _ => panic!("Oh no"),
3251 }
3252
3253 drop(async_tx);
3254 assert!(async_rx.blocking_recv().is_none());
3255 drop(audit_tx);
3256 assert!(audit_rx.blocking_recv().is_none());
3257 }
3258
3259 #[test]
3260 fn test_idm_authsession_multiple_totp_password_mech() {
3261 sketching::test_init();
3264 let webauthn = create_webauthn();
3265 let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3267
3268 let ts = Duration::from_secs(12345);
3270
3271 let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
3273 let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
3274
3275 let totp_good_a = totp_a
3276 .do_totp_duration_from_epoch(&ts)
3277 .expect("failed to perform totp.");
3278
3279 let totp_good_b = totp_b
3280 .do_totp_duration_from_epoch(&ts)
3281 .expect("failed to perform totp.");
3282
3283 assert!(totp_good_a != totp_good_b);
3284
3285 let pw_good = "test_password";
3286
3287 let p = CryptoPolicy::minimum();
3288 let cred = Credential::new_password_only(&p, pw_good)
3289 .unwrap()
3290 .append_totp("totp_a".to_string(), totp_a)
3291 .append_totp("totp_b".to_string(), totp_b);
3292 account.primary = Some(cred);
3294
3295 let (async_tx, mut async_rx) = unbounded();
3296 let (audit_tx, mut audit_rx) = unbounded();
3297
3298 {
3300 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3301
3302 match session.validate_creds(
3303 &AuthCredential::Totp(totp_good_a),
3304 ts,
3305 &async_tx,
3306 &audit_tx,
3307 &webauthn,
3308 &pw_badlist_cache,
3309 ) {
3310 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3311 _ => panic!(),
3312 };
3313 match session.validate_creds(
3314 &AuthCredential::Password(pw_good.to_string()),
3315 ts,
3316 &async_tx,
3317 &audit_tx,
3318 &webauthn,
3319 &pw_badlist_cache,
3320 ) {
3321 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3322 _ => panic!(),
3323 };
3324
3325 match async_rx.blocking_recv() {
3326 Some(DelayedAction::AuthSessionRecord(_)) => {}
3327 _ => panic!("Oh no"),
3328 }
3329 }
3330
3331 {
3333 let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3334
3335 match session.validate_creds(
3336 &AuthCredential::Totp(totp_good_b),
3337 ts,
3338 &async_tx,
3339 &audit_tx,
3340 &webauthn,
3341 &pw_badlist_cache,
3342 ) {
3343 Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3344 _ => panic!(),
3345 };
3346 match session.validate_creds(
3347 &AuthCredential::Password(pw_good.to_string()),
3348 ts,
3349 &async_tx,
3350 &audit_tx,
3351 &webauthn,
3352 &pw_badlist_cache,
3353 ) {
3354 Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3355 _ => panic!(),
3356 };
3357
3358 match async_rx.blocking_recv() {
3359 Some(DelayedAction::AuthSessionRecord(_)) => {}
3360 _ => panic!("Oh no"),
3361 }
3362 }
3363
3364 drop(async_tx);
3365 assert!(async_rx.blocking_recv().is_none());
3366 drop(audit_tx);
3367 assert!(audit_rx.blocking_recv().is_none());
3368 }
3369}