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