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
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 username = %account.name,
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 };
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 match auth_session {
148 Some(auth_session) => {
149 let mut session_write = self.sessions.write();
150 if session_write.contains_key(&sessionid) {
151 Err(OperationError::InvalidSessionState)
154 } else {
155 session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
156 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 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 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 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 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 let da = idms_delayed.try_recv().expect("invalid");
393 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
394 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 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 let da = idms_delayed.try_recv().expect("invalid");
464 assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
465 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 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 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 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 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_testaccount(idms, ct).await;
630 let mut passkey = setup_testaccount_passkey(idms, ct).await;
631
632 let token = auth_passkey(idms, ct, &mut passkey, idms_delayed)
634 .await
635 .expect("failed to authenticate with passkey");
636
637 let ident = token_to_ident(idms, ct, token.clone().into()).await;
639
640 debug!(?ident);
642 assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
643
644 let session = ident.get_session().expect("Unable to access sessions");
648
649 assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
650
651 let token = reauth_passkey(idms, ct, &ident, &mut passkey, idms_delayed)
653 .await
654 .expect("Failed to get new session token");
655
656 let ident = token_to_ident(idms, ct, token.clone().into()).await;
658
659 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 let ct = duration_from_epoch_now();
673
674 setup_testaccount(idms, ct).await;
676 let (pw, totp) = setup_testaccount_password_totp(idms, ct).await;
677
678 let token = auth_password_totp(idms, ct, &pw, &totp, idms_delayed)
680 .await
681 .expect("failed to authenticate with passkey");
682
683 let ident = token_to_ident(idms, ct, token.into()).await;
685
686 debug!(?ident);
688 assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
689
690 let session = ident.get_session().expect("Unable to access sessions");
693
694 assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
695
696 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 match idms_audit.audit_rx().try_recv() {
710 Ok(AuditEvent::AuthenticationDenied { .. }) => {}
711 _ => panic!("Oh no"),
712 }
713
714 assert!(
716 reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)
717 .await
718 .is_none()
719 );
720 }
721}