mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
495 backup codes cli extension (#517)
This commit is contained in:
parent
fc2824eec5
commit
620a1717a8
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue