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