From 8306c3bc6a12797223b978be975ce8bb1536ed3f Mon Sep 17 00:00:00 2001 From: cuberoot74088 Date: Thu, 22 Jul 2021 04:04:56 +0200 Subject: [PATCH] Rename to SetCredentialRequest::BackupCodeGenerate (#524) --- kanidm_client/src/asynchronous.rs | 52 +++++++++++++++++++- kanidm_client/src/lib.rs | 12 +++++ kanidm_client/tests/proto_v1_test.rs | 73 ++++++++++++++++++++++++++++ kanidm_proto/src/v1.rs | 2 +- kanidm_tools/src/cli/account.rs | 4 +- kanidm_tools/src/opt/kanidm.rs | 2 +- kanidmd/src/lib/actors/v1_write.rs | 2 +- 7 files changed, 141 insertions(+), 6 deletions(-) diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 79abd5d71..655c97669 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -634,6 +634,56 @@ impl KanidmAsyncClient { } } + pub async fn auth_password_backup_code( + &self, + ident: &str, + password: &str, + backup_code: &str, + ) -> Result<(), ClientError> { + let mechs = match self.auth_step_init(ident).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !mechs.contains(&AuthMech::PasswordMfa) { + debug!("PasswordMfa mech not presented"); + return Err(ClientError::AuthenticationFailed); + } + + let state = match self.auth_step_begin(AuthMech::PasswordMfa).await { + Ok(s) => s, + Err(e) => return Err(e), + }; + + if !state.contains(&AuthAllowed::BackupCode) { + debug!("Backup Code step not offered."); + return Err(ClientError::AuthenticationFailed); + } + + let r = self.auth_step_backup_code(backup_code).await?; + + // Should need to continue. + match r.state { + AuthState::Continue(allowed) => { + if !allowed.contains(&AuthAllowed::Password) { + debug!("Password step not offered."); + return Err(ClientError::AuthenticationFailed); + } + } + _ => { + debug!("Invalid AuthState presented."); + return Err(ClientError::AuthenticationFailed); + } + }; + + let r = self.auth_step_password(password).await?; + + match r.state { + AuthState::Success(_token) => Ok(()), + _ => Err(ClientError::AuthenticationFailed), + } + } + pub async fn auth_webauthn_begin( &self, ident: &str, @@ -1105,7 +1155,7 @@ impl KanidmAsyncClient { &self, id: &str, ) -> Result, ClientError> { - let r = SetCredentialRequest::GenerateBackupCode; + let r = SetCredentialRequest::BackupCodeGenerate; let res: Result = self .perform_put_request( format!("/v1/account/{}/_credential/primary", id).as_str(), diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index fbd846c71..b6b271ac5 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -457,6 +457,18 @@ impl KanidmClient { tokio_block_on(self.asclient.auth_password_totp(ident, password, totp)) } + pub fn auth_password_backup_code( + &self, + ident: &str, + password: &str, + backup_code: &str, + ) -> Result<(), ClientError> { + tokio_block_on( + self.asclient + .auth_password_backup_code(ident, password, backup_code), + ) + } + pub fn auth_webauthn_begin( &self, ident: &str, diff --git a/kanidm_client/tests/proto_v1_test.rs b/kanidm_client/tests/proto_v1_test.rs index 4dbdcd677..42b4d52c3 100644 --- a/kanidm_client/tests/proto_v1_test.rs +++ b/kanidm_client/tests/proto_v1_test.rs @@ -820,6 +820,79 @@ fn test_server_rest_totp_auth_lifecycle() { }); } +#[test] +fn test_server_rest_backup_code_auth_lifecycle() { + run_test(|rsclient: KanidmClient| { + let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD); + assert!(res.is_ok()); + + // Not recommended in production! + rsclient + .idm_group_add_members("idm_admins", &["admin"]) + .unwrap(); + + // Create a new account + rsclient + .idm_account_create("demo_account", "Deeeeemo") + .unwrap(); + + // Enroll a totp to the account + assert!(rsclient + .idm_account_primary_credential_set_password("demo_account", "sohdi3iuHo6mai7noh0a") + .is_ok()); + let (sessionid, tok) = rsclient + .idm_account_primary_credential_generate_totp("demo_account") + .unwrap(); + + let r_tok: Totp = tok.into(); + let totp = r_tok + .do_totp_duration_from_epoch( + &SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(), + ) + .expect("Failed to do totp?"); + + rsclient + .idm_account_primary_credential_verify_totp("demo_account", totp, sessionid) + .unwrap(); // the result + + // Generate backup codes + let backup_codes = rsclient + .idm_account_primary_credential_generate_backup_code("demo_account") + .expect("Failed to generate backup codes?"); + + // Check a good auth using a backup code + let rsclient_good = rsclient.new_session().unwrap(); + assert!(rsclient_good + .auth_password_backup_code( + "demo_account", + "sohdi3iuHo6mai7noh0a", + backup_codes[0].as_str() + ) + .is_ok()); + + // Check a bad auth - needs to be second as we are going to trigger the slock. + // Get a new connection + let rsclient_bad = rsclient.new_session().unwrap(); + assert!(rsclient_bad + .auth_password_backup_code("demo_account", "sohdi3iuHo6mai7noh0a", "wrong-backup-code") + .is_err()); + // Delay by one second to allow the account to recover from the softlock. + std::thread::sleep(std::time::Duration::from_millis(1100)); + + // Remove TOTP and backup codes on the account. + rsclient + .idm_account_primary_credential_remove_totp("demo_account") + .unwrap(); + // Check password auth. + let rsclient_good = rsclient.new_session().unwrap(); + assert!(rsclient_good + .auth_simple_password("demo_account", "sohdi3iuHo6mai7noh0a") + .is_ok()); + }); +} + #[test] fn test_server_rest_webauthn_auth_lifecycle() { run_test(|rsclient: KanidmClient| { diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index b3b3a18a3..197522fdb 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -685,7 +685,7 @@ pub enum SetCredentialRequest { WebauthnRegister(Uuid, RegisterPublicKeyCredential), // Remove WebauthnRemove(String), - GenerateBackupCode, + BackupCodeGenerate, BackupCodeRemove, } diff --git a/kanidm_tools/src/cli/account.rs b/kanidm_tools/src/cli/account.rs index 505bd7994..82573405d 100644 --- a/kanidm_tools/src/cli/account.rs +++ b/kanidm_tools/src/cli/account.rs @@ -23,7 +23,7 @@ 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::BackupCodeGenerate(acs) => acs.copt.debug, AccountCredential::BackupCodeRemove(acs) => acs.copt.debug, AccountCredential::Status(acs) => acs.copt.debug, }, @@ -283,7 +283,7 @@ impl AccountOpt { } } } - AccountCredential::GenerateBackupCode(acsopt) => { + AccountCredential::BackupCodeGenerate(acsopt) => { let client = acsopt.copt.to_client(); match client.idm_account_primary_credential_generate_backup_code( acsopt.aopts.account_id.as_str(), diff --git a/kanidm_tools/src/opt/kanidm.rs b/kanidm_tools/src/opt/kanidm.rs index 95a5a1e18..70ff78190 100644 --- a/kanidm_tools/src/opt/kanidm.rs +++ b/kanidm_tools/src/opt/kanidm.rs @@ -181,7 +181,7 @@ pub enum AccountCredential { GeneratePassword(AccountCredentialSet), /// Generate a new set of backup codes. #[structopt(name = "generate_backup_codes")] - GenerateBackupCode(AccountNamedOpt), + BackupCodeGenerate(AccountNamedOpt), /// Remove backup codes from the account. #[structopt(name = "remove_backup_codes")] BackupCodeRemove(AccountNamedOpt), diff --git a/kanidmd/src/lib/actors/v1_write.rs b/kanidmd/src/lib/actors/v1_write.rs index 74664c1d6..7eb3e6d6a 100644 --- a/kanidmd/src/lib/actors/v1_write.rs +++ b/kanidmd/src/lib/actors/v1_write.rs @@ -691,7 +691,7 @@ impl QueryServerWriteV1 { .remove_account_webauthn(&mut audit, &rwe) .and_then(|r| idms_prox_write.commit(&mut audit).map(|_| r)) } - SetCredentialRequest::GenerateBackupCode => { + SetCredentialRequest::BackupCodeGenerate => { let gbe = GenerateBackupCodeEvent::from_parts( &mut audit, // &idms_prox_write.qs_write,