kanidmd_lib/idm/
reauth.rs

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