Skip to main content

kanidmd_lib/idm/
reauth.rs

1use crate::prelude::*;
2
3use crate::credential::softlock::CredSoftLock;
4use crate::idm::account::Account;
5use crate::idm::authentication::{AuthState, ReauthRequest};
6use crate::idm::authsession::{AuthSession, AuthSessionData};
7use crate::idm::event::AuthResult;
8use crate::idm::server::IdmServerAuthTransaction;
9use crate::utils::uuid_from_duration;
10
11// use webauthn_rs::prelude::Webauthn;
12
13use std::sync::Arc;
14use tokio::sync::Mutex;
15
16use kanidm_proto::v1::AuthIssueSession;
17
18use super::server::CredSoftLockMutex;
19
20impl IdmServerAuthTransaction<'_> {
21    pub async fn reauth_init(
22        &mut self,
23        ident: Identity,
24        issue: AuthIssueSession,
25        ct: Duration,
26        client_auth_info: ClientAuthInfo,
27        reauth_req: ReauthRequest,
28    ) -> Result<AuthResult, OperationError> {
29        // re-auth only works on users, so lets get the user account.
30        // hint - it's in the ident!
31        let Some(entry) = ident.get_user_entry() else {
32            error!("Ident is not a user and has no entry associated. Unable to proceed.");
33            return Err(OperationError::InvalidState);
34        };
35
36        // Setup the account record.
37        let (account, account_policy) =
38            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;
39
40        security_info!(
41            spn = %account.spn(),
42            issue = ?issue,
43            uuid = %account.uuid,
44            "Initiating Re-Authentication Session",
45        );
46
47        // Check that the entry/session can be re-authed.
48        let session = entry
49            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
50            .and_then(|sessions| sessions.get(&ident.session_id))
51            .ok_or_else(|| {
52                error!("Ident session is not present in entry. Perhaps replication is delayed?");
53                OperationError::InvalidState
54            })?;
55
56        match session.scope {
57            SessionScope::PrivilegeCapable => {
58                // Yes! This session can re-auth!
59            }
60            SessionScope::ReadOnly | SessionScope::ReadWrite | SessionScope::Synchronise => {
61                // These can not!
62                error!("Session scope is not PrivilegeCapable and can not be used in re-auth.");
63                return Err(OperationError::SessionMayNotReauth);
64            }
65        };
66
67        // Get the credential id.
68        let session_cred_id = session.cred_id;
69
70        // == Everything Checked Out! ==
71        // Let's setup to proceed with the re-auth.
72
73        // Allocate the *authentication* session id based on current time / sid.
74        let sessionid = uuid_from_duration(ct, self.sid);
75
76        // Start getting things.
77        let _session_ticket = self.session_ticket.acquire().await;
78
79        // Setup soft locks here if required.
80        let maybe_slock = account
81            .primary_cred_uuid_and_policy()
82            .and_then(|(cred_uuid, policy)| {
83                // Acquire the softlock map
84                //
85                // We have no issue calling this with .write here, since we
86                // already hold the session_ticket above.
87                //
88                // We only do this if the primary credential being used here is for
89                // this re-auth session. Else passkeys/devicekeys are not bounded by this
90                // problem.
91                if cred_uuid == session_cred_id {
92                    let mut softlock_write = self.softlocks.write();
93                    let slock_ref: CredSoftLockMutex =
94                        if let Some(slock_ref) = softlock_write.get(&cred_uuid) {
95                            slock_ref.clone()
96                        } else {
97                            // Create if not exist, and the cred type supports softlocking.
98                            let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
99                            softlock_write.insert(cred_uuid, slock.clone());
100                            slock
101                        };
102                    softlock_write.commit();
103                    Some(slock_ref)
104                } else {
105                    None
106                }
107            });
108
109        // Check if the cred is locked! We want to fail fast here! Unlike the auth flow we have
110        // already selected our credential so we can test it's slock, else we could be allowing
111        // 1-attempt per-reauth.
112
113        let is_valid = if let Some(slock_ref) = maybe_slock {
114            let mut slock = slock_ref.lock().await;
115            slock.apply_time_step(ct, None);
116            slock.is_valid()
117        } else {
118            true
119        };
120
121        if !is_valid {
122            warn!(
123                "Credential {:?} is currently softlocked, unable to proceed",
124                session_cred_id
125            );
126            return Ok(AuthResult {
127                sessionid: ident.get_session_id(),
128                state: AuthState::Denied("Credential is temporarily locked".to_string()),
129            });
130        }
131
132        // Create a re-auth session
133        let asd: AuthSessionData = AuthSessionData {
134            account,
135            account_policy,
136            issue,
137            webauthn: self.webauthn,
138            ct,
139            client_auth_info,
140            oauth2_client_provider: None,
141        };
142
143        let domain_keys = self.qs_read.get_domain_key_object_handle()?;
144
145        let (auth_session, state) = AuthSession::new_reauth(
146            asd,
147            ident.session_id,
148            session,
149            session_cred_id,
150            domain_keys,
151            &reauth_req,
152        );
153
154        // Push the re-auth session to the session maps.
155        match auth_session {
156            Some(auth_session) => {
157                let mut session_write = self.sessions.write();
158                if session_write.contains_key(&sessionid) {
159                    // If we have a session of the same id, return an error (despite how
160                    // unlikely this is ...
161                    Err(OperationError::InvalidSessionState)
162                } else {
163                    session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
164                    // Debugging: ensure we really inserted ...
165                    debug_assert!(session_write.get(&sessionid).is_some());
166                    Ok(())
167                }?;
168                session_write.commit();
169            }
170            None => {
171                security_info!("Authentication Session Unable to begin");
172            }
173        };
174
175        Ok(AuthResult { sessionid, state })
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use crate::credential::totp::Totp;
182    use crate::idm::audit::AuditEvent;
183    use crate::idm::authentication::{AuthState, ReauthRequest};
184    use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus};
185    use crate::idm::delayed::DelayedAction;
186    use crate::idm::event::{AuthEvent, AuthResult};
187    use crate::idm::server::IdmServerTransaction;
188    use crate::prelude::*;
189    use compact_jwt::JwsCompact;
190    use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
191    use uuid::uuid;
192    use webauthn_authenticator_rs::softpasskey::SoftPasskey;
193    use webauthn_authenticator_rs::WebauthnAuthenticator;
194
195    const TESTPERSON_UUID: Uuid = uuid!("cf231fea-1a8f-4410-a520-fd9b1a379c86");
196
197    async fn setup_testaccount(idms: &IdmServer, ct: Duration) {
198        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
199
200        let e2 = entry_init!(
201            (Attribute::Class, EntryClass::Object.to_value()),
202            (Attribute::Class, EntryClass::Account.to_value()),
203            (Attribute::Class, EntryClass::Person.to_value()),
204            (Attribute::Name, Value::new_iname("testperson")),
205            (Attribute::Uuid, Value::Uuid(TESTPERSON_UUID)),
206            (Attribute::Description, Value::new_utf8s("testperson")),
207            (Attribute::DisplayName, Value::new_utf8s("testperson"))
208        );
209
210        let cr = idms_prox_write.qs_write.internal_create(vec![e2]);
211        assert!(cr.is_ok());
212        assert!(idms_prox_write.commit().is_ok());
213    }
214
215    async fn setup_testaccount_passkey(idms: &IdmServer, ct: Duration) -> SoftPasskey {
216        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
217        let testperson = idms_prox_write
218            .qs_write
219            .internal_search_uuid(TESTPERSON_UUID)
220            .expect("failed");
221        let (cust, _c_status) = idms_prox_write
222            .init_credential_update(
223                &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
224                ct,
225            )
226            .expect("Failed to begin credential update.");
227        idms_prox_write.commit().expect("Failed to commit txn");
228
229        // Update session is setup.
230
231        let cutxn = idms.cred_update_transaction().await.unwrap();
232        let origin = cutxn.get_origin().clone();
233
234        let mut wa = SoftPasskey::new(true);
235
236        let c_status = cutxn
237            .credential_passkey_init(&cust, ct)
238            .expect("Failed to initiate passkey registration");
239
240        let passkey_chal = match c_status.mfaregstate() {
241            MfaRegStateStatus::Passkey(c) => Some(c),
242            _ => None,
243        }
244        .expect("Unable to access passkey challenge, invalid state");
245
246        let passkey_resp = wa
247            .do_registration(origin.clone(), passkey_chal.clone())
248            .expect("Failed to create soft passkey");
249
250        // Finish the registration
251        let label = "softtoken".to_string();
252        let c_status = cutxn
253            .credential_passkey_finish(&cust, ct, label, &passkey_resp)
254            .expect("Failed to initiate passkey registration");
255
256        assert!(c_status.can_commit());
257
258        drop(cutxn);
259        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
260
261        idms_prox_write
262            .commit_credential_update(&cust, ct)
263            .expect("Failed to commit credential update.");
264
265        idms_prox_write.commit().expect("Failed to commit txn");
266
267        wa
268    }
269
270    async fn setup_testaccount_password_totp(idms: &IdmServer, ct: Duration) -> (String, Totp) {
271        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
272        let testperson = idms_prox_write
273            .qs_write
274            .internal_search_uuid(TESTPERSON_UUID)
275            .expect("failed");
276        let (cust, _c_status) = idms_prox_write
277            .init_credential_update(
278                &InitCredentialUpdateEvent::new_impersonate_entry(testperson),
279                ct,
280            )
281            .expect("Failed to begin credential update.");
282        idms_prox_write.commit().expect("Failed to commit txn");
283
284        let cutxn = idms.cred_update_transaction().await.unwrap();
285
286        let pw = crate::utils::password_from_random();
287
288        let _c_status = cutxn
289            .credential_primary_set_password(&cust, ct, &pw)
290            .expect("Failed to update the primary cred password");
291
292        let c_status = cutxn
293            .credential_primary_init_totp(&cust, ct)
294            .expect("Failed to update the primary cred password");
295
296        // Check the status has the token.
297        let totp_token: Totp = match c_status.mfaregstate() {
298            MfaRegStateStatus::TotpCheck(secret) => Some(secret.clone().try_into().unwrap()),
299
300            _ => None,
301        }
302        .expect("Unable to retrieve totp token, invalid state.");
303
304        trace!(?totp_token);
305        let chal = totp_token
306            .do_totp_duration_from_epoch(&ct)
307            .expect("Failed to perform totp step");
308
309        let c_status = cutxn
310            .credential_primary_check_totp(&cust, ct, chal, "totp")
311            .expect("Failed to update the primary cred password");
312
313        assert!(matches!(c_status.mfaregstate(), MfaRegStateStatus::None));
314        assert!(c_status.can_commit());
315
316        drop(cutxn);
317        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
318
319        idms_prox_write
320            .commit_credential_update(&cust, ct)
321            .expect("Failed to commit credential update.");
322
323        idms_prox_write.commit().expect("Failed to commit txn");
324
325        (pw, totp_token)
326    }
327
328    async fn auth_passkey(
329        idms: &IdmServer,
330        ct: Duration,
331        wa: &mut SoftPasskey,
332        idms_delayed: &mut IdmServerDelayed,
333    ) -> Option<JwsCompact> {
334        let mut idms_auth = idms.auth().await.unwrap();
335        let origin = idms_auth.get_origin().clone();
336
337        let auth_init = AuthEvent::named_init("testperson");
338
339        let r1 = idms_auth
340            .auth(&auth_init, ct, Source::Internal.into())
341            .await;
342        let ar = r1.unwrap();
343        let AuthResult { sessionid, state } = ar;
344
345        if !matches!(state, AuthState::Choose(_)) {
346            debug!("Can't proceed - {:?}", state);
347            return None;
348        };
349
350        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
351
352        let r2 = idms_auth
353            .auth(&auth_begin, ct, Source::Internal.into())
354            .await;
355        let ar = r2.unwrap();
356        let AuthResult { sessionid, state } = ar;
357
358        trace!(?state);
359
360        let rcr = match state {
361            AuthState::Continue(mut allowed) => match allowed.pop() {
362                Some(AuthAllowed::Passkey(rcr)) => rcr,
363                _ => unreachable!(),
364            },
365            _ => unreachable!(),
366        };
367
368        trace!(?rcr);
369
370        let resp = wa
371            .do_authentication(origin, rcr)
372            .expect("failed to use softtoken to authenticate");
373
374        let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
375
376        let r3 = idms_auth
377            .auth(&passkey_step, ct, Source::Internal.into())
378            .await;
379        debug!("r3 ==> {:?}", r3);
380        idms_auth.commit().expect("Must not fail");
381
382        match r3 {
383            Ok(AuthResult {
384                sessionid: _,
385                state: AuthState::Success(token, AuthIssueSession::Token),
386            }) => {
387                // Process the webauthn update
388                let da = idms_delayed.try_recv().expect("invalid");
389                assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
390                let r = idms.delayed_action(ct, da).await;
391                assert!(r.is_ok());
392
393                // Process the auth session
394                let da = idms_delayed.try_recv().expect("invalid");
395                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
396                // We have to actually write this one else the following tests
397                // won't work!
398                let r = idms.delayed_action(ct, da).await;
399                assert!(r.is_ok());
400
401                Some(*token)
402            }
403            _ => None,
404        }
405    }
406
407    async fn auth_password_totp(
408        idms: &IdmServer,
409        ct: Duration,
410        pw: &str,
411        token: &Totp,
412        idms_delayed: &mut IdmServerDelayed,
413    ) -> Option<JwsCompact> {
414        let mut idms_auth = idms.auth().await.unwrap();
415
416        let auth_init = AuthEvent::named_init("testperson");
417
418        let r1 = idms_auth
419            .auth(&auth_init, ct, Source::Internal.into())
420            .await;
421        let ar = r1.unwrap();
422        let AuthResult { sessionid, state } = ar;
423
424        if !matches!(state, AuthState::Choose(_)) {
425            debug!("Can't proceed - {:?}", state);
426            return None;
427        };
428
429        let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordTotp);
430
431        let r2 = idms_auth
432            .auth(&auth_begin, ct, Source::Internal.into())
433            .await;
434        let ar = r2.unwrap();
435        let AuthResult { sessionid, state } = ar;
436
437        assert!(matches!(state, AuthState::Continue(_)));
438
439        let totp = token
440            .do_totp_duration_from_epoch(&ct)
441            .expect("Failed to perform totp step");
442
443        let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
444        let r2 = idms_auth
445            .auth(&totp_step, ct, Source::Internal.into())
446            .await;
447        let ar = r2.unwrap();
448        let AuthResult { sessionid, state } = ar;
449
450        assert!(matches!(state, AuthState::Continue(_)));
451
452        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
453
454        // Expect success
455        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
456        debug!("r3 ==> {:?}", r3);
457        idms_auth.commit().expect("Must not fail");
458
459        match r3 {
460            Ok(AuthResult {
461                sessionid: _,
462                state: AuthState::Success(token, AuthIssueSession::Token),
463            }) => {
464                // Process the auth session
465                let da = idms_delayed.try_recv().expect("invalid");
466                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
467                // We have to actually write this one else the following tests
468                // won't work!
469                let r = idms.delayed_action(ct, da).await;
470                assert!(r.is_ok());
471
472                Some(*token)
473            }
474            _ => None,
475        }
476    }
477
478    async fn token_to_ident(
479        idms: &IdmServer,
480        ct: Duration,
481        client_auth_info: ClientAuthInfo,
482    ) -> Identity {
483        let mut idms_prox_read = idms.proxy_read().await.unwrap();
484
485        idms_prox_read
486            .validate_client_auth_info_to_ident(client_auth_info, ct)
487            .expect("Invalid UAT")
488    }
489
490    async fn reauth_passkey(
491        idms: &IdmServer,
492        ct: Duration,
493        ident: &Identity,
494        wa: &mut SoftPasskey,
495        idms_delayed: &mut IdmServerDelayed,
496        reauth_req: ReauthRequest,
497    ) -> Option<JwsCompact> {
498        let mut idms_auth = idms.auth().await.unwrap();
499        let origin = idms_auth.get_origin().clone();
500
501        let auth_allowed = idms_auth
502            .reauth_init(
503                ident.clone(),
504                AuthIssueSession::Token,
505                ct,
506                Source::Internal.into(),
507                reauth_req,
508            )
509            .await
510            .expect("Failed to start reauth.");
511
512        let AuthResult { sessionid, state } = auth_allowed;
513
514        trace!(?state);
515
516        let rcr = match state {
517            AuthState::Continue(mut allowed) => match allowed.pop() {
518                Some(AuthAllowed::Passkey(rcr)) => rcr,
519                _ => return None,
520            },
521            _ => unreachable!(),
522        };
523
524        trace!(?rcr);
525
526        let resp = wa
527            .do_authentication(origin, rcr)
528            .expect("failed to use softtoken to authenticate");
529
530        let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
531
532        let r3 = idms_auth
533            .auth(&passkey_step, ct, Source::Internal.into())
534            .await;
535        debug!("r3 ==> {:?}", r3);
536        idms_auth.commit().expect("Must not fail");
537
538        match r3 {
539            Ok(AuthResult {
540                sessionid: _,
541                state: AuthState::Success(token, AuthIssueSession::Token),
542            }) => {
543                // Process the webauthn update
544                let da = idms_delayed.try_recv().expect("invalid");
545                assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
546                let r = idms.delayed_action(ct, da).await;
547                assert!(r.is_ok());
548
549                // NOTE: Unlike initial auth we don't need to check the auth session in the queue
550                // since we don't re-issue it.
551
552                Some(*token)
553            }
554            _ => unreachable!(),
555        }
556    }
557
558    async fn reauth_password_totp(
559        idms: &IdmServer,
560        ct: Duration,
561        ident: &Identity,
562        pw: &str,
563        token: &Totp,
564        idms_delayed: &mut IdmServerDelayed,
565    ) -> Option<JwsCompact> {
566        let mut idms_auth = idms.auth().await.unwrap();
567
568        let auth_allowed = idms_auth
569            .reauth_init(
570                ident.clone(),
571                AuthIssueSession::Token,
572                ct,
573                Source::Internal.into(),
574                ReauthRequest::GrantReadWrite,
575            )
576            .await
577            .expect("Failed to start reauth.");
578
579        let AuthResult { sessionid, state } = auth_allowed;
580
581        trace!(?state);
582
583        match state {
584            AuthState::Denied(reason) => {
585                trace!("{}", reason);
586                return None;
587            }
588            AuthState::Continue(mut allowed) => match allowed.pop() {
589                Some(AuthAllowed::Totp) => {}
590                _ => return None,
591            },
592            _ => unreachable!(),
593        };
594
595        let totp = token
596            .do_totp_duration_from_epoch(&ct)
597            .expect("Failed to perform totp step");
598
599        let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
600        let r2 = idms_auth
601            .auth(&totp_step, ct, Source::Internal.into())
602            .await;
603        let ar = r2.unwrap();
604        let AuthResult { sessionid, state } = ar;
605
606        assert!(matches!(state, AuthState::Continue(_)));
607
608        let pw_step = AuthEvent::cred_step_password(sessionid, pw);
609
610        // Expect success
611        let r3 = idms_auth.auth(&pw_step, ct, Source::Internal.into()).await;
612        debug!("r3 ==> {:?}", r3);
613        idms_auth.commit().expect("Must not fail");
614
615        match r3 {
616            Ok(AuthResult {
617                sessionid: _,
618                state: AuthState::Success(token, AuthIssueSession::Token),
619            }) => {
620                // Process the auth session
621                let da = idms_delayed.try_recv().expect("invalid");
622                assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
623                Some(*token)
624            }
625            _ => None,
626        }
627    }
628
629    #[idm_test]
630    async fn test_idm_reauth_passkey(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
631        let ct = duration_from_epoch_now();
632
633        // Setup the test account
634        setup_testaccount(idms, ct).await;
635        let mut passkey = setup_testaccount_passkey(idms, ct).await;
636
637        // Do an initial auth.
638        let token = auth_passkey(idms, ct, &mut passkey, idms_delayed)
639            .await
640            .expect("failed to authenticate with passkey");
641
642        // Token_str to uat
643        let ident = token_to_ident(idms, ct, token.clone().into()).await;
644
645        // Check that the rw entitlement is not present
646        debug!(?ident);
647        assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
648
649        // Assert the session is rw capable though which is what will allow the re-auth
650        // to proceed.
651
652        let session = ident.get_session().expect("Unable to access sessions");
653
654        assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
655
656        // Start the re-auth
657        let token = reauth_passkey(
658            idms,
659            ct,
660            &ident,
661            &mut passkey,
662            idms_delayed,
663            ReauthRequest::GrantReadWrite,
664        )
665        .await
666        .expect("Failed to get new session token");
667
668        // Token_str to uat
669        let ident = token_to_ident(idms, ct, token.clone().into()).await;
670
671        // They now have the entitlement.
672        debug!(?ident);
673        assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
674
675        // If you re-auth asking for verify only, you don't get the entitlement.
676        let token = reauth_passkey(
677            idms,
678            ct,
679            &ident,
680            &mut passkey,
681            idms_delayed,
682            ReauthRequest::VerifyCredentials,
683        )
684        .await
685        .expect("Failed to get new session token");
686
687        // Token_str to uat
688        let ident = token_to_ident(idms, ct, token.clone().into()).await;
689
690        // They now have the entitlement.
691        debug!(?ident);
692        assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
693    }
694
695    #[idm_test(audit = 1)]
696    async fn test_idm_reauth_softlocked_pw(
697        idms: &IdmServer,
698        idms_delayed: &mut IdmServerDelayed,
699        idms_audit: &mut IdmServerAudit,
700    ) {
701        // This test is to enforce that an account in a soft lock state can't proceed
702        // we a re-auth.
703        let ct = duration_from_epoch_now();
704
705        // Setup the test account
706        setup_testaccount(idms, ct).await;
707        let (pw, totp) = setup_testaccount_password_totp(idms, ct).await;
708
709        // Do an initial auth.
710        let token = auth_password_totp(idms, ct, &pw, &totp, idms_delayed)
711            .await
712            .expect("failed to authenticate with passkey");
713
714        // Token_str to uat
715        let ident = token_to_ident(idms, ct, token.into()).await;
716
717        // Check that the rw entitlement is not present
718        debug!(?ident);
719        assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
720
721        // Assert the session is rw capable though which is what will allow the re-auth
722        // to proceed.
723        let session = ident.get_session().expect("Unable to access sessions");
724
725        assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
726
727        // Softlock the account now.
728        assert!(reauth_password_totp(
729            idms,
730            ct,
731            &ident,
732            "absolutely-wrong-password",
733            &totp,
734            idms_delayed
735        )
736        .await
737        .is_none());
738
739        // There should be a queued audit event
740        match idms_audit.audit_rx().try_recv() {
741            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
742            _ => panic!("Oh no"),
743        }
744
745        // Start the re-auth - MUST FAIL!
746        assert!(
747            reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)
748                .await
749                .is_none()
750        );
751    }
752}