1use crate::common::{OpType, ToClientError};
2use std::cmp::Reverse;
3use std::collections::BTreeMap;
4use std::fs::{create_dir, File};
5use std::io::{self, BufReader, BufWriter, ErrorKind, IsTerminal, Write};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use compact_jwt::{
10 traits::JwsVerifiable, Jwk, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError,
11};
12use dialoguer::theme::ColorfulTheme;
13use dialoguer::Select;
14use kanidm_client::{ClientError, KanidmClient};
15use kanidm_proto::constants::CLIENT_TOKEN_CACHE;
16use kanidm_proto::internal::UserAuthToken;
17use kanidm_proto::v1::{AuthAllowed, AuthResponse, AuthState};
18#[cfg(target_family = "unix")]
19use libc::umask;
20use webauthn_authenticator_rs::prelude::RequestChallengeResponse;
21
22use crate::common::prompt_for_username_get_username;
23use crate::webauthn::get_authenticator;
24use crate::{CommonOpt, LoginOpt, LogoutOpt, ReauthOpt, SessionOpt};
25
26use serde::{Deserialize, Serialize};
27
28static TOKEN_DIR: &str = "~/.cache";
29
30#[derive(Debug, Serialize, Clone, Deserialize, Default)]
31pub struct TokenInstance {
32 keys: BTreeMap<String, Jwk>,
33 tokens: BTreeMap<String, JwsCompact>,
34}
35
36impl TokenInstance {
37 pub fn tokens(&self) -> &BTreeMap<String, JwsCompact> {
38 &self.tokens
39 }
40
41 pub fn keys(&self) -> &BTreeMap<String, Jwk> {
42 &self.keys
43 }
44
45 pub fn valid_uats(&self) -> BTreeMap<String, UserAuthToken> {
46 self.tokens
47 .iter()
48 .filter_map(|(u, jwsc)| {
49 let key_id = jwsc.kid()?;
51
52 let pub_jwk = self.keys.get(key_id)?;
54
55 let jws_verifier = JwsEs256Verifier::try_from(pub_jwk)
56 .map_err(|e| {
57 error!(?e, "Unable to configure jws verifier");
58 })
59 .ok()?;
60
61 jws_verifier
62 .verify(jwsc)
63 .and_then(|jws| {
64 jws.from_json::<UserAuthToken>().map_err(|serde_err| {
65 error!(?serde_err);
66 JwtError::InvalidJwt
67 })
68 })
69 .map_err(|e| {
70 error!(?e, "Unable to verify token signature, may be corrupt");
71 })
72 .map(|uat| (u.clone(), uat))
73 .ok()
74 })
75 .collect()
76 }
77
78 pub fn cleanup(&mut self, now: time::OffsetDateTime) -> usize {
79 let retain = self.valid_uats();
81
82 let start_len = self.tokens.len();
83
84 self.tokens.retain(|spn, _tonk| {
85 if let Some(uat) = retain.get(spn) {
86 if let Some(exp) = uat.expiry {
87 exp > now
89 } else {
90 true
91 }
92 } else {
93 false
94 }
95 });
96
97 start_len - self.tokens.len()
98 }
99}
100
101#[derive(Debug, Serialize, Clone, Deserialize, Default)]
102pub struct TokenStore {
103 instances: BTreeMap<String, TokenInstance>,
104}
105
106impl TokenStore {
107 pub fn instances(&self, name: &Option<String>) -> Option<&TokenInstance> {
108 let n_lookup = name.clone().unwrap_or_default();
109
110 self.instances.get(&n_lookup)
111 }
112
113 pub fn instances_mut(&mut self, name: &Option<String>) -> Option<&mut TokenInstance> {
114 let n_lookup = name.clone().unwrap_or_default();
115
116 self.instances.get_mut(&n_lookup)
117 }
118}
119
120impl CommonOpt {
121 fn get_token_cache_path(&self) -> String {
122 match self.token_cache_path.clone() {
123 None => CLIENT_TOKEN_CACHE.to_string(),
124 Some(val) => val.clone(),
125 }
126 }
127}
128
129#[allow(clippy::result_unit_err)]
130pub fn read_tokens(token_path: &str) -> Result<TokenStore, ()> {
131 let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
132 if !token_path.exists() {
133 debug!(
134 "Token cache file path {:?} does not exist, returning an empty token store.",
135 token_path
136 );
137 return Ok(Default::default());
138 }
139
140 debug!("Attempting to read tokens from {:?}", &token_path);
141 let file = match File::open(&token_path) {
143 Ok(f) => f,
144 Err(e) => {
145 match e.kind() {
146 ErrorKind::PermissionDenied => {
147 error!(
149 "Permission denied reading token store file {:?}",
150 &token_path
151 );
152 return Err(());
153 }
154 _ => {
156 warn!(
157 "Cannot read tokens from {} due to error: {:?} ... continuing.",
158 token_path.display(),
159 e
160 );
161 return Ok(Default::default());
162 }
163 };
164 }
165 };
166 let reader = BufReader::new(file);
167
168 serde_json::from_reader(reader).map_err(|e| {
170 warn!(
171 "JSON/IO error reading tokens from {:?} -> {:?}",
172 &token_path, e
173 );
174 })
175}
176
177#[allow(clippy::result_unit_err)]
178pub fn write_tokens(tokens: &TokenStore, token_path: &str) -> Result<(), ()> {
179 let token_dir = PathBuf::from(shellexpand::tilde(TOKEN_DIR).into_owned());
180 let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
181
182 token_dir
183 .parent()
184 .ok_or_else(|| {
185 error!(
186 "Parent directory to {} is invalid (root directory?).",
187 TOKEN_DIR
188 );
189 })
190 .and_then(|parent_dir| {
191 if parent_dir.exists() {
192 Ok(())
193 } else {
194 error!("Parent directory to {} does not exist.", TOKEN_DIR);
195 Err(())
196 }
197 })?;
198
199 if !token_dir.exists() {
200 create_dir(token_dir).map_err(|e| {
201 error!("Unable to create directory - {} {:?}", TOKEN_DIR, e);
202 })?;
203 }
204
205 #[cfg(target_family = "unix")]
207 let before = unsafe { umask(0o177) };
208
209 let file = File::create(&token_path).map_err(|e| {
210 #[cfg(target_family = "unix")]
211 let _ = unsafe { umask(before) };
212 error!("Can not write to {} -> {:?}", token_path.display(), e);
213 })?;
214
215 #[cfg(target_family = "unix")]
216 let _ = unsafe { umask(before) };
217
218 let writer = BufWriter::new(file);
219 serde_json::to_writer_pretty(writer, tokens).map_err(|e| {
220 error!(
221 "JSON/IO error writing tokens to file {:?} -> {:?}",
222 &token_path, e
223 );
224 })
225}
226
227fn get_index_choice_dialoguer(msg: &str, options: &[String]) -> usize {
229 let user_select = Select::with_theme(&ColorfulTheme::default())
230 .with_prompt(msg)
231 .default(0)
232 .items(options)
233 .interact();
234
235 let selection = match user_select {
236 Err(error) => {
237 error!("Failed to handle user input: {:?}", error);
238 std::process::exit(1);
239 }
240 Ok(value) => value,
241 };
242 debug!("Index of the chosen menu item: {:?}", selection);
243
244 selection
245}
246
247async fn do_password(
248 client: &mut KanidmClient,
249 password: &Option<String>,
250) -> Result<AuthResponse, ClientError> {
251 let password = match password {
252 Some(password) => {
253 trace!("User provided password directly, don't need to prompt.");
254 password.to_owned()
255 }
256 None => dialoguer::Password::new()
257 .with_prompt("Enter password")
258 .interact()
259 .unwrap_or_else(|e| {
260 error!("Failed to create password prompt -- {:?}", e);
261 std::process::exit(1);
262 }),
263 };
264 client.auth_step_password(password.as_str()).await
265}
266
267async fn do_backup_code(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
268 print!("Enter Backup Code: ");
269 #[allow(clippy::unwrap_used)]
271 io::stdout().flush().unwrap();
272 let mut backup_code = String::new();
273 loop {
274 if let Err(e) = io::stdin().read_line(&mut backup_code) {
275 error!("Failed to read from stdin -> {:?}", e);
276 return Err(ClientError::SystemError);
277 };
278 if !backup_code.trim().is_empty() {
279 break;
280 };
281 }
282 client.auth_step_backup_code(backup_code.trim()).await
283}
284
285async fn do_totp(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
286 let totp = loop {
287 print!("Enter TOTP: ");
288 if let Err(e) = io::stdout().flush() {
290 error!("Somehow we failed to flush stdout: {:?}", e);
291 };
292 let mut buffer = String::new();
293 if let Err(e) = io::stdin().read_line(&mut buffer) {
294 error!("Failed to read from stdin -> {:?}", e);
295 return Err(ClientError::SystemError);
296 };
297
298 let response = buffer.trim();
299 match response.parse::<u32>() {
300 Ok(i) => break i,
301 Err(_) => eprintln!("Invalid Number"),
302 };
303 };
304 client.auth_step_totp(totp).await
305}
306
307async fn do_passkey(
308 client: &mut KanidmClient,
309 pkr: RequestChallengeResponse,
310) -> Result<AuthResponse, ClientError> {
311 let mut wa = get_authenticator();
312 println!("If your authenticator is not attached, attach it now.");
313 println!("Your authenticator will then flash/prompt for confirmation.");
314 #[cfg(target_os = "macos")]
315 println!("Note: TouchID is not currently supported on the CLI 🫤");
316 let auth = wa
317 .do_authentication(client.get_origin().clone(), pkr)
318 .map(Box::new)
319 .unwrap_or_else(|e| {
320 error!("Failed to interact with webauthn device. -- {:?}", e);
321 std::process::exit(1);
322 });
323
324 client.auth_step_passkey_complete(auth).await
325}
326
327async fn do_securitykey(
328 client: &mut KanidmClient,
329 pkr: RequestChallengeResponse,
330) -> Result<AuthResponse, ClientError> {
331 let mut wa = get_authenticator();
332 println!("Your authenticator will now flash for you to interact with it.");
333 let auth = wa
334 .do_authentication(client.get_origin().clone(), pkr)
335 .map(Box::new)
336 .unwrap_or_else(|e| {
337 error!("Failed to interact with webauthn device. -- {:?}", e);
338 std::process::exit(1);
339 });
340
341 client.auth_step_securitykey_complete(auth).await
342}
343
344async fn process_auth_state(
345 mut allowed: Vec<AuthAllowed>,
346 mut client: KanidmClient,
347 maybe_password: &Option<String>,
348 instance_name: &Option<String>,
349) {
350 loop {
351 debug!("Allowed mechanisms -> {:?}", allowed);
352 let choice = match allowed.len() {
354 0 => {
355 error!("Error during authentication phase: Server offered no method to proceed");
356 std::process::exit(1);
357 }
358 1 =>
359 {
360 #[allow(clippy::expect_used)]
361 allowed
362 .first()
363 .expect("can not fail - bounds already checked.")
364 }
365 _ => {
366 let mut options = Vec::new();
367 allowed.sort_unstable_by(|a, b| Reverse(a).cmp(&Reverse(b)));
369 for val in allowed.iter() {
370 options.push(val.to_string());
371 }
372 let msg = "Please choose which credential to provide:";
373 let selection = get_index_choice_dialoguer(msg, &options);
374
375 #[allow(clippy::expect_used)]
376 allowed
377 .get(selection)
378 .expect("Failed to select an authentication option!")
379 }
380 };
381
382 let res = match choice {
383 AuthAllowed::Anonymous => client.auth_step_anonymous().await,
384 AuthAllowed::Password => do_password(&mut client, maybe_password).await,
385 AuthAllowed::BackupCode => do_backup_code(&mut client).await,
386 AuthAllowed::Totp => do_totp(&mut client).await,
387 AuthAllowed::Passkey(chal) => do_passkey(&mut client, chal.clone()).await,
388 AuthAllowed::SecurityKey(chal) => do_securitykey(&mut client, chal.clone()).await,
389 };
390
391 let state = res
393 .unwrap_or_else(|e| {
394 error!("Error in authentication phase: {:?}", e);
395 std::process::exit(1);
396 })
397 .state;
398
399 allowed = match &state {
401 AuthState::Continue(allowed) => allowed.to_vec(),
402 AuthState::Success(_token) => break,
403 AuthState::Denied(reason) => {
404 error!("Authentication Denied: {:?}", reason);
405 std::process::exit(1);
406 }
407 _ => {
408 error!("Error in authentication phase: invalid authstate");
409 std::process::exit(1);
410 }
411 };
412 }
414
415 let mut tokens = read_tokens(&client.get_token_cache_path()).unwrap_or_default();
417
418 let n_lookup = instance_name.clone().unwrap_or_default();
420 let token_instance = tokens.instances.entry(n_lookup).or_default();
421
422 let (spn, tonk) = match client.get_token().await {
424 Some(t) => {
425 let jwsc = match JwsCompact::from_str(&t) {
426 Ok(j) => j,
427 Err(err) => {
428 error!(?err, "Unable to parse token");
429 std::process::exit(1);
430 }
431 };
432
433 let Some(key_id) = jwsc.kid() else {
434 error!("JWS invalid, not key id associated");
435 std::process::exit(1);
436 };
437
438 let pub_jwk = if let Some(pub_jwk) = token_instance.keys.get(key_id).cloned() {
440 pub_jwk
441 } else {
442 let pub_jwk = match client.get_public_jwk(key_id).await {
444 Ok(pj) => pj,
445 Err(err) => {
446 error!(?err, "Unable to retrieve jwk from server");
447 std::process::exit(1);
448 }
449 };
450 token_instance
451 .keys
452 .insert(key_id.to_string(), pub_jwk.clone());
453 pub_jwk
454 };
455
456 let jws_verifier = match JwsEs256Verifier::try_from(&pub_jwk) {
457 Ok(verifier) => verifier,
458 Err(err) => {
459 error!(?err, "Unable to configure jws verifier");
460 std::process::exit(1);
461 }
462 };
463
464 let tonk = match jws_verifier.verify(&jwsc).and_then(|jws| {
465 jws.from_json::<UserAuthToken>().map_err(|serde_err| {
466 error!(?serde_err);
467 JwtError::InvalidJwt
468 })
469 }) {
470 Ok(uat) => uat,
471 Err(err) => {
472 error!(?err, "Unable to verify token signature");
473 std::process::exit(1);
474 }
475 };
476
477 let spn = tonk.spn;
478 (spn, jwsc)
480 }
481 None => {
482 error!("Error retrieving client session");
483 std::process::exit(1);
484 }
485 };
486
487 token_instance.tokens.insert(spn.clone(), tonk);
488
489 if write_tokens(&tokens, &client.get_token_cache_path()).is_err() {
491 trace!(?tokens);
492 error!("Error persisting authentication token store");
493 std::process::exit(1);
494 };
495
496 println!("Login Success for {}", spn);
498}
499
500impl LoginOpt {
501 pub fn debug(&self) -> bool {
502 self.copt.debug
503 }
504
505 pub async fn exec(&self) {
506 let client = self.copt.to_unauth_client();
507 let username = match self.copt.username.as_deref() {
508 Some(val) => val,
509 None => {
510 error!("Please specify a username with -D <USERNAME> to login.");
511 std::process::exit(1);
512 }
513 };
514
515 let mut mechs: Vec<_> = client
517 .auth_step_init(username)
518 .await
519 .unwrap_or_else(|e| {
520 error!("Error during authentication init phase: {:?}", e);
521 std::process::exit(1);
522 })
523 .into_iter()
524 .collect();
525
526 mechs.sort_unstable_by(|a, b| Reverse(a).cmp(&Reverse(b)));
527
528 let mech = match mechs.len() {
529 0 => {
530 error!("Error during authentication init phase: Server offered no authentication mechanisms");
531 std::process::exit(1);
532 }
533 1 =>
534 {
535 #[allow(clippy::expect_used)]
536 mechs
537 .first()
538 .expect("can not fail - bounds already checked.")
539 }
540 _ => {
541 let mut options = Vec::new();
542 for val in mechs.iter() {
543 options.push(val.to_string());
544 }
545 let msg = "Please choose how you want to authenticate:";
546 let selection = get_index_choice_dialoguer(msg, &options);
547
548 #[allow(clippy::expect_used)]
549 mechs
550 .get(selection)
551 .expect("can not fail - bounds already checked.")
552 }
553 };
554
555 let allowed = client
556 .auth_step_begin((*mech).clone())
557 .await
558 .unwrap_or_else(|e| {
559 error!("Error during authentication begin phase: {:?}", e);
560 std::process::exit(1);
561 });
562
563 let instance_name = &self.copt.instance;
564
565 process_auth_state(allowed, client, &self.password, instance_name).await;
567 }
568}
569
570impl ReauthOpt {
571 pub fn debug(&self) -> bool {
572 self.copt.debug
573 }
574
575 pub(crate) async fn inner(&self, client: KanidmClient) {
576 let instance_name = &self.copt.instance;
577
578 let allowed = client.reauth_begin().await.unwrap_or_else(|e| {
579 error!("Error during reauthentication begin phase: {:?}", e);
580 std::process::exit(1);
581 });
582
583 process_auth_state(allowed, client, &None, instance_name).await;
584 }
585
586 pub async fn exec(&self) {
587 let client = self.copt.to_client(OpType::Read).await;
588 self.inner(client).await
590 }
591}
592
593impl LogoutOpt {
594 pub fn debug(&self) -> bool {
595 self.copt.debug
596 }
597
598 pub async fn exec(&self) {
599 let mut tokens = read_tokens(&self.copt.get_token_cache_path()).unwrap_or_else(|_| {
600 error!("Error retrieving authentication token store");
601 std::process::exit(1);
602 });
603
604 let instance_name = &self.copt.instance;
605 let n_lookup = instance_name.clone().unwrap_or_default();
606 let Some(token_instance) = tokens.instances.get_mut(&n_lookup) else {
607 println!("No sessions for instance {}", n_lookup);
608 return;
609 };
610
611 let spn: String = if self.local_only {
612 let mut _tmp_username = String::new();
614 match &self.copt.username {
615 Some(value) => value.clone(),
616 None => {
617 if std::io::stdin().is_terminal() {
619 match prompt_for_username_get_username(
620 &self.copt.get_token_cache_path(),
621 instance_name,
622 ) {
623 Ok(value) => value,
624 Err(msg) => {
625 error!("{}", msg);
626 std::process::exit(1);
627 }
628 }
629 } else {
630 eprintln!("Not running in interactive mode and no username specified, can't continue!");
631 return;
632 }
633 }
634 }
635 } else {
636 let client = match self.copt.try_to_client(OpType::Read).await {
637 Ok(c) => c,
638 Err(ToClientError::NeedLogin(_)) => {
639 std::process::exit(0);
641 }
642 Err(ToClientError::NeedReauth(_, _)) | Err(ToClientError::Other) => {
643 std::process::exit(1);
645 }
646 };
647
648 let token = match client.get_token().await {
649 Some(t) => t,
650 None => {
651 error!("Client token store is empty/corrupt");
652 std::process::exit(1);
653 }
654 };
655
656 let jwsc = match JwsCompact::from_str(&token) {
659 Ok(j) => j,
660 Err(err) => {
661 error!(?err, "Unable to parse token");
662 info!("The token can be removed locally with `--local-only`");
663 std::process::exit(1);
664 }
665 };
666
667 let Some(key_id) = jwsc.kid() else {
668 error!("Invalid token, missing KeyID");
669 info!("The token can be removed locally with `--local-only`");
670 std::process::exit(1);
671 };
672
673 let Some(pub_jwk) = token_instance.keys().get(key_id) else {
674 error!("Invalid instance, no signing keys are available");
675 info!("The token can be removed locally with `--local-only`");
676 std::process::exit(1);
677 };
678
679 let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
680 Ok(verifier) => verifier,
681 Err(err) => {
682 error!(?err, "Unable to configure jws verifier");
683 info!("The token can be removed locally with `--local-only`");
684 std::process::exit(1);
685 }
686 };
687
688 let uat = match jws_verifier.verify(&jwsc).and_then(|jws| {
689 jws.from_json::<UserAuthToken>().map_err(|serde_err| {
690 error!(?serde_err);
691 info!("The token can be removed locally with `--local-only`");
692 JwtError::InvalidJwt
693 })
694 }) {
695 Ok(uat) => uat,
696 Err(e) => {
697 error!(?e, "Unable to verify token signature, may be corrupt");
698 info!("The token can be removed locally with `--local-only`");
699 std::process::exit(1);
700 }
701 };
702
703 if let Err(e) = client.logout().await {
705 error!("Failed to logout - {:?}", e);
706 std::process::exit(1);
707 }
708
709 uat.spn
712 };
713
714 if token_instance.tokens.remove(&spn).is_some() {
716 if let Err(_e) = write_tokens(&tokens, &self.copt.get_token_cache_path()) {
718 error!("Error persisting authentication token store");
719 std::process::exit(1);
720 };
721 println!("Removed session for {}", spn);
722 } else {
723 println!("No sessions for {}", spn);
724 }
725 }
726}
727
728impl SessionOpt {
729 pub fn debug(&self) -> bool {
730 match self {
731 SessionOpt::List(dopt) | SessionOpt::Cleanup(dopt) => dopt.debug,
732 }
733 }
734
735 pub async fn exec(&self) {
736 match self {
737 SessionOpt::List(copt) => {
738 let token_store = read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
739 error!("Error retrieving authentication token store");
740 std::process::exit(1);
741 });
742
743 let instance_name = &copt.instance;
744
745 let Some(token_instance) = token_store.instances(instance_name) else {
746 return;
747 };
748
749 for (_, uat) in token_instance.valid_uats() {
750 println!("---");
751 println!("{}", uat);
752 }
753 }
754 SessionOpt::Cleanup(copt) => {
755 let mut token_store =
756 read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
757 error!("Error retrieving authentication token store");
758 std::process::exit(1);
759 });
760
761 let instance_name = &copt.instance;
762
763 let Some(token_instance) = token_store.instances_mut(instance_name) else {
764 error!("No tokens for instance");
765 std::process::exit(1);
766 };
767
768 let now = time::OffsetDateTime::now_utc();
769 let change = token_instance.cleanup(now);
770
771 if let Err(_e) = write_tokens(&token_store, &copt.get_token_cache_path()) {
772 error!("Error persisting authentication token store");
773 std::process::exit(1);
774 };
775
776 println!("Removed {} sessions", change);
777 }
778 }
779 }
780}