From 620a1717a838c0b16b45517d38dda21da60ac0f3 Mon Sep 17 00:00:00 2001 From: cuberoot74088 Date: Thu, 8 Jul 2021 04:50:55 +0200 Subject: [PATCH] 495 backup codes cli extension (#517) --- kanidm_client/src/asynchronous.rs | 53 ++++++++++++++++++++++++++++++ kanidm_client/src/lib.rs | 24 ++++++++++++++ kanidm_proto/src/v1.rs | 4 +++ kanidm_tools/src/cli/account.rs | 31 +++++++++++++++++ kanidm_tools/src/cli/session.rs | 18 ++++++++++ kanidm_tools/src/opt/kanidm.rs | 9 +++-- kanidmd/src/lib/idm/authsession.rs | 5 +-- kanidmd_web_ui/src/login.rs | 34 +++++++++++++++++++ 8 files changed, 173 insertions(+), 5 deletions(-) diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 74ed03a1a..79abd5d71 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -481,6 +481,23 @@ impl KanidmAsyncClient { r } + pub async fn auth_step_backup_code( + &self, + backup_code: &str, + ) -> Result { + let auth_req = AuthRequest { + step: AuthStep::Cred(AuthCredential::BackupCode(backup_code.to_string())), + }; + let r: Result = self.perform_auth_post_request("/v1/auth", auth_req).await; + + if let Ok(ar) = &r { + if let AuthState::Success(token) = &ar.state { + self.set_token(token.clone()).await; + }; + }; + r + } + pub async fn auth_step_totp(&self, totp: u32) -> Result { let auth_req = AuthRequest { step: AuthStep::Cred(AuthCredential::Totp(totp)), @@ -1084,6 +1101,42 @@ impl KanidmAsyncClient { } } + pub async fn idm_account_primary_credential_generate_backup_code( + &self, + id: &str, + ) -> Result, ClientError> { + let r = SetCredentialRequest::GenerateBackupCode; + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::BackupCodes(s)) => Ok(s), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + + pub async fn idm_account_primary_credential_remove_backup_code( + &self, + id: &str, + ) -> Result<(), ClientError> { + let r = SetCredentialRequest::BackupCodeRemove; + let res: Result = self + .perform_put_request( + format!("/v1/account/{}/_credential/primary", id).as_str(), + r, + ) + .await; + match res { + Ok(SetCredentialResponse::Success) => Ok(()), + Ok(_) => Err(ClientError::EmptyResponse), + Err(e) => Err(e), + } + } + pub async fn idm_account_get_credential_status( &self, id: &str, diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index 4c6e525c6..a6f50d21b 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -425,6 +425,10 @@ impl KanidmClient { tokio_block_on(self.asclient.auth_step_password(password)) } + pub fn auth_step_backup_code(&self, backup_code: &str) -> Result { + tokio_block_on(self.asclient.auth_step_backup_code(backup_code)) + } + pub fn auth_step_totp(&self, totp: u32) -> Result { tokio_block_on(self.asclient.auth_step_totp(totp)) } @@ -701,6 +705,26 @@ impl KanidmClient { ) } + pub fn idm_account_primary_credential_generate_backup_code( + &self, + id: &str, + ) -> Result, ClientError> { + tokio_block_on( + self.asclient + .idm_account_primary_credential_generate_backup_code(id), + ) + } + + pub fn idm_account_primary_credential_remove_backup_code( + &self, + id: &str, + ) -> Result<(), ClientError> { + tokio_block_on( + self.asclient + .idm_account_primary_credential_remove_backup_code(id), + ) + } + pub fn idm_account_get_credential_status( &self, id: &str, diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 3658fff9a..b3b3a18a3 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -594,6 +594,7 @@ pub struct AuthRequest { #[serde(rename_all = "lowercase")] pub enum AuthAllowed { Anonymous, + BackupCode, Password, Totp, Webauthn(RequestChallengeResponse), @@ -618,6 +619,8 @@ impl Ord for AuthAllowed { (_, AuthAllowed::Anonymous) => Ordering::Greater, (AuthAllowed::Password, _) => Ordering::Less, (_, AuthAllowed::Password) => Ordering::Greater, + (AuthAllowed::BackupCode, _) => Ordering::Less, + (_, AuthAllowed::BackupCode) => Ordering::Greater, (AuthAllowed::Totp, _) => Ordering::Less, (_, AuthAllowed::Totp) => Ordering::Greater, (AuthAllowed::Webauthn(_), _) => Ordering::Less, @@ -639,6 +642,7 @@ impl fmt::Display for AuthAllowed { match self { AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"), AuthAllowed::Password => write!(f, "Password"), + AuthAllowed::BackupCode => write!(f, "Backup Code"), AuthAllowed::Totp => write!(f, "TOTP"), AuthAllowed::Webauthn(_) => write!(f, "Webauthn Token"), } diff --git a/kanidm_tools/src/cli/account.rs b/kanidm_tools/src/cli/account.rs index 1073dc7de..505bd7994 100644 --- a/kanidm_tools/src/cli/account.rs +++ b/kanidm_tools/src/cli/account.rs @@ -23,6 +23,8 @@ impl AccountOpt { AccountCredential::RemoveWebauthn(acs) => acs.copt.debug, AccountCredential::RegisterTotp(acs) => acs.copt.debug, AccountCredential::RemoveTotp(acs) => acs.copt.debug, + AccountCredential::GenerateBackupCode(acs) => acs.copt.debug, + AccountCredential::BackupCodeRemove(acs) => acs.copt.debug, AccountCredential::Status(acs) => acs.copt.debug, }, AccountOpt::Radius(acopt) => match acopt { @@ -281,6 +283,35 @@ impl AccountOpt { } } } + AccountCredential::GenerateBackupCode(acsopt) => { + let client = acsopt.copt.to_client(); + match client.idm_account_primary_credential_generate_backup_code( + acsopt.aopts.account_id.as_str(), + ) { + Ok(s) => { + println!("Please store these Backup codes in a safe place"); + println!("---"); + println!("{}", s.join("\n")); + println!("---"); + } + Err(e) => { + eprintln!("Error generating Backup Codes for account -> {:?}", e); + } + } + } + AccountCredential::BackupCodeRemove(acsopt) => { + let client = acsopt.copt.to_client(); + match client.idm_account_primary_credential_remove_backup_code( + acsopt.aopts.account_id.as_str(), + ) { + Ok(_) => { + println!("BackupCodeRemove success."); + } + Err(e) => { + eprintln!("Error BackupCodeRemove for account -> {:?}", e); + } + } + } AccountCredential::Status(acsopt) => { let client = acsopt.copt.to_client(); match client.idm_account_get_credential_status(acsopt.aopts.account_id.as_str()) diff --git a/kanidm_tools/src/cli/session.rs b/kanidm_tools/src/cli/session.rs index fa44289ee..8b413db0b 100644 --- a/kanidm_tools/src/cli/session.rs +++ b/kanidm_tools/src/cli/session.rs @@ -142,6 +142,23 @@ impl LoginOpt { client.auth_step_password(password.as_str()) } + fn do_backup_code(&self, client: &mut KanidmClient) -> Result { + print!("Enter Backup Code: "); + // We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts. + io::stdout().flush().unwrap(); + let mut backup_code = String::new(); + loop { + if let Err(e) = io::stdin().read_line(&mut backup_code) { + eprintln!("Failed to read from stdin -> {:?}", e); + return Err(ClientError::SystemError); + }; + if backup_code.trim().len() > 0 { + break; + }; + } + client.auth_step_backup_code(backup_code.trim()) + } + fn do_totp(&self, client: &mut KanidmClient) -> Result { let totp = loop { print!("Enter TOTP: "); @@ -264,6 +281,7 @@ impl LoginOpt { let res = match choice { AuthAllowed::Anonymous => client.auth_step_anonymous(), AuthAllowed::Password => self.do_password(&mut client), + AuthAllowed::BackupCode => self.do_backup_code(&mut client), AuthAllowed::Totp => self.do_totp(&mut client), AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()), }; diff --git a/kanidm_tools/src/opt/kanidm.rs b/kanidm_tools/src/opt/kanidm.rs index b51c026f7..95a5a1e18 100644 --- a/kanidm_tools/src/opt/kanidm.rs +++ b/kanidm_tools/src/opt/kanidm.rs @@ -76,8 +76,6 @@ pub enum GroupOpt { Posix(GroupPosix), } - - #[derive(Debug, StructOpt)] pub struct AccountCommonOpt { #[structopt()] @@ -181,6 +179,12 @@ pub enum AccountCredential { /// and generate a new strong random password. #[structopt(name = "reset_credential")] GeneratePassword(AccountCredentialSet), + /// Generate a new set of backup codes. + #[structopt(name = "generate_backup_codes")] + GenerateBackupCode(AccountNamedOpt), + /// Remove backup codes from the account. + #[structopt(name = "remove_backup_codes")] + BackupCodeRemove(AccountNamedOpt), } #[derive(Debug, StructOpt)] @@ -407,4 +411,3 @@ pub enum KanidmClientOpt { /// Unsafe - low level, raw database operations. Raw(RawOpt), } - diff --git a/kanidmd/src/lib/idm/authsession.rs b/kanidmd/src/lib/idm/authsession.rs index a6b48bf70..9364229a2 100644 --- a/kanidmd/src/lib/idm/authsession.rs +++ b/kanidmd/src/lib/idm/authsession.rs @@ -499,9 +499,10 @@ impl CredHandler { CredHandler::Anonymous => vec![AuthAllowed::Anonymous], CredHandler::Password(_, _) => vec![AuthAllowed::Password], CredHandler::PasswordMfa(ref pw_mfa) => pw_mfa - .totp + .backup_code .iter() - .map(|_| AuthAllowed::Totp) + .map(|_| AuthAllowed::BackupCode) + .chain(pw_mfa.totp.iter().map(|_| AuthAllowed::Totp)) .chain( pw_mfa .wan diff --git a/kanidmd_web_ui/src/login.rs b/kanidmd_web_ui/src/login.rs index d9ec76da5..963607871 100644 --- a/kanidmd_web_ui/src/login.rs +++ b/kanidmd_web_ui/src/login.rs @@ -39,6 +39,7 @@ enum LoginState { // MechChoice // CredChoice Password(bool), + BackupCode(bool), Totp(TotpState), Webauthn(web_sys::CredentialRequestOptions), Error(String, Option), @@ -51,6 +52,7 @@ pub enum LoginAppMsg { Restart, Begin, PasswordSubmit, + BackupCodeSubmit, TotpSubmit, WebauthnSubmit(PublicKeyCredential), Start(String, AuthResponse), @@ -159,6 +161,23 @@ impl LoginApp { } } + LoginState::BackupCode(enable) => { + html! { + <> +
+

+ {" Backup Code: "} +

+
+
+
+ + +
+
+ + } + } LoginState::Totp(state) => { html! { <> @@ -321,6 +340,18 @@ impl Component for LoginApp { self.inputvalue = "".to_string(); true } + LoginAppMsg::BackupCodeSubmit => { + ConsoleService::log("backupcode"); + // Disable the button? + self.state = LoginState::BackupCode(false); + let authreq = AuthRequest { + step: AuthStep::Cred(AuthCredential::BackupCode(self.inputvalue.clone())), + }; + self.auth_step(authreq); + // Clear the backup code from memory. + self.inputvalue = "".to_string(); + true + } LoginAppMsg::TotpSubmit => { ConsoleService::log("totp"); // Disable the button? @@ -411,6 +442,9 @@ impl Component for LoginApp { // Go to the password view. self.state = LoginState::Password(true); } + AuthAllowed::BackupCode => { + self.state = LoginState::BackupCode(true); + } AuthAllowed::Totp => { self.state = LoginState::Totp(TotpState::Enabled); }