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
}
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> {
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<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(
&self,
id: &str,

View file

@ -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<AuthResponse, ClientError> {
tokio_block_on(self.asclient.auth_step_backup_code(backup_code))
}
pub fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
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(
&self,
id: &str,

View file

@ -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"),
}

View file

@ -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())

View file

@ -142,6 +142,23 @@ impl LoginOpt {
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> {
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()),
};

View file

@ -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),
}

View file

@ -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

View file

@ -39,6 +39,7 @@ enum LoginState {
// MechChoice
// CredChoice
Password(bool),
BackupCode(bool),
Totp(TotpState),
Webauthn(web_sys::CredentialRequestOptions),
Error(String, Option<String>),
@ -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! {
<>
<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) => {
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);
}