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