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
11use 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 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 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 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 }
59 SessionScope::ReadOnly | SessionScope::ReadWrite | SessionScope::Synchronise => {
60 error!("Session scope is not PrivilegeCapable and can not be used in re-auth.");
62 return Err(OperationError::InvalidState);
63 }
64 };
65
66 let session_cred_id = session.cred_id;
68
69 let sessionid = uuid_from_duration(ct, self.sid);
74
75 let _session_ticket = self.session_ticket.acquire().await;
77
78 let maybe_slock = account
80 .primary_cred_uuid_and_policy()
81 .and_then(|(cred_uuid, policy)| {
82 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 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 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 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 match auth_session {
149 Some(auth_session) => {
150 let mut session_write = self.sessions.write();
151 if session_write.contains_key(&sessionid) {
152 Err(OperationError::InvalidSessionState)
155 } else {
156 session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
157 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 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 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 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 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 let da = idms_delayed.try_recv().expect("invalid");
394 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
395 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 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 let da = idms_delayed.try_recv().expect("invalid");
465 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
466 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 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 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 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 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_testaccount(idms, ct).await;
631 let mut passkey = setup_testaccount_passkey(idms, ct).await;
632
633 let token = auth_passkey(idms, ct, &mut passkey, idms_delayed)
635 .await
636 .expect("failed to authenticate with passkey");
637
638 let ident = token_to_ident(idms, ct, token.clone().into()).await;
640
641 debug!(?ident);
643 assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
644
645 let session = ident.get_session().expect("Unable to access sessions");
649
650 assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
651
652 let token = reauth_passkey(idms, ct, &ident, &mut passkey, idms_delayed)
654 .await
655 .expect("Failed to get new session token");
656
657 let ident = token_to_ident(idms, ct, token.clone().into()).await;
659
660 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 let ct = duration_from_epoch_now();
674
675 setup_testaccount(idms, ct).await;
677 let (pw, totp) = setup_testaccount_password_totp(idms, ct).await;
678
679 let token = auth_password_totp(idms, ct, &pw, &totp, idms_delayed)
681 .await
682 .expect("failed to authenticate with passkey");
683
684 let ident = token_to_ident(idms, ct, token.into()).await;
686
687 debug!(?ident);
689 assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
690
691 let session = ident.get_session().expect("Unable to access sessions");
694
695 assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
696
697 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 match idms_audit.audit_rx().try_recv() {
711 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
712 _ => panic!("Oh no"),
713 }
714
715 assert!(
717 reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)
718 .await
719 .is_none()
720 );
721 }
722}