495 backup codes cli extension (#517)

This commit is contained in:
cuberoot74088 2021-07-08 04:50:55 +02:00 committed by GitHub
parent fc2824eec5
commit 620a1717a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 173 additions and 5 deletions

View file

@ -481,6 +481,23 @@ impl KanidmAsyncClient {
r r
} }
pub async fn auth_step_backup_code(
&self,
backup_code: &str,
) -> Result<AuthResponse, ClientError> {
let auth_req = AuthRequest {
step: AuthStep::Cred(AuthCredential::BackupCode(backup_code.to_string())),
};
let r: Result<AuthResponse, _> = 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<AuthResponse, ClientError> { pub async fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
let auth_req = AuthRequest { let auth_req = AuthRequest {
step: AuthStep::Cred(AuthCredential::Totp(totp)), 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<Vec<String>, ClientError> {
let r = SetCredentialRequest::GenerateBackupCode;
let res: Result<SetCredentialResponse, ClientError> = 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<SetCredentialResponse, ClientError> = 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( pub async fn idm_account_get_credential_status(
&self, &self,
id: &str, id: &str,

View file

@ -425,6 +425,10 @@ impl KanidmClient {
tokio_block_on(self.asclient.auth_step_password(password)) tokio_block_on(self.asclient.auth_step_password(password))
} }
pub fn auth_step_backup_code(&self, backup_code: &str) -> Result<AuthResponse, ClientError> {
tokio_block_on(self.asclient.auth_step_backup_code(backup_code))
}
pub fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> { pub fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
tokio_block_on(self.asclient.auth_step_totp(totp)) 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<Vec<String>, 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( pub fn idm_account_get_credential_status(
&self, &self,
id: &str, id: &str,

View file

@ -594,6 +594,7 @@ pub struct AuthRequest {
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AuthAllowed { pub enum AuthAllowed {
Anonymous, Anonymous,
BackupCode,
Password, Password,
Totp, Totp,
Webauthn(RequestChallengeResponse), Webauthn(RequestChallengeResponse),
@ -618,6 +619,8 @@ impl Ord for AuthAllowed {
(_, AuthAllowed::Anonymous) => Ordering::Greater, (_, AuthAllowed::Anonymous) => Ordering::Greater,
(AuthAllowed::Password, _) => Ordering::Less, (AuthAllowed::Password, _) => Ordering::Less,
(_, AuthAllowed::Password) => Ordering::Greater, (_, AuthAllowed::Password) => Ordering::Greater,
(AuthAllowed::BackupCode, _) => Ordering::Less,
(_, AuthAllowed::BackupCode) => Ordering::Greater,
(AuthAllowed::Totp, _) => Ordering::Less, (AuthAllowed::Totp, _) => Ordering::Less,
(_, AuthAllowed::Totp) => Ordering::Greater, (_, AuthAllowed::Totp) => Ordering::Greater,
(AuthAllowed::Webauthn(_), _) => Ordering::Less, (AuthAllowed::Webauthn(_), _) => Ordering::Less,
@ -639,6 +642,7 @@ impl fmt::Display for AuthAllowed {
match self { match self {
AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"), AuthAllowed::Anonymous => write!(f, "Anonymous (no credentials)"),
AuthAllowed::Password => write!(f, "Password"), AuthAllowed::Password => write!(f, "Password"),
AuthAllowed::BackupCode => write!(f, "Backup Code"),
AuthAllowed::Totp => write!(f, "TOTP"), AuthAllowed::Totp => write!(f, "TOTP"),
AuthAllowed::Webauthn(_) => write!(f, "Webauthn Token"), AuthAllowed::Webauthn(_) => write!(f, "Webauthn Token"),
} }

View file

@ -23,6 +23,8 @@ impl AccountOpt {
AccountCredential::RemoveWebauthn(acs) => acs.copt.debug, AccountCredential::RemoveWebauthn(acs) => acs.copt.debug,
AccountCredential::RegisterTotp(acs) => acs.copt.debug, AccountCredential::RegisterTotp(acs) => acs.copt.debug,
AccountCredential::RemoveTotp(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, AccountCredential::Status(acs) => acs.copt.debug,
}, },
AccountOpt::Radius(acopt) => match acopt { 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) => { AccountCredential::Status(acsopt) => {
let client = acsopt.copt.to_client(); let client = acsopt.copt.to_client();
match client.idm_account_get_credential_status(acsopt.aopts.account_id.as_str()) match client.idm_account_get_credential_status(acsopt.aopts.account_id.as_str())

View file

@ -142,6 +142,23 @@ impl LoginOpt {
client.auth_step_password(password.as_str()) client.auth_step_password(password.as_str())
} }
fn do_backup_code(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
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<AuthResponse, ClientError> { fn do_totp(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
let totp = loop { let totp = loop {
print!("Enter TOTP: "); print!("Enter TOTP: ");
@ -264,6 +281,7 @@ impl LoginOpt {
let res = match choice { let res = match choice {
AuthAllowed::Anonymous => client.auth_step_anonymous(), AuthAllowed::Anonymous => client.auth_step_anonymous(),
AuthAllowed::Password => self.do_password(&mut client), AuthAllowed::Password => self.do_password(&mut client),
AuthAllowed::BackupCode => self.do_backup_code(&mut client),
AuthAllowed::Totp => self.do_totp(&mut client), AuthAllowed::Totp => self.do_totp(&mut client),
AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()), AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()),
}; };

View file

@ -76,8 +76,6 @@ pub enum GroupOpt {
Posix(GroupPosix), Posix(GroupPosix),
} }
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
pub struct AccountCommonOpt { pub struct AccountCommonOpt {
#[structopt()] #[structopt()]
@ -181,6 +179,12 @@ pub enum AccountCredential {
/// and generate a new strong random password. /// and generate a new strong random password.
#[structopt(name = "reset_credential")] #[structopt(name = "reset_credential")]
GeneratePassword(AccountCredentialSet), 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)] #[derive(Debug, StructOpt)]
@ -407,4 +411,3 @@ pub enum KanidmClientOpt {
/// Unsafe - low level, raw database operations. /// Unsafe - low level, raw database operations.
Raw(RawOpt), Raw(RawOpt),
} }

View file

@ -499,9 +499,10 @@ impl CredHandler {
CredHandler::Anonymous => vec![AuthAllowed::Anonymous], CredHandler::Anonymous => vec![AuthAllowed::Anonymous],
CredHandler::Password(_, _) => vec![AuthAllowed::Password], CredHandler::Password(_, _) => vec![AuthAllowed::Password],
CredHandler::PasswordMfa(ref pw_mfa) => pw_mfa CredHandler::PasswordMfa(ref pw_mfa) => pw_mfa
.totp .backup_code
.iter() .iter()
.map(|_| AuthAllowed::Totp) .map(|_| AuthAllowed::BackupCode)
.chain(pw_mfa.totp.iter().map(|_| AuthAllowed::Totp))
.chain( .chain(
pw_mfa pw_mfa
.wan .wan

View file

@ -39,6 +39,7 @@ enum LoginState {
// MechChoice // MechChoice
// CredChoice // CredChoice
Password(bool), Password(bool),
BackupCode(bool),
Totp(TotpState), Totp(TotpState),
Webauthn(web_sys::CredentialRequestOptions), Webauthn(web_sys::CredentialRequestOptions),
Error(String, Option<String>), Error(String, Option<String>),
@ -51,6 +52,7 @@ pub enum LoginAppMsg {
Restart, Restart,
Begin, Begin,
PasswordSubmit, PasswordSubmit,
BackupCodeSubmit,
TotpSubmit, TotpSubmit,
WebauthnSubmit(PublicKeyCredential), WebauthnSubmit(PublicKeyCredential),
Start(String, AuthResponse), Start(String, AuthResponse),
@ -159,6 +161,23 @@ impl LoginApp {
</> </>
} }
} }
LoginState::BackupCode(enable) => {
html! {
<>
<div class="container">
<p>
{" Backup Code: "}
</p>
</div>
<div class="container">
<div>
<input id="backupcode" type="text" class="form-control" value=self.inputvalue oninput=self.link.callback(|e: InputData| LoginAppMsg::Input(e.value)) disabled=!enable />
<button type="button" class="btn btn-dark" onclick=self.link.callback(|_| LoginAppMsg::BackupCodeSubmit) disabled=!enable >{" Submit "}</button>
</div>
</div>
</>
}
}
LoginState::Totp(state) => { LoginState::Totp(state) => {
html! { html! {
<> <>
@ -321,6 +340,18 @@ impl Component for LoginApp {
self.inputvalue = "".to_string(); self.inputvalue = "".to_string();
true 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 => { LoginAppMsg::TotpSubmit => {
ConsoleService::log("totp"); ConsoleService::log("totp");
// Disable the button? // Disable the button?
@ -411,6 +442,9 @@ impl Component for LoginApp {
// Go to the password view. // Go to the password view.
self.state = LoginState::Password(true); self.state = LoginState::Password(true);
} }
AuthAllowed::BackupCode => {
self.state = LoginState::BackupCode(true);
}
AuthAllowed::Totp => { AuthAllowed::Totp => {
self.state = LoginState::Totp(TotpState::Enabled); self.state = LoginState::Totp(TotpState::Enabled);
} }