kanidmd_lib/idm/
reauth.rs

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