kanidmd_lib/idm/
authsession.rs

1//! This module contains the logic to conduct an authentication of an account.
2//! Generally this has to process an authentication attempt, and validate each
3//! factor to assert that the user is legitimate. This also contains some
4//! support code for asynchronous task execution.
5use 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
37// Each CredHandler takes one or more credentials and determines if the
38// handlers requirements can be 100% fulfilled. This is where MFA or other
39// auth policies would exist, but each credHandler has to be a whole
40// encapsulated unit of function.
41
42const 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
63/// A response type to indicate the progress and potential result of an authentication attempt.
64enum 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)]
71/// The state of verification of an individual credential during an authentication.
72enum CredVerifyState {
73    Init,
74    Success,
75    Fail,
76}
77
78#[derive(Clone, Debug)]
79/// The state of a multifactor authenticator during authentication.
80struct CredTotp {
81    pw: Password,
82    pw_state: CredVerifyState,
83    totp: BTreeMap<String, Totp>,
84    mfa_state: CredVerifyState,
85}
86
87#[derive(Clone, Debug)]
88/// The state of a multifactor authenticator during authentication.
89struct CredBackupCode {
90    pw: Password,
91    pw_state: CredVerifyState,
92    backup_code: BackupCodes,
93    mfa_state: CredVerifyState,
94}
95
96#[derive(Clone, Debug)]
97/// The state of a multifactor authenticator during authentication.
98struct CredSecurityKey {
99    pw: Password,
100    pw_state: CredVerifyState,
101    chal: RequestChallengeResponse,
102    ska: SecurityKeyAuthentication,
103    mfa_state: CredVerifyState,
104}
105
106#[derive(Clone, Debug)]
107/// The state of a passkey during authentication
108struct CredPasskey {
109    chal: RequestChallengeResponse,
110    wan_state: PasskeyAuthentication,
111    state: CredVerifyState,
112}
113
114#[derive(Clone, Debug)]
115/// The state of an attested passkey during authentication
116struct CredAttestedPasskey {
117    chal: RequestChallengeResponse,
118    wan_state: AttestedPasskeyAuthentication,
119    state: CredVerifyState,
120}
121
122/// The current active handler for this authentication session. This is determined from what credentials
123/// are possible from the account, and what the user selected as the preferred authentication
124/// mechanism.
125#[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        // To verify the attestation post auth
154        att_ca_list: AttestationCaList,
155        // AP does `PartialEq` on cred_id
156        creds: BTreeMap<AttestedPasskeyV4, Uuid>,
157    },
158}
159
160impl CredHandler {
161    /// Given a credential and some external configuration, Generate the credential handler
162    /// that will be used for this session. This credential handler is a "self contained"
163    /// unit that defines what is possible to use during this authentication session to prevent
164    /// inconsistency.
165    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                // maps to unit.
198            })
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                // maps to unit.
226            })
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                // maps to unit.
260            })
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                // maps to unit.
290            })
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    /// Determine if this password factor requires an upgrade of it's cryptographic type. If
388    /// so, send an asynchronous event into the queue that will allow the password to have it's
389    /// content upgraded later.
390    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    /// validate that the client wants to authenticate as the anonymous user.
407    fn validate_anonymous(cred: &AuthCredential, cred_id: Uuid) -> CredState {
408        match cred {
409            AuthCredential::Anonymous => {
410                // For anonymous, no claims will ever be issued.
411                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    /// Validate a single password credential of the account.
427    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            // All other cases fail.
463            _ => {
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    /// Proceed with the next step in a multifactor authentication, based on the current
473    /// verification results and state. If this logic of this statemachine is violated, the
474    /// authentication will fail.
475    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                // MFA first
487                match cred {
488                    AuthCredential::Totp(totp_chal) => {
489                        // So long as one totp matches, success. Log which token was used.
490                        // We don't need to worry about the empty case since none will match and we
491                        // will get the failure.
492                        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                // PW second.
522                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    } // end CredHandler::PasswordTotp
565
566    /// Proceed with the next step in a multifactor authentication, based on the current
567    /// verification results and state. If this logic of this statemachine is violated, the
568    /// authentication will fail.
569    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                // MFA first
581                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                                // Success. Determine if we need to update the counter
587                                // async from r.
588                                if auth_result.needs_update() {
589                                    // Do async
590                                    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                                // Denied.
609                                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                // PW second.
625                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    /// Proceed with the next step in a multifactor authentication, based on the current
668    /// verification results and state. If this logic of this statemachine is violated, the
669    /// authentication will fail.
670    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                // MFA first
681                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                // PW second.
714                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    /// Validate a webauthn authentication attempt
757    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                // lets see how we go.
773                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                            // Success. Determine if we need to update the counter
778                            // async from r.
779                            if auth_result.needs_update() {
780                                // Do async
781                                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                            // Denied.
800                            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                        // Denied.
807                        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    /// Validate a webauthn authentication attempt
822    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                // lets see how we go.
839                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                            // Verify attestation of the key.
843
844                            if let Err(webauthn_err) = apk.verify_attestation(att_ca_list) {
845                                wan_cred.state = CredVerifyState::Fail;
846                                // Denied.
847                                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                            // Success. Determine if we need to update the counter
854                            // async from r.
855                            if auth_result.needs_update() {
856                                // Do async
857                                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                            // Denied.
876                            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                        // Denied.
883                        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    /// Given the current handler, proceed to authenticate the attempted credential step.
899    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    /// Determine based on the current status, what is the next allowed step that
979    /// can proceed.
980    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    /// Determine which mechanismes can proceed given the requested mechanism.
998    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)]
1026/// This interleaves with the client auth step. The client sends an "init"
1027/// and we go to the init state, sending back the list of what can proceed.
1028/// The client then sends a "begin" with the chosen mech that moves to
1029/// "InProgress", "Success" or "Denied". From there the CredHandler
1030/// is interacted with until we move to either "Success" or "Denied".
1031enum AuthSessionState {
1032    Init(NonEmpty<CredHandler>),
1033    // Stop! Don't make this a vec - make the credhandler able to hold multiple
1034    // internal copies of it's type and check against them all.
1035    //
1036    // Clippy wants this to be boxxed, however match on box types is a pain / problematic,
1037    // so I'm not sure it can be done.
1038    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)]
1062/// The current state of an authentication session that is in progress.
1063pub(crate) struct AuthSession {
1064    // Do we store a copy of the entry?
1065    // How do we know what claims to add?
1066    account: Account,
1067    // This policies that apply to this account
1068    account_policy: ResolvedAccountPolicy,
1069
1070    // Store how we plan to handle this sessions authentication: this is generally
1071    // made apparent by the presentation of an application id or not. If none is presented
1072    // we want the primary-interaction credentials.
1073    //
1074    // This handler will then handle the mfa and stepping up through to generate the auth states
1075    state: AuthSessionState,
1076
1077    // The type of session we will issue if successful
1078    issue: AuthIssueSession,
1079
1080    // What is the "intent" behind this auth session? Are we doing an initial auth? Or a re-auth
1081    // for a privilege grant?
1082    intent: AuthIntent,
1083
1084    // Where did the event come from?
1085    source: Source,
1086
1087    // The cryptographic provider to encrypt or sign anything in this operation.
1088    key_object: Arc<KeyObject>,
1089}
1090
1091impl AuthSession {
1092    /// Create a new auth session, based on the available credential handlers of the account.
1093    /// the session is a whole encapsulated unit of what we need to proceed, so that subsequent
1094    /// or interleved write operations do not cause inconsistency in this process.
1095    pub fn new(
1096        asd: AuthSessionData<'_>,
1097        privileged: bool,
1098        key_object: Arc<KeyObject>,
1099    ) -> (Option<Self>, AuthState) {
1100        // During this setup, determine the credential handler that we'll be using
1101        // for this session. This is currently based on presentation of an application
1102        // id.
1103        let state = if asd.account.is_within_valid_time(asd.ct) {
1104            // We want the primary handler - this is where we make a decision
1105            // based on the anonymous ... in theory this could be cleaner
1106            // and interact with the account more?
1107            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                // TODO: We can't yet fully enforce account policy on auth, there is a bit of work
1118                // to do to be able to check for pw / mfa etc.
1119                // A possible gotcha is service accounts which can't be affected by these policies?
1120
1121                // let cred_type_min = asd.account_policy.credential_policy();
1122
1123                if let Some(cred) = &asd.account.primary {
1124                    // Is it a pw-only credential?
1125                    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                        // No MFA types were setup, allow the PW only to proceed then.
1141                        if let Some(ch) = CredHandler::build_from_password_only(cred) {
1142                            handlers.push(ch);
1143                        }
1144                    }
1145                }
1146
1147                trace!(?handlers);
1148
1149                // Important - if attested is present, don't use passkeys
1150                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 credhandler == deny, finish = true.
1191        if let Some(reason) = state.is_denied() {
1192            // Already denied, lets send that result
1193            (None, AuthState::Denied(reason.to_string()))
1194        } else {
1195            // We can proceed
1196            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            // Get the set of mechanisms that can proceed. This is tied
1206            // to the session so that it can mutate state and have progression
1207            // of what's next, or ordering.
1208            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    /// Build a new auth session which has been preconfigured for re-authentication.
1217    /// This differs from [`AuthSession::new`] as we preselect the credential that
1218    /// will be used in this operation based on the credential id that was used in the
1219    /// initial authentication.
1220    pub(crate) fn new_reauth(
1221        asd: AuthSessionData<'_>,
1222        session_id: Uuid,
1223        session: &Session,
1224        cred_id: Uuid,
1225        key_object: Arc<KeyObject>,
1226    ) -> (Option<Self>, AuthState) {
1227        #[allow(clippy::large_enum_variant)]
1228        /// An inner enum to allow us to more easily define state within this fn
1229        enum State {
1230            Expired,
1231            NoMatchingCred,
1232            Proceed(CredHandler),
1233        }
1234
1235        let state = if asd.account.is_within_valid_time(asd.ct) {
1236            // Get the credential that matches this cred_id and auth type used in the
1237            // initial authentication.
1238
1239            // We can't yet fully enforce account policy on auth, there is a bit of work
1240            // to do to be able to check the credential types match what we expect.
1241
1242            let mut cred_handler = None;
1243
1244            match session.type_ {
1245                AuthType::Password
1246                | AuthType::GeneratedPassword
1247                // If a backup code was used, since the code was scrubbed at use we need to
1248                // fall back to the password of the account instead.
1249                | AuthType::PasswordBackupCode => {
1250                    if let Some(primary) = asd.account.primary.as_ref() {
1251                        if primary.uuid == cred_id {
1252                            cred_handler = CredHandler::build_from_password_only(primary)
1253                        }
1254                    }
1255                }
1256                AuthType::PasswordTotp => {
1257                    if let Some(primary) = asd.account.primary.as_ref() {
1258                        if primary.uuid == cred_id {
1259                            cred_handler = CredHandler::build_from_password_totp(primary)
1260                        }
1261                    }
1262                }
1263                AuthType::PasswordSecurityKey => {
1264                    if let Some(primary) = asd.account.primary.as_ref() {
1265                        if primary.uuid == cred_id {
1266                            cred_handler =
1267                                CredHandler::build_from_password_security_key(primary, asd.webauthn)
1268                        }
1269                    }
1270                }
1271                AuthType::Passkey => {
1272                    // Scan both attested and passkeys for the possible credential.
1273                    let maybe_pk: Option<PasskeyV4> = asd
1274                        .account
1275                        .attested_passkeys
1276                        .get(&cred_id)
1277                        .map(|(_, apk)| apk.into())
1278                        .or_else(|| asd.account.passkeys.get(&cred_id).map(|(_, pk)| pk.clone()));
1279
1280                    if let Some(pk) = maybe_pk {
1281                        if let Some(ch) =
1282                            CredHandler::build_from_single_passkey(cred_id, pk, asd.webauthn)
1283                        {
1284                            // Update it.
1285                            debug_assert!(cred_handler.is_none());
1286                            cred_handler = Some(ch);
1287                        } else {
1288                            security_critical!(
1289                                "corrupt credentials, unable to start passkey credhandler"
1290                            );
1291                        }
1292                    }
1293                }
1294                AuthType::AttestedPasskey => {
1295                    if let Some(att_ca_list) = asd.account_policy.webauthn_attestation_ca_list() {
1296                        if let Some(pk) = asd
1297                            .account
1298                            .attested_passkeys
1299                            .get(&cred_id)
1300                            .map(|(_, pk)| pk)
1301                        {
1302                            if let Some(ch) = CredHandler::build_from_single_attested_pk(
1303                                cred_id,
1304                                pk,
1305                                att_ca_list,
1306                                asd.webauthn,
1307                            ) {
1308                                // Update it.
1309                                debug_assert!(cred_handler.is_none());
1310                                cred_handler = Some(ch);
1311                            } else {
1312                                security_critical!(
1313                            "corrupt credentials, unable to start attested passkey credhandler"
1314                        );
1315                            }
1316                        }
1317                    }
1318                }
1319                AuthType::Anonymous => {}
1320            }
1321
1322            // Did anything get set-up?
1323
1324            if let Some(cred_handler) = cred_handler {
1325                State::Proceed(cred_handler)
1326            } else {
1327                State::NoMatchingCred
1328            }
1329        } else {
1330            State::Expired
1331        };
1332
1333        let session_expiry = match session.state {
1334            SessionState::ExpiresAt(odt) => Some(odt),
1335            SessionState::NeverExpires => None,
1336            SessionState::RevokedAt(_) => {
1337                security_error!(
1338                    "Invalid State - Should not be possible to trigger re-auth on revoked session."
1339                );
1340                return (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()));
1341            }
1342        };
1343
1344        match state {
1345            State::Proceed(handler) => {
1346                let allow = handler.next_auth_allowed();
1347                let auth_session = AuthSession {
1348                    account: asd.account,
1349                    account_policy: asd.account_policy,
1350                    state: AuthSessionState::InProgress(handler),
1351                    issue: asd.issue,
1352                    intent: AuthIntent::Reauth {
1353                        session_id,
1354                        session_expiry,
1355                    },
1356                    source: asd.client_auth_info.source,
1357                    key_object,
1358                };
1359
1360                let as_state = AuthState::Continue(allow);
1361                (Some(auth_session), as_state)
1362            }
1363            State::Expired => {
1364                security_info!("account expired");
1365                (None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()))
1366            }
1367            State::NoMatchingCred => {
1368                security_error!("Unable to select a credential for authentication");
1369                (None, AuthState::Denied(BAD_CREDENTIALS.to_string()))
1370            }
1371        }
1372    }
1373
1374    /// If the credential class can be softlocked, retrieve the credential ID. This is
1375    /// only used when a credential requires softlocking.
1376    pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
1377        match &self.state {
1378            AuthSessionState::InProgress(CredHandler::Password { cred_id, .. })
1379            | AuthSessionState::InProgress(CredHandler::PasswordTotp { cred_id, .. })
1380            | AuthSessionState::InProgress(CredHandler::PasswordBackupCode { cred_id, .. }) => {
1381                Ok(Some(*cred_id))
1382            }
1383            AuthSessionState::InProgress(CredHandler::Anonymous { .. })
1384            | AuthSessionState::InProgress(CredHandler::PasswordSecurityKey { .. })
1385            | AuthSessionState::InProgress(CredHandler::Passkey { .. })
1386            | AuthSessionState::InProgress(CredHandler::AttestedPasskey { .. }) => Ok(None),
1387
1388            AuthSessionState::Init(_) => {
1389                debug!(
1390                    "Request for credential uuid invalid as auth session state not yet initialised"
1391                );
1392                Err(OperationError::AU0001InvalidState)
1393            }
1394            AuthSessionState::Success | AuthSessionState::Denied(_) => {
1395                debug!("Request for credential uuid invalid as auth session state has progressed");
1396                Err(OperationError::AU0001InvalidState)
1397            }
1398        }
1399    }
1400
1401    /// Given the users indicated and preferred authentication mechanism that they want to proceed
1402    /// with, select the credential handler and begin the process of stepping through the
1403    /// authentication process.
1404    pub fn start_session(
1405        &mut self,
1406        mech: &AuthMech,
1407        // time: &Duration,
1408        // webauthn: &WebauthnCore,
1409    ) -> Result<AuthState, OperationError> {
1410        // Given some auth mech, select which credential(s) are appropriate
1411        // and attempt to use them.
1412
1413        // Today we only select one, but later we could have *multiple* that
1414        // match the selector.
1415        let (next_state, response) = match &mut self.state {
1416            AuthSessionState::Success
1417            | AuthSessionState::Denied(_)
1418            | AuthSessionState::InProgress(_) => (
1419                None,
1420                Err(OperationError::InvalidAuthState(
1421                    "session already finalised!".to_string(),
1422                )),
1423            ),
1424            AuthSessionState::Init(handlers) => {
1425                // Which handlers are relevant?
1426                let mut allowed_handlers: Vec<_> = handlers
1427                    .iter()
1428                    .filter(|ch| ch.can_proceed(mech))
1429                    .cloned()
1430                    .collect();
1431
1432                if let Some(allowed_handler) = allowed_handlers.pop() {
1433                    let allowed: Vec<_> = allowed_handler.next_auth_allowed();
1434
1435                    if allowed.is_empty() {
1436                        security_info!("Unable to negotiate credentials");
1437                        (
1438                            None,
1439                            Err(OperationError::InvalidAuthState(
1440                                "unable to negotiate credentials".to_string(),
1441                            )),
1442                        )
1443                    } else {
1444                        (
1445                            Some(AuthSessionState::InProgress(allowed_handler)),
1446                            Ok(AuthState::Continue(allowed)),
1447                        )
1448                    }
1449                } else {
1450                    security_error!("Unable to select a credential for authentication");
1451                    (
1452                        Some(AuthSessionState::Denied(BAD_CREDENTIALS)),
1453                        Ok(AuthState::Denied(BAD_CREDENTIALS.to_string())),
1454                    )
1455                }
1456            }
1457        };
1458
1459        if let Some(mut next_state) = next_state {
1460            std::mem::swap(&mut self.state, &mut next_state);
1461        };
1462
1463        response
1464    }
1465
1466    /// Conduct a step of the authentication process. This validates the next credential factor
1467    /// presented and returns a result of Success, Continue, or Denied. Only in the success
1468    /// case is a UAT granted -- all others do not, including raised operation errors.
1469    pub fn validate_creds(
1470        &mut self,
1471        cred: &AuthCredential,
1472        time: Duration,
1473        async_tx: &Sender<DelayedAction>,
1474        audit_tx: &Sender<AuditEvent>,
1475        webauthn: &Webauthn,
1476        pw_badlist: &HashSet<String>,
1477    ) -> Result<AuthState, OperationError> {
1478        let (next_state, response) = match &mut self.state {
1479            AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => {
1480                return Err(OperationError::InvalidAuthState(
1481                    "session already finalised!".to_string(),
1482                ));
1483            }
1484            AuthSessionState::InProgress(ref mut handler) => {
1485                match handler.validate(
1486                    cred,
1487                    time,
1488                    self.account.uuid,
1489                    async_tx,
1490                    webauthn,
1491                    pw_badlist,
1492                ) {
1493                    CredState::Success { auth_type, cred_id } => {
1494                        // Issue the uat based on a set of factors.
1495                        let uat = self.issue_uat(auth_type, time, async_tx, cred_id)?;
1496
1497                        let jwt = Jws::into_json(&uat).map_err(|e| {
1498                            admin_error!(?e, "Failed to serialise into Jws");
1499                            OperationError::AU0002JwsSerialisation
1500                        })?;
1501
1502                        // Now encrypt and prepare the token for return to the client.
1503                        let token = self.key_object.jws_es256_sign(&jwt, time).map_err(|e| {
1504                            admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
1505                            OperationError::AU0003JwsSignature
1506                        })?;
1507
1508                        (
1509                            Some(AuthSessionState::Success),
1510                            Ok(AuthState::Success(Box::new(token), self.issue)),
1511                        )
1512                    }
1513                    CredState::Continue(allowed) => {
1514                        security_info!(?allowed, "Request credential continuation");
1515                        (None, Ok(AuthState::Continue(allowed.into_iter().collect())))
1516                    }
1517                    CredState::Denied(reason) => {
1518                        if audit_tx
1519                            .send(AuditEvent::AuthenticationDenied {
1520                                source: self.source.clone().into(),
1521                                spn: self.account.spn.clone(),
1522                                uuid: self.account.uuid,
1523                                time: OffsetDateTime::UNIX_EPOCH + time,
1524                            })
1525                            .is_err()
1526                        {
1527                            error!("Unable to submit audit event to queue");
1528                        }
1529                        security_info!(%reason, "Credentials denied");
1530                        (
1531                            Some(AuthSessionState::Denied(reason)),
1532                            Ok(AuthState::Denied(reason.to_string())),
1533                        )
1534                    }
1535                }
1536            }
1537        };
1538
1539        if let Some(mut next_state) = next_state {
1540            std::mem::swap(&mut self.state, &mut next_state);
1541        };
1542
1543        // Also send an async message to self to log the auth as provided.
1544        // Alternately, open a write, and commit the needed security metadata here
1545        // now rather than async (probably better for lock-outs etc)
1546        //
1547        // TODO #59: Async message the account owner about the login?
1548        // If this fails, how can we in memory lock the account?
1549        //
1550        // The lockouts could also be an in-memory concept too?
1551
1552        // If this succeeds audit?
1553        //  If success, to authtoken?
1554
1555        response
1556    }
1557
1558    fn issue_uat(
1559        &mut self,
1560        auth_type: AuthType,
1561        time: Duration,
1562        async_tx: &Sender<DelayedAction>,
1563        cred_id: Uuid,
1564    ) -> Result<UserAuthToken, OperationError> {
1565        security_debug!("Successful cred handling");
1566        match self.intent {
1567            AuthIntent::InitialAuth { privileged } => {
1568                let session_id = Uuid::new_v4();
1569                // We need to actually work this out better, and then
1570                // pass it to to_userauthtoken
1571                let scope = match auth_type {
1572                    AuthType::Anonymous => SessionScope::ReadOnly,
1573                    AuthType::GeneratedPassword => SessionScope::ReadWrite,
1574                    AuthType::Password
1575                    | AuthType::PasswordTotp
1576                    | AuthType::PasswordBackupCode
1577                    | AuthType::PasswordSecurityKey
1578                    | AuthType::Passkey
1579                    | AuthType::AttestedPasskey => {
1580                        if privileged {
1581                            SessionScope::ReadWrite
1582                        } else {
1583                            SessionScope::PrivilegeCapable
1584                        }
1585                    }
1586                };
1587
1588                security_info!(
1589                    "Issuing {:?} session ({:?}) {} for {} {}",
1590                    self.issue,
1591                    scope,
1592                    session_id,
1593                    self.account.spn,
1594                    self.account.uuid
1595                );
1596
1597                let uat = self
1598                    .account
1599                    .to_userauthtoken(session_id, scope, time, &self.account_policy)
1600                    .ok_or(OperationError::AU0004UserAuthTokenInvalid)?;
1601
1602                // Queue the session info write.
1603                // This is dependent on the type of authentication factors
1604                // used. Generally we won't submit for Anonymous. Add an extra
1605                // safety barrier for auth types that shouldn't be here. Generally we
1606                // submit session info for everything else.
1607                match auth_type {
1608                    AuthType::Anonymous => {
1609                        // Skip - these sessions are not validated by session id.
1610                    }
1611                    AuthType::Password
1612                    | AuthType::GeneratedPassword
1613                    | AuthType::PasswordTotp
1614                    | AuthType::PasswordBackupCode
1615                    | AuthType::PasswordSecurityKey
1616                    | AuthType::Passkey
1617                    | AuthType::AttestedPasskey => {
1618                        trace!("⚠️   Queued AuthSessionRecord for {}", self.account.uuid);
1619                        async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
1620                            target_uuid: self.account.uuid,
1621                            session_id,
1622                            cred_id,
1623                            label: "Auth Session".to_string(),
1624                            expiry: uat.expiry,
1625                            issued_at: uat.issued_at,
1626                            issued_by: IdentityId::User(self.account.uuid),
1627                            scope,
1628                            type_: auth_type,
1629                        }))
1630                        .map_err(|e| {
1631                            debug!(?e, "queue failure");
1632                            admin_error!("unable to queue failing authentication as the session will not validate ... ");
1633                            OperationError::AU0005DelayedProcessFailure
1634                        })?;
1635                    }
1636                };
1637
1638                Ok(uat)
1639            }
1640            AuthIntent::Reauth {
1641                session_id,
1642                session_expiry,
1643            } => {
1644                // Sanity check - We have already been really strict about what session types
1645                // can actually trigger a re-auth, but we recheck here for paranoia!
1646                let scope = match auth_type {
1647                    AuthType::Anonymous | AuthType::GeneratedPassword => {
1648                        error!("AuthType used in Reauth is not valid for session re-issuance. Rejecting");
1649                        return Err(OperationError::AU0006CredentialMayNotReauthenticate);
1650                    }
1651                    AuthType::Password
1652                    | AuthType::PasswordTotp
1653                    | AuthType::PasswordBackupCode
1654                    | AuthType::PasswordSecurityKey
1655                    | AuthType::Passkey
1656                    | AuthType::AttestedPasskey => SessionScope::PrivilegeCapable,
1657                };
1658
1659                let uat = self
1660                    .account
1661                    .to_reissue_userauthtoken(
1662                        session_id,
1663                        session_expiry,
1664                        scope,
1665                        time,
1666                        &self.account_policy,
1667                    )
1668                    .ok_or(OperationError::AU0007UserAuthTokenInvalid)?;
1669
1670                Ok(uat)
1671            }
1672        }
1673    }
1674
1675    /// End the session, defaulting to a denied.
1676    pub fn end_session(&mut self, reason: &'static str) -> Result<AuthState, OperationError> {
1677        let mut next_state = AuthSessionState::Denied(reason);
1678        std::mem::swap(&mut self.state, &mut next_state);
1679        Ok(AuthState::Denied(reason.to_string()))
1680    }
1681
1682    fn valid_auth_mechs(&self) -> Vec<AuthMech> {
1683        match &self.state {
1684            AuthSessionState::Success
1685            | AuthSessionState::Denied(_)
1686            | AuthSessionState::InProgress(_) => Vec::with_capacity(0),
1687            AuthSessionState::Init(handlers) => {
1688                // Iterate over the handlers into what mechs they are
1689                // and filter to unique?
1690                handlers.iter().map(|h| h.allows_mech()).collect()
1691            }
1692        }
1693    }
1694}
1695
1696#[cfg(test)]
1697mod tests {
1698    use std::time::Duration;
1699
1700    use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
1701    use hashbrown::HashSet;
1702    use kanidm_proto::internal::{UatPurpose, UserAuthToken};
1703    use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
1704    use tokio::sync::mpsc::unbounded_channel as unbounded;
1705    use webauthn_authenticator_rs::softpasskey::SoftPasskey;
1706    use webauthn_authenticator_rs::WebauthnAuthenticator;
1707    use webauthn_rs::prelude::{RequestChallengeResponse, Webauthn};
1708
1709    use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
1710    use crate::credential::{BackupCodes, Credential};
1711    use crate::idm::account::Account;
1712    use crate::idm::accountpolicy::ResolvedAccountPolicy;
1713    use crate::idm::audit::AuditEvent;
1714    use crate::idm::authsession::{
1715        AuthSession, AuthSessionData, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG,
1716        BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG,
1717    };
1718    use crate::idm::delayed::DelayedAction;
1719    use crate::idm::AuthState;
1720    use crate::migration_data::{BUILTIN_ACCOUNT_ANONYMOUS, BUILTIN_ACCOUNT_TEST_PERSON};
1721    use crate::prelude::*;
1722    use crate::server::keys::KeyObjectInternal;
1723    use crate::utils::readable_password_from_random;
1724    use kanidm_lib_crypto::CryptoPolicy;
1725
1726    fn create_pw_badlist_cache() -> HashSet<String> {
1727        let mut s = HashSet::new();
1728        s.insert("list@no3IBTyqHu$bad".to_lowercase());
1729        s
1730    }
1731
1732    fn create_webauthn() -> webauthn_rs::Webauthn {
1733        webauthn_rs::WebauthnBuilder::new(
1734            "example.com",
1735            &url::Url::parse("https://idm.example.com").unwrap(),
1736        )
1737        .and_then(|builder| builder.build())
1738        .unwrap()
1739    }
1740
1741    #[test]
1742    fn test_idm_authsession_anonymous_auth_mech() {
1743        sketching::test_init();
1744
1745        let webauthn = create_webauthn();
1746
1747        let anon_account: Account = BUILTIN_ACCOUNT_ANONYMOUS.clone().into();
1748
1749        let asd = AuthSessionData {
1750            account: anon_account,
1751            account_policy: ResolvedAccountPolicy::default(),
1752            issue: AuthIssueSession::Token,
1753            webauthn: &webauthn,
1754            ct: duration_from_epoch_now(),
1755            client_auth_info: Source::Internal.into(),
1756        };
1757
1758        let key_object = KeyObjectInternal::new_test();
1759        let (session, state) = AuthSession::new(asd, false, key_object);
1760        if let AuthState::Choose(auth_mechs) = state {
1761            assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
1762        } else {
1763            panic!("Invalid auth state")
1764        }
1765
1766        let state = session
1767            .expect("Missing auth session?")
1768            .start_session(&AuthMech::Anonymous)
1769            .expect("Failed to select anonymous mech.");
1770
1771        if let AuthState::Continue(auth_mechs) = state {
1772            assert!(auth_mechs
1773                .iter()
1774                .any(|x| matches!(x, AuthAllowed::Anonymous)));
1775        } else {
1776            panic!("Invalid auth state")
1777        }
1778    }
1779
1780    macro_rules! start_password_session {
1781        (
1782            $audit:expr,
1783            $account:expr,
1784            $webauthn:expr,
1785            $privileged:expr
1786        ) => {{
1787            let asd = AuthSessionData {
1788                account: $account.clone(),
1789                account_policy: ResolvedAccountPolicy::default(),
1790                issue: AuthIssueSession::Token,
1791                webauthn: $webauthn,
1792                ct: duration_from_epoch_now(),
1793                client_auth_info: Source::Internal.into(),
1794            };
1795            let key_object = KeyObjectInternal::new_test();
1796            let (session, state) = AuthSession::new(asd, $privileged, key_object);
1797            let mut session = session.unwrap();
1798
1799            if let AuthState::Choose(auth_mechs) = state {
1800                assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Password)));
1801            } else {
1802                panic!();
1803            }
1804
1805            let state = session
1806                .start_session(&AuthMech::Password)
1807                .expect("Failed to select anonymous mech.");
1808
1809            if let AuthState::Continue(auth_mechs) = state {
1810                assert!(auth_mechs
1811                    .iter()
1812                    .any(|x| matches!(x, AuthAllowed::Password)));
1813            } else {
1814                panic!("Invalid auth state")
1815            }
1816
1817            (session, create_pw_badlist_cache())
1818        }};
1819    }
1820
1821    fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
1822        let webauthn = create_webauthn();
1823        // create the ent
1824        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1825        // manually load in a cred
1826        let p = CryptoPolicy::minimum();
1827        let cred = Credential::new_password_only(&p, "test_password").unwrap();
1828        account.primary = Some(cred);
1829
1830        let (async_tx, mut async_rx) = unbounded();
1831        let (audit_tx, mut audit_rx) = unbounded();
1832
1833        // now check
1834        let (mut session, pw_badlist_cache) =
1835            start_password_session!(&mut audit, account, &webauthn, false);
1836
1837        let attempt = AuthCredential::Password("bad_password".to_string());
1838        match session.validate_creds(
1839            &attempt,
1840            Duration::from_secs(0),
1841            &async_tx,
1842            &audit_tx,
1843            &webauthn,
1844            &pw_badlist_cache,
1845        ) {
1846            Ok(AuthState::Denied(_)) => {}
1847            _ => panic!(),
1848        };
1849
1850        match audit_rx.try_recv() {
1851            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1852            _ => panic!("Oh no"),
1853        }
1854
1855        // === Now begin a new session, and use a good pw.
1856
1857        let (mut session, pw_badlist_cache) =
1858            start_password_session!(&mut audit, account, &webauthn, privileged);
1859
1860        let attempt = AuthCredential::Password("test_password".to_string());
1861        let uat: UserAuthToken = match session.validate_creds(
1862            &attempt,
1863            Duration::from_secs(0),
1864            &async_tx,
1865            &audit_tx,
1866            &webauthn,
1867            &pw_badlist_cache,
1868        ) {
1869            Ok(AuthState::Success(jwsc, AuthIssueSession::Token)) => {
1870                let jws_verifier = JwsDangerReleaseWithoutVerify::default();
1871
1872                jws_verifier
1873                    .verify(&*jwsc)
1874                    .unwrap()
1875                    .from_json::<UserAuthToken>()
1876                    .unwrap()
1877            }
1878            _ => panic!(),
1879        };
1880
1881        match async_rx.blocking_recv() {
1882            Some(DelayedAction::AuthSessionRecord(_)) => {}
1883            _ => panic!("Oh no"),
1884        }
1885
1886        drop(async_tx);
1887        assert!(async_rx.blocking_recv().is_none());
1888        drop(audit_tx);
1889        assert!(audit_rx.blocking_recv().is_none());
1890
1891        uat
1892    }
1893
1894    #[test]
1895    fn test_idm_authsession_simple_password_mech() {
1896        sketching::test_init();
1897        let uat = start_session_simple_password_mech(false);
1898        match uat.purpose {
1899            UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1900            UatPurpose::ReadWrite { expiry } => {
1901                // Long lived RO session capable of reauth
1902                assert!(expiry.is_none())
1903            }
1904        }
1905    }
1906
1907    #[test]
1908    fn test_idm_authsession_simple_password_mech_priv_shortcut() {
1909        sketching::test_init();
1910        let uat = start_session_simple_password_mech(true);
1911        match uat.purpose {
1912            UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
1913            UatPurpose::ReadWrite { expiry } => {
1914                // Short lived RW session
1915                assert!(expiry.is_some())
1916            }
1917        }
1918    }
1919
1920    #[test]
1921    fn test_idm_authsession_simple_password_badlist() {
1922        sketching::test_init();
1923        let webauthn = create_webauthn();
1924        // create the ent
1925        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
1926        // manually load in a cred
1927        let p = CryptoPolicy::minimum();
1928        let cred = Credential::new_password_only(&p, "list@no3IBTyqHu$bad").unwrap();
1929        account.primary = Some(cred);
1930
1931        let (async_tx, mut async_rx) = unbounded();
1932        let (audit_tx, mut audit_rx) = unbounded();
1933
1934        // now check, even though the password is correct, Auth should be denied since it is in badlist
1935        let (mut session, pw_badlist_cache) =
1936            start_password_session!(&mut audit, account, &webauthn, false);
1937
1938        let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
1939        match session.validate_creds(
1940            &attempt,
1941            Duration::from_secs(0),
1942            &async_tx,
1943            &audit_tx,
1944            &webauthn,
1945            &pw_badlist_cache,
1946        ) {
1947            Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
1948            _ => panic!(),
1949        };
1950
1951        match audit_rx.try_recv() {
1952            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
1953            _ => panic!("Oh no"),
1954        }
1955
1956        drop(async_tx);
1957        assert!(async_rx.blocking_recv().is_none());
1958        drop(audit_tx);
1959        assert!(audit_rx.blocking_recv().is_none());
1960    }
1961
1962    fn start_password_totp_session(
1963        account: &Account,
1964        webauthn: &Webauthn,
1965    ) -> (AuthSession, HashSet<String>) {
1966        let asd = AuthSessionData {
1967            account: account.clone(),
1968            account_policy: ResolvedAccountPolicy::default(),
1969            issue: AuthIssueSession::Token,
1970            webauthn,
1971            ct: duration_from_epoch_now(),
1972            client_auth_info: Source::Internal.into(),
1973        };
1974        let key_object = KeyObjectInternal::new_test();
1975        let (session, state) = AuthSession::new(asd, false, key_object);
1976        let mut session = session.expect("Session was unable to be created.");
1977
1978        if let AuthState::Choose(auth_mechs) = state {
1979            assert!(auth_mechs
1980                .iter()
1981                .any(|x| matches!(x, AuthMech::PasswordTotp)))
1982        } else {
1983            panic!();
1984        }
1985
1986        let state = session
1987            .start_session(&AuthMech::PasswordTotp)
1988            .expect("Failed to select password totp mech.");
1989
1990        if let AuthState::Continue(auth_mechs) = state {
1991            assert!(auth_mechs.iter().fold(false, |acc, x| match x {
1992                AuthAllowed::Totp => true,
1993                _ => acc,
1994            }));
1995        } else {
1996            panic!("Invalid auth state")
1997        }
1998
1999        (session, create_pw_badlist_cache())
2000    }
2001
2002    fn start_password_sk_session(
2003        account: &Account,
2004        webauthn: &Webauthn,
2005    ) -> (AuthSession, RequestChallengeResponse, HashSet<String>) {
2006        let asd = AuthSessionData {
2007            account: account.clone(),
2008            account_policy: ResolvedAccountPolicy::default(),
2009            issue: AuthIssueSession::Token,
2010            webauthn,
2011            ct: duration_from_epoch_now(),
2012            client_auth_info: Source::Internal.into(),
2013        };
2014        let key_object = KeyObjectInternal::new_test();
2015        let (session, state) = AuthSession::new(asd, false, key_object);
2016        let mut session = session.expect("Session was unable to be created.");
2017
2018        if let AuthState::Choose(auth_mechs) = state {
2019            assert!(auth_mechs
2020                .iter()
2021                .any(|x| matches!(x, AuthMech::PasswordSecurityKey)))
2022        } else {
2023            panic!();
2024        }
2025
2026        let state = session
2027            .start_session(&AuthMech::PasswordSecurityKey)
2028            .expect("Failed to select password security key mech.");
2029
2030        let mut rchal = None;
2031
2032        if let AuthState::Continue(auth_mechs) = state {
2033            assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2034                AuthAllowed::SecurityKey(chal) => {
2035                    rchal = Some(chal.clone());
2036                    true
2037                }
2038                _ => acc,
2039            }));
2040        } else {
2041            panic!("Invalid auth state")
2042        }
2043
2044        (session, rchal.unwrap(), create_pw_badlist_cache())
2045    }
2046
2047    fn start_password_bc_session(
2048        account: &Account,
2049        webauthn: &Webauthn,
2050    ) -> (AuthSession, HashSet<String>) {
2051        let asd = AuthSessionData {
2052            account: account.clone(),
2053            account_policy: ResolvedAccountPolicy::default(),
2054            issue: AuthIssueSession::Token,
2055            webauthn,
2056            ct: duration_from_epoch_now(),
2057            client_auth_info: Source::Internal.into(),
2058        };
2059        let key_object = KeyObjectInternal::new_test();
2060        let (session, state) = AuthSession::new(asd, false, key_object);
2061        let mut session = session.expect("Session was unable to be created.");
2062
2063        if let AuthState::Choose(auth_mechs) = state {
2064            assert!(auth_mechs
2065                .iter()
2066                .any(|x| matches!(x, AuthMech::PasswordBackupCode)))
2067        } else {
2068            panic!();
2069        }
2070
2071        let state = session
2072            .start_session(&AuthMech::PasswordBackupCode)
2073            .expect("Failed to select password backup code mech.");
2074
2075        if let AuthState::Continue(auth_mechs) = state {
2076            assert!(auth_mechs.iter().fold(false, |acc, x| match x {
2077                AuthAllowed::BackupCode => true,
2078                _ => acc,
2079            }));
2080        } else {
2081            panic!("Invalid auth state")
2082        }
2083
2084        (session, create_pw_badlist_cache())
2085    }
2086
2087    #[test]
2088    fn test_idm_authsession_totp_password_mech() {
2089        sketching::test_init();
2090        let webauthn = create_webauthn();
2091        // create the ent
2092        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2093
2094        // Setup a fake time stamp for consistency.
2095        let ts = Duration::from_secs(12345);
2096
2097        // manually load in a cred
2098        let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2099
2100        let totp_good = totp
2101            .do_totp_duration_from_epoch(&ts)
2102            .expect("failed to perform totp.");
2103        let totp_bad = totp
2104            .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2105            .expect("failed to perform totp.");
2106        assert!(totp_bad != totp_good);
2107
2108        let pw_good = "test_password";
2109        let pw_bad = "bad_password";
2110
2111        let p = CryptoPolicy::minimum();
2112        let cred = Credential::new_password_only(&p, pw_good)
2113            .unwrap()
2114            .append_totp("totp".to_string(), totp);
2115        // add totp also
2116        account.primary = Some(cred);
2117
2118        let (async_tx, mut async_rx) = unbounded();
2119        let (audit_tx, mut audit_rx) = unbounded();
2120
2121        // now check
2122
2123        // check send anon (fail)
2124        {
2125            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2126
2127            match session.validate_creds(
2128                &AuthCredential::Anonymous,
2129                ts,
2130                &async_tx,
2131                &audit_tx,
2132                &webauthn,
2133                &pw_badlist_cache,
2134            ) {
2135                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2136                _ => panic!(),
2137            };
2138
2139            match audit_rx.try_recv() {
2140                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2141                _ => panic!("Oh no"),
2142            }
2143        }
2144
2145        // == two step checks
2146
2147        // Sending a PW first is an immediate fail.
2148        {
2149            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2150
2151            match session.validate_creds(
2152                &AuthCredential::Password(pw_bad.to_string()),
2153                ts,
2154                &async_tx,
2155                &audit_tx,
2156                &webauthn,
2157                &pw_badlist_cache,
2158            ) {
2159                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2160                _ => panic!(),
2161            };
2162
2163            match audit_rx.try_recv() {
2164                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2165                _ => panic!("Oh no"),
2166            }
2167        }
2168        // check send bad totp, should fail immediate
2169        {
2170            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2171
2172            match session.validate_creds(
2173                &AuthCredential::Totp(totp_bad),
2174                ts,
2175                &async_tx,
2176                &audit_tx,
2177                &webauthn,
2178                &pw_badlist_cache,
2179            ) {
2180                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2181                _ => panic!(),
2182            };
2183
2184            match audit_rx.try_recv() {
2185                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2186                _ => panic!("Oh no"),
2187            }
2188        }
2189
2190        // check send good totp, should continue
2191        //      then bad pw, fail pw
2192        {
2193            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2194
2195            match session.validate_creds(
2196                &AuthCredential::Totp(totp_good),
2197                ts,
2198                &async_tx,
2199                &audit_tx,
2200                &webauthn,
2201                &pw_badlist_cache,
2202            ) {
2203                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2204                _ => panic!(),
2205            };
2206            match session.validate_creds(
2207                &AuthCredential::Password(pw_bad.to_string()),
2208                ts,
2209                &async_tx,
2210                &audit_tx,
2211                &webauthn,
2212                &pw_badlist_cache,
2213            ) {
2214                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2215                _ => panic!(),
2216            };
2217
2218            match audit_rx.try_recv() {
2219                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2220                _ => panic!("Oh no"),
2221            }
2222        }
2223
2224        // check send good totp, should continue
2225        //      then good pw, success
2226        {
2227            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2228
2229            match session.validate_creds(
2230                &AuthCredential::Totp(totp_good),
2231                ts,
2232                &async_tx,
2233                &audit_tx,
2234                &webauthn,
2235                &pw_badlist_cache,
2236            ) {
2237                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2238                _ => panic!(),
2239            };
2240            match session.validate_creds(
2241                &AuthCredential::Password(pw_good.to_string()),
2242                ts,
2243                &async_tx,
2244                &audit_tx,
2245                &webauthn,
2246                &pw_badlist_cache,
2247            ) {
2248                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2249                _ => panic!(),
2250            };
2251
2252            match async_rx.blocking_recv() {
2253                Some(DelayedAction::AuthSessionRecord(_)) => {}
2254                _ => panic!("Oh no"),
2255            }
2256        }
2257
2258        drop(async_tx);
2259        assert!(async_rx.blocking_recv().is_none());
2260        drop(audit_tx);
2261        assert!(audit_rx.blocking_recv().is_none());
2262    }
2263
2264    #[test]
2265    fn test_idm_authsession_password_mfa_badlist() {
2266        sketching::test_init();
2267        let webauthn = create_webauthn();
2268        // create the ent
2269        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2270
2271        // Setup a fake time stamp for consistency.
2272        let ts = Duration::from_secs(12345);
2273
2274        // manually load in a cred
2275        let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2276
2277        let totp_good = totp
2278            .do_totp_duration_from_epoch(&ts)
2279            .expect("failed to perform totp.");
2280
2281        let pw_badlist = "list@no3IBTyqHu$bad";
2282
2283        let p = CryptoPolicy::minimum();
2284        let cred = Credential::new_password_only(&p, pw_badlist)
2285            .unwrap()
2286            .append_totp("totp".to_string(), totp);
2287        // add totp also
2288        account.primary = Some(cred);
2289
2290        let (async_tx, mut async_rx) = unbounded();
2291        let (audit_tx, mut audit_rx) = unbounded();
2292
2293        // now check
2294
2295        // == two step checks
2296
2297        // check send good totp, should continue
2298        //      then badlist pw, failed
2299        {
2300            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2301
2302            match session.validate_creds(
2303                &AuthCredential::Totp(totp_good),
2304                ts,
2305                &async_tx,
2306                &audit_tx,
2307                &webauthn,
2308                &pw_badlist_cache,
2309            ) {
2310                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2311                _ => panic!(),
2312            };
2313            match session.validate_creds(
2314                &AuthCredential::Password(pw_badlist.to_string()),
2315                ts,
2316                &async_tx,
2317                &audit_tx,
2318                &webauthn,
2319                &pw_badlist_cache,
2320            ) {
2321                Ok(AuthState::Denied(msg)) => assert_eq!(msg, PW_BADLIST_MSG),
2322                _ => panic!(),
2323            };
2324
2325            match audit_rx.try_recv() {
2326                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2327                _ => panic!("Oh no"),
2328            }
2329        }
2330
2331        drop(async_tx);
2332        assert!(async_rx.blocking_recv().is_none());
2333        drop(audit_tx);
2334        assert!(audit_rx.blocking_recv().is_none());
2335    }
2336
2337    macro_rules! start_webauthn_only_session {
2338        (
2339            $audit:expr,
2340            $account:expr,
2341            $webauthn:expr
2342        ) => {{
2343            let asd = AuthSessionData {
2344                account: $account.clone(),
2345                account_policy: ResolvedAccountPolicy::default(),
2346                issue: AuthIssueSession::Token,
2347                webauthn: $webauthn,
2348                ct: duration_from_epoch_now(),
2349                client_auth_info: Source::Internal.into(),
2350            };
2351            let key_object = KeyObjectInternal::new_test();
2352            let (session, state) = AuthSession::new(asd, false, key_object);
2353            let mut session = session.unwrap();
2354
2355            if let AuthState::Choose(auth_mechs) = state {
2356                assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Passkey)));
2357            } else {
2358                panic!();
2359            }
2360
2361            let state = session
2362                .start_session(&AuthMech::Passkey)
2363                .expect("Failed to select Passkey mech.");
2364
2365            let wan_chal = if let AuthState::Continue(auth_mechs) = state {
2366                assert_eq!(auth_mechs.len(), 1);
2367                auth_mechs
2368                    .into_iter()
2369                    .fold(None, |_acc, x| match x {
2370                        AuthAllowed::Passkey(chal) => Some(chal),
2371                        _ => None,
2372                    })
2373                    .expect("No securitykey challenge found.")
2374            } else {
2375                panic!();
2376            };
2377
2378            (session, wan_chal)
2379        }};
2380    }
2381
2382    fn setup_webauthn_passkey(
2383        name: &str,
2384    ) -> (
2385        webauthn_rs::prelude::Webauthn,
2386        webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2387        webauthn_rs::prelude::Passkey,
2388    ) {
2389        let webauthn = create_webauthn();
2390        // Setup a soft token
2391        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2392
2393        let uuid = Uuid::new_v4();
2394
2395        let (chal, reg_state) = webauthn
2396            .start_passkey_registration(uuid, name, name, None)
2397            .expect("Failed to setup passkey rego challenge");
2398
2399        let r = wa
2400            .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2401            .expect("Failed to create soft passkey");
2402
2403        let wan_cred = webauthn
2404            .finish_passkey_registration(&r, &reg_state)
2405            .expect("Failed to register soft token");
2406
2407        (webauthn, wa, wan_cred)
2408    }
2409
2410    fn setup_webauthn_securitykey(
2411        name: &str,
2412    ) -> (
2413        webauthn_rs::prelude::Webauthn,
2414        webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
2415        webauthn_rs::prelude::SecurityKey,
2416    ) {
2417        let webauthn = create_webauthn();
2418        // Setup a soft token
2419        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2420
2421        let uuid = Uuid::new_v4();
2422
2423        let (chal, reg_state) = webauthn
2424            .start_securitykey_registration(uuid, name, name, None, None, None)
2425            .expect("Failed to setup passkey rego challenge");
2426
2427        let r = wa
2428            .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2429            .expect("Failed to create soft securitykey");
2430
2431        let wan_cred = webauthn
2432            .finish_securitykey_registration(&r, &reg_state)
2433            .expect("Failed to register soft token");
2434
2435        (webauthn, wa, wan_cred)
2436    }
2437
2438    #[test]
2439    fn test_idm_authsession_webauthn_only_mech() {
2440        sketching::test_init();
2441        let (async_tx, mut async_rx) = unbounded();
2442        let (audit_tx, mut audit_rx) = unbounded();
2443        let ts = duration_from_epoch_now();
2444        // create the ent
2445        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2446
2447        let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
2448
2449        // Now create the credential for the account.
2450        account.passkeys = btreemap![(Uuid::new_v4(), ("soft".to_string(), wan_cred))];
2451
2452        // now check correct mech was offered.
2453
2454        // check send anon (fail)
2455        {
2456            let (mut session, _inv_chal) =
2457                start_webauthn_only_session!(&mut audit, account, &webauthn);
2458
2459            match session.validate_creds(
2460                &AuthCredential::Anonymous,
2461                ts,
2462                &async_tx,
2463                &audit_tx,
2464                &webauthn,
2465                &Default::default(),
2466            ) {
2467                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2468                _ => panic!(),
2469            };
2470
2471            match audit_rx.try_recv() {
2472                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2473                _ => panic!("Oh no"),
2474            }
2475        }
2476
2477        // Check good challenge
2478        {
2479            let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2480
2481            let resp = wa
2482                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2483                .map(Box::new)
2484                .expect("failed to use softtoken to authenticate");
2485
2486            match session.validate_creds(
2487                &AuthCredential::Passkey(resp),
2488                ts,
2489                &async_tx,
2490                &audit_tx,
2491                &webauthn,
2492                &Default::default(),
2493            ) {
2494                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2495                _ => panic!(),
2496            };
2497
2498            // Check the async counter update was sent.
2499            match async_rx.blocking_recv() {
2500                Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2501                _ => panic!("Oh no"),
2502            }
2503            match async_rx.blocking_recv() {
2504                Some(DelayedAction::AuthSessionRecord(_)) => {}
2505                _ => panic!("Oh no"),
2506            }
2507        }
2508
2509        // Check bad challenge.
2510        {
2511            let (_session, inv_chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2512            let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2513
2514            let resp = wa
2515                // HERE -> we use inv_chal instead.
2516                .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2517                .map(Box::new)
2518                .expect("failed to use softtoken to authenticate");
2519
2520            match session.validate_creds(
2521                &AuthCredential::Passkey(resp),
2522                ts,
2523                &async_tx,
2524                &audit_tx,
2525                &webauthn,
2526                &Default::default(),
2527            ) {
2528                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2529                _ => panic!(),
2530            };
2531
2532            match audit_rx.try_recv() {
2533                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2534                _ => panic!("Oh no"),
2535            }
2536        }
2537
2538        // Use an incorrect softtoken.
2539        {
2540            let mut inv_wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
2541            let (chal, reg_state) = webauthn
2542                .start_passkey_registration(account.uuid, &account.name, &account.displayname, None)
2543                .expect("Failed to setup webauthn rego challenge");
2544
2545            let r = inv_wa
2546                .do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
2547                .expect("Failed to create soft token");
2548
2549            let inv_cred = webauthn
2550                .finish_passkey_registration(&r, &reg_state)
2551                .expect("Failed to register soft token");
2552
2553            // Discard the auth_state, we only need the invalid challenge.
2554            let (chal, _auth_state) = webauthn
2555                .start_passkey_authentication(&vec![inv_cred])
2556                .expect("Failed to generate challenge for in inv softtoken");
2557
2558            // Create the response.
2559            let resp = inv_wa
2560                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2561                .map(Box::new)
2562                .expect("Failed to use softtoken for response.");
2563
2564            let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
2565            // Ignore the real cred, use the diff cred. Normally this shouldn't even
2566            // get this far, because the client should identify that the cred id's are
2567            // not inline.
2568            match session.validate_creds(
2569                &AuthCredential::Passkey(resp),
2570                ts,
2571                &async_tx,
2572                &audit_tx,
2573                &webauthn,
2574                &Default::default(),
2575            ) {
2576                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2577                _ => panic!(),
2578            };
2579
2580            match audit_rx.try_recv() {
2581                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2582                _ => panic!("Oh no"),
2583            }
2584        }
2585
2586        drop(async_tx);
2587        assert!(async_rx.blocking_recv().is_none());
2588        drop(audit_tx);
2589        assert!(audit_rx.blocking_recv().is_none());
2590    }
2591
2592    #[test]
2593    fn test_idm_authsession_webauthn_password_mech() {
2594        sketching::test_init();
2595        let (async_tx, mut async_rx) = unbounded();
2596        let (audit_tx, mut audit_rx) = unbounded();
2597        let ts = duration_from_epoch_now();
2598        // create the ent
2599        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2600
2601        let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2602        let pw_good = "test_password";
2603        let pw_bad = "bad_password";
2604
2605        // Now create the credential for the account.
2606        let p = CryptoPolicy::minimum();
2607        let cred = Credential::new_password_only(&p, pw_good)
2608            .unwrap()
2609            .append_securitykey("soft".to_string(), wan_cred)
2610            .unwrap();
2611
2612        account.primary = Some(cred);
2613
2614        // check pw first (fail)
2615        {
2616            let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2617
2618            match session.validate_creds(
2619                &AuthCredential::Password(pw_bad.to_string()),
2620                ts,
2621                &async_tx,
2622                &audit_tx,
2623                &webauthn,
2624                &pw_badlist_cache,
2625            ) {
2626                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2627                _ => panic!(),
2628            };
2629
2630            match audit_rx.try_recv() {
2631                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2632                _ => panic!("Oh no"),
2633            }
2634        }
2635
2636        // Check totp first attempt fails.
2637        {
2638            let (mut session, _, pw_badlist_cache) = start_password_sk_session(&account, &webauthn);
2639
2640            match session.validate_creds(
2641                &AuthCredential::Totp(0),
2642                ts,
2643                &async_tx,
2644                &audit_tx,
2645                &webauthn,
2646                &pw_badlist_cache,
2647            ) {
2648                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2649                _ => panic!(),
2650            };
2651
2652            match audit_rx.try_recv() {
2653                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2654                _ => panic!("Oh no"),
2655            }
2656        }
2657
2658        // check bad webauthn (fail)
2659        // NOTE: We only check bad challenge here as bad softtoken is already
2660        // extensively tested.
2661        {
2662            let (_session, inv_chal, pw_badlist_cache) =
2663                start_password_sk_session(&account, &webauthn);
2664            let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2665
2666            let resp = wa
2667                // HERE -> we use inv_chal instead.
2668                .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2669                .map(Box::new)
2670                .expect("failed to use softtoken to authenticate");
2671
2672            match session.validate_creds(
2673                &AuthCredential::SecurityKey(resp),
2674                ts,
2675                &async_tx,
2676                &audit_tx,
2677                &webauthn,
2678                &pw_badlist_cache,
2679            ) {
2680                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2681                _ => panic!(),
2682            };
2683
2684            match audit_rx.try_recv() {
2685                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2686                _ => panic!("Oh no"),
2687            }
2688        }
2689
2690        // check good webauthn/bad pw (fail)
2691        {
2692            let (mut session, chal, pw_badlist_cache) =
2693                start_password_sk_session(&account, &webauthn);
2694
2695            let resp = wa
2696                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2697                .map(Box::new)
2698                .expect("failed to use softtoken to authenticate");
2699
2700            match session.validate_creds(
2701                &AuthCredential::SecurityKey(resp),
2702                ts,
2703                &async_tx,
2704                &audit_tx,
2705                &webauthn,
2706                &pw_badlist_cache,
2707            ) {
2708                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2709                _ => panic!(),
2710            };
2711            match session.validate_creds(
2712                &AuthCredential::Password(pw_bad.to_string()),
2713                ts,
2714                &async_tx,
2715                &audit_tx,
2716                &webauthn,
2717                &pw_badlist_cache,
2718            ) {
2719                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2720                _ => panic!(),
2721            };
2722
2723            match audit_rx.try_recv() {
2724                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2725                _ => panic!("Oh no"),
2726            }
2727
2728            // Check the async counter update was sent.
2729            match async_rx.blocking_recv() {
2730                Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2731                _ => panic!("Oh no"),
2732            }
2733        }
2734
2735        // Check good webauthn/good pw (pass)
2736        {
2737            let (mut session, chal, pw_badlist_cache) =
2738                start_password_sk_session(&account, &webauthn);
2739
2740            let resp = wa
2741                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2742                .map(Box::new)
2743                .expect("failed to use softtoken to authenticate");
2744
2745            match session.validate_creds(
2746                &AuthCredential::SecurityKey(resp),
2747                ts,
2748                &async_tx,
2749                &audit_tx,
2750                &webauthn,
2751                &pw_badlist_cache,
2752            ) {
2753                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2754                _ => panic!(),
2755            };
2756            match session.validate_creds(
2757                &AuthCredential::Password(pw_good.to_string()),
2758                ts,
2759                &async_tx,
2760                &audit_tx,
2761                &webauthn,
2762                &pw_badlist_cache,
2763            ) {
2764                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2765                _ => panic!(),
2766            };
2767
2768            // Check the async counter update was sent.
2769            match async_rx.blocking_recv() {
2770                Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2771                _ => panic!("Oh no"),
2772            }
2773            match async_rx.blocking_recv() {
2774                Some(DelayedAction::AuthSessionRecord(_)) => {}
2775                _ => panic!("Oh no"),
2776            }
2777        }
2778
2779        drop(async_tx);
2780        assert!(async_rx.blocking_recv().is_none());
2781        drop(audit_tx);
2782        assert!(audit_rx.blocking_recv().is_none());
2783    }
2784
2785    #[test]
2786    fn test_idm_authsession_webauthn_password_totp_mech() {
2787        sketching::test_init();
2788        let (async_tx, mut async_rx) = unbounded();
2789        let (audit_tx, mut audit_rx) = unbounded();
2790        let ts = duration_from_epoch_now();
2791        // create the ent
2792        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
2793
2794        let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
2795
2796        let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
2797        let totp_good = totp
2798            .do_totp_duration_from_epoch(&ts)
2799            .expect("failed to perform totp.");
2800        let totp_bad = totp
2801            .do_totp_duration_from_epoch(&Duration::from_secs(1234567))
2802            .expect("failed to perform totp.");
2803        assert!(totp_bad != totp_good);
2804
2805        let pw_good = "test_password";
2806        let pw_bad = "bad_password";
2807
2808        // Now create the credential for the account.
2809        let p = CryptoPolicy::minimum();
2810        let cred = Credential::new_password_only(&p, pw_good)
2811            .unwrap()
2812            .append_securitykey("soft".to_string(), wan_cred)
2813            .unwrap()
2814            .append_totp("totp".to_string(), totp);
2815
2816        account.primary = Some(cred);
2817
2818        // check pw first (fail)
2819        {
2820            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2821
2822            match session.validate_creds(
2823                &AuthCredential::Password(pw_bad.to_string()),
2824                ts,
2825                &async_tx,
2826                &audit_tx,
2827                &webauthn,
2828                &pw_badlist_cache,
2829            ) {
2830                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
2831                _ => panic!(),
2832            };
2833
2834            match audit_rx.try_recv() {
2835                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2836                _ => panic!("Oh no"),
2837            }
2838        }
2839
2840        // Check bad totp (fail)
2841        {
2842            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2843
2844            match session.validate_creds(
2845                &AuthCredential::Totp(totp_bad),
2846                ts,
2847                &async_tx,
2848                &audit_tx,
2849                &webauthn,
2850                &pw_badlist_cache,
2851            ) {
2852                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_TOTP_MSG),
2853                _ => panic!(),
2854            };
2855
2856            match audit_rx.try_recv() {
2857                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2858                _ => panic!("Oh no"),
2859            }
2860        }
2861
2862        // check bad webauthn (fail)
2863        {
2864            let (_session, inv_chal, pw_badlist_cache) =
2865                start_password_sk_session(&account, &webauthn);
2866            let (mut session, _chal, _) = start_password_sk_session(&account, &webauthn);
2867
2868            let resp = wa
2869                // HERE -> we use inv_chal instead.
2870                .do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
2871                .map(Box::new)
2872                .expect("failed to use softtoken to authenticate");
2873
2874            match session.validate_creds(
2875                &AuthCredential::SecurityKey(resp),
2876                ts,
2877                &async_tx,
2878                &audit_tx,
2879                &webauthn,
2880                &pw_badlist_cache,
2881            ) {
2882                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_WEBAUTHN_MSG),
2883                _ => panic!(),
2884            };
2885
2886            match audit_rx.try_recv() {
2887                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2888                _ => panic!("Oh no"),
2889            }
2890        }
2891
2892        // check good webauthn/bad pw (fail)
2893        {
2894            let (mut session, chal, pw_badlist_cache) =
2895                start_password_sk_session(&account, &webauthn);
2896
2897            let resp = wa
2898                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
2899                .map(Box::new)
2900                .expect("failed to use softtoken to authenticate");
2901
2902            match session.validate_creds(
2903                &AuthCredential::SecurityKey(resp),
2904                ts,
2905                &async_tx,
2906                &audit_tx,
2907                &webauthn,
2908                &pw_badlist_cache,
2909            ) {
2910                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2911                _ => panic!(),
2912            };
2913            match session.validate_creds(
2914                &AuthCredential::Password(pw_bad.to_string()),
2915                ts,
2916                &async_tx,
2917                &audit_tx,
2918                &webauthn,
2919                &pw_badlist_cache,
2920            ) {
2921                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2922                _ => panic!(),
2923            };
2924
2925            match audit_rx.try_recv() {
2926                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2927                _ => panic!("Oh no"),
2928            }
2929
2930            // Check the async counter update was sent.
2931            match async_rx.blocking_recv() {
2932                Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
2933                _ => panic!("Oh no"),
2934            }
2935        }
2936
2937        // check good totp/bad pw (fail)
2938        {
2939            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2940
2941            match session.validate_creds(
2942                &AuthCredential::Totp(totp_good),
2943                ts,
2944                &async_tx,
2945                &audit_tx,
2946                &webauthn,
2947                &pw_badlist_cache,
2948            ) {
2949                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2950                _ => panic!(),
2951            };
2952            match session.validate_creds(
2953                &AuthCredential::Password(pw_bad.to_string()),
2954                ts,
2955                &async_tx,
2956                &audit_tx,
2957                &webauthn,
2958                &pw_badlist_cache,
2959            ) {
2960                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
2961                _ => panic!(),
2962            };
2963
2964            match audit_rx.try_recv() {
2965                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
2966                _ => panic!("Oh no"),
2967            }
2968        }
2969
2970        // check good totp/good pw (pass)
2971        {
2972            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
2973
2974            match session.validate_creds(
2975                &AuthCredential::Totp(totp_good),
2976                ts,
2977                &async_tx,
2978                &audit_tx,
2979                &webauthn,
2980                &pw_badlist_cache,
2981            ) {
2982                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
2983                _ => panic!(),
2984            };
2985            match session.validate_creds(
2986                &AuthCredential::Password(pw_good.to_string()),
2987                ts,
2988                &async_tx,
2989                &audit_tx,
2990                &webauthn,
2991                &pw_badlist_cache,
2992            ) {
2993                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
2994                _ => panic!(),
2995            };
2996
2997            match async_rx.blocking_recv() {
2998                Some(DelayedAction::AuthSessionRecord(_)) => {}
2999                _ => panic!("Oh no"),
3000            }
3001        }
3002
3003        // Check good webauthn/good pw (pass)
3004        {
3005            let (mut session, chal, pw_badlist_cache) =
3006                start_password_sk_session(&account, &webauthn);
3007
3008            let resp = wa
3009                .do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
3010                .map(Box::new)
3011                .expect("failed to use softtoken to authenticate");
3012
3013            match session.validate_creds(
3014                &AuthCredential::SecurityKey(resp),
3015                ts,
3016                &async_tx,
3017                &audit_tx,
3018                &webauthn,
3019                &pw_badlist_cache,
3020            ) {
3021                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3022                _ => panic!(),
3023            };
3024            match session.validate_creds(
3025                &AuthCredential::Password(pw_good.to_string()),
3026                ts,
3027                &async_tx,
3028                &audit_tx,
3029                &webauthn,
3030                &pw_badlist_cache,
3031            ) {
3032                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3033                _ => panic!(),
3034            };
3035
3036            // Check the async counter update was sent.
3037            match async_rx.blocking_recv() {
3038                Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
3039                _ => panic!("Oh no"),
3040            }
3041            match async_rx.blocking_recv() {
3042                Some(DelayedAction::AuthSessionRecord(_)) => {}
3043                _ => panic!("Oh no"),
3044            }
3045        }
3046
3047        drop(async_tx);
3048        assert!(async_rx.blocking_recv().is_none());
3049        drop(audit_tx);
3050        assert!(audit_rx.blocking_recv().is_none());
3051    }
3052
3053    #[test]
3054    fn test_idm_authsession_backup_code_mech() {
3055        sketching::test_init();
3056        let webauthn = create_webauthn();
3057        // create the ent
3058        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3059
3060        // Setup a fake time stamp for consistency.
3061        let ts = Duration::from_secs(12345);
3062
3063        // manually load in a cred
3064        let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
3065
3066        let totp_good = totp
3067            .do_totp_duration_from_epoch(&ts)
3068            .expect("failed to perform totp.");
3069
3070        let pw_good = "test_password";
3071        let pw_bad = "bad_password";
3072
3073        let backup_code_good = readable_password_from_random();
3074        let backup_code_bad = readable_password_from_random();
3075        assert!(backup_code_bad != backup_code_good);
3076        let mut code_set = HashSet::new();
3077        code_set.insert(backup_code_good.clone());
3078
3079        let backup_codes = BackupCodes::new(code_set);
3080
3081        // add totp and backup codes also
3082        let p = CryptoPolicy::minimum();
3083        let cred = Credential::new_password_only(&p, pw_good)
3084            .unwrap()
3085            .append_totp("totp".to_string(), totp)
3086            .update_backup_code(backup_codes)
3087            .unwrap();
3088
3089        account.primary = Some(cred);
3090
3091        let (async_tx, mut async_rx) = unbounded();
3092        let (audit_tx, mut audit_rx) = unbounded();
3093
3094        // now check
3095        // == two step checks
3096
3097        // Sending a PW first is an immediate fail.
3098        {
3099            let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3100
3101            match session.validate_creds(
3102                &AuthCredential::Password(pw_bad.to_string()),
3103                ts,
3104                &async_tx,
3105                &audit_tx,
3106                &webauthn,
3107                &pw_badlist_cache,
3108            ) {
3109                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_AUTH_TYPE_MSG),
3110                _ => panic!(),
3111            };
3112
3113            match audit_rx.try_recv() {
3114                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3115                _ => panic!("Oh no"),
3116            }
3117        }
3118        // check send wrong backup code, should fail immediate
3119        {
3120            let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3121
3122            match session.validate_creds(
3123                &AuthCredential::BackupCode(backup_code_bad),
3124                ts,
3125                &async_tx,
3126                &audit_tx,
3127                &webauthn,
3128                &pw_badlist_cache,
3129            ) {
3130                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_BACKUPCODE_MSG),
3131                _ => panic!(),
3132            };
3133
3134            match audit_rx.try_recv() {
3135                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3136                _ => panic!("Oh no"),
3137            }
3138        }
3139        // check send good backup code, should continue
3140        //      then bad pw, fail pw
3141        {
3142            let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3143
3144            match session.validate_creds(
3145                &AuthCredential::BackupCode(backup_code_good.clone()),
3146                ts,
3147                &async_tx,
3148                &audit_tx,
3149                &webauthn,
3150                &pw_badlist_cache,
3151            ) {
3152                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3153                _ => panic!(),
3154            };
3155            match session.validate_creds(
3156                &AuthCredential::Password(pw_bad.to_string()),
3157                ts,
3158                &async_tx,
3159                &audit_tx,
3160                &webauthn,
3161                &pw_badlist_cache,
3162            ) {
3163                Ok(AuthState::Denied(msg)) => assert_eq!(msg, BAD_PASSWORD_MSG),
3164                _ => panic!(),
3165            };
3166
3167            match audit_rx.try_recv() {
3168                Ok(AuditEvent::AuthenticationDenied { .. }) => {}
3169                _ => panic!("Oh no"),
3170            }
3171        }
3172        // Can't process BackupCodeRemoval without the server instance
3173        match async_rx.blocking_recv() {
3174            Some(DelayedAction::BackupCodeRemoval(_)) => {}
3175            _ => panic!("Oh no"),
3176        }
3177
3178        // check send good backup code, should continue
3179        //      then good pw, success
3180        {
3181            let (mut session, pw_badlist_cache) = start_password_bc_session(&account, &webauthn);
3182
3183            match session.validate_creds(
3184                &AuthCredential::BackupCode(backup_code_good),
3185                ts,
3186                &async_tx,
3187                &audit_tx,
3188                &webauthn,
3189                &pw_badlist_cache,
3190            ) {
3191                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3192                _ => panic!(),
3193            };
3194            match session.validate_creds(
3195                &AuthCredential::Password(pw_good.to_string()),
3196                ts,
3197                &async_tx,
3198                &audit_tx,
3199                &webauthn,
3200                &pw_badlist_cache,
3201            ) {
3202                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3203                _ => panic!(),
3204            };
3205        }
3206        // Can't process BackupCodeRemoval without the server instance
3207        match async_rx.blocking_recv() {
3208            Some(DelayedAction::BackupCodeRemoval(_)) => {}
3209            _ => panic!("Oh no"),
3210        }
3211
3212        // There will be a auth session record too
3213        match async_rx.blocking_recv() {
3214            Some(DelayedAction::AuthSessionRecord(_)) => {}
3215            _ => panic!("Oh no"),
3216        }
3217
3218        // TOTP should also work:
3219        // check send good TOTP, should continue
3220        //      then good pw, success
3221        {
3222            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3223
3224            match session.validate_creds(
3225                &AuthCredential::Totp(totp_good),
3226                ts,
3227                &async_tx,
3228                &audit_tx,
3229                &webauthn,
3230                &pw_badlist_cache,
3231            ) {
3232                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3233                _ => panic!(),
3234            };
3235            match session.validate_creds(
3236                &AuthCredential::Password(pw_good.to_string()),
3237                ts,
3238                &async_tx,
3239                &audit_tx,
3240                &webauthn,
3241                &pw_badlist_cache,
3242            ) {
3243                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3244                _ => panic!(),
3245            };
3246        }
3247
3248        // There will be a auth session record too
3249        match async_rx.blocking_recv() {
3250            Some(DelayedAction::AuthSessionRecord(_)) => {}
3251            _ => panic!("Oh no"),
3252        }
3253
3254        drop(async_tx);
3255        assert!(async_rx.blocking_recv().is_none());
3256        drop(audit_tx);
3257        assert!(audit_rx.blocking_recv().is_none());
3258    }
3259
3260    #[test]
3261    fn test_idm_authsession_multiple_totp_password_mech() {
3262        // Slightly different to the other TOTP test, this
3263        // checks handling when multiple TOTP's are registered.
3264        sketching::test_init();
3265        let webauthn = create_webauthn();
3266        // create the ent
3267        let mut account: Account = BUILTIN_ACCOUNT_TEST_PERSON.clone().into();
3268
3269        // Setup a fake time stamp for consistency.
3270        let ts = Duration::from_secs(12345);
3271
3272        // manually load in a cred
3273        let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
3274        let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
3275
3276        let totp_good_a = totp_a
3277            .do_totp_duration_from_epoch(&ts)
3278            .expect("failed to perform totp.");
3279
3280        let totp_good_b = totp_b
3281            .do_totp_duration_from_epoch(&ts)
3282            .expect("failed to perform totp.");
3283
3284        assert!(totp_good_a != totp_good_b);
3285
3286        let pw_good = "test_password";
3287
3288        let p = CryptoPolicy::minimum();
3289        let cred = Credential::new_password_only(&p, pw_good)
3290            .unwrap()
3291            .append_totp("totp_a".to_string(), totp_a)
3292            .append_totp("totp_b".to_string(), totp_b);
3293        // add totp also
3294        account.primary = Some(cred);
3295
3296        let (async_tx, mut async_rx) = unbounded();
3297        let (audit_tx, mut audit_rx) = unbounded();
3298
3299        // Test totp_a
3300        {
3301            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3302
3303            match session.validate_creds(
3304                &AuthCredential::Totp(totp_good_a),
3305                ts,
3306                &async_tx,
3307                &audit_tx,
3308                &webauthn,
3309                &pw_badlist_cache,
3310            ) {
3311                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3312                _ => panic!(),
3313            };
3314            match session.validate_creds(
3315                &AuthCredential::Password(pw_good.to_string()),
3316                ts,
3317                &async_tx,
3318                &audit_tx,
3319                &webauthn,
3320                &pw_badlist_cache,
3321            ) {
3322                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3323                _ => panic!(),
3324            };
3325
3326            match async_rx.blocking_recv() {
3327                Some(DelayedAction::AuthSessionRecord(_)) => {}
3328                _ => panic!("Oh no"),
3329            }
3330        }
3331
3332        // Test totp_b
3333        {
3334            let (mut session, pw_badlist_cache) = start_password_totp_session(&account, &webauthn);
3335
3336            match session.validate_creds(
3337                &AuthCredential::Totp(totp_good_b),
3338                ts,
3339                &async_tx,
3340                &audit_tx,
3341                &webauthn,
3342                &pw_badlist_cache,
3343            ) {
3344                Ok(AuthState::Continue(cont)) => assert_eq!(cont, vec![AuthAllowed::Password]),
3345                _ => panic!(),
3346            };
3347            match session.validate_creds(
3348                &AuthCredential::Password(pw_good.to_string()),
3349                ts,
3350                &async_tx,
3351                &audit_tx,
3352                &webauthn,
3353                &pw_badlist_cache,
3354            ) {
3355                Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
3356                _ => panic!(),
3357            };
3358
3359            match async_rx.blocking_recv() {
3360                Some(DelayedAction::AuthSessionRecord(_)) => {}
3361                _ => panic!("Oh no"),
3362            }
3363        }
3364
3365        drop(async_tx);
3366        assert!(async_rx.blocking_recv().is_none());
3367        drop(audit_tx);
3368        assert!(audit_rx.blocking_recv().is_none());
3369    }
3370}