mirror of
https://github.com/kanidm/kanidm.git
synced 2025-06-12 11:07:46 +02:00
383 164 authentication updates 9 (#956)
* implementation of passkeys as an auth mech * listing the current passkeys when asking to remove one * tweaking insecure dev server config so passkeys will work * Fix domain rename Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
This commit is contained in:
parent
f6fe2f575c
commit
4151897948
Cargo.lockCargo.toml
examples
kanidm_client
kanidm_proto
kanidm_tools
kanidmd
kanidmd_web_ui
4638
Cargo.lock
generated
4638
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -32,9 +32,14 @@ exclude = [
|
|||
|
||||
# ldap3_server = { path = "../ldap3_server" }
|
||||
|
||||
# webauthn-rs = { path = "../webauthn-rs" }
|
||||
|
||||
# webauthn-authenticator-rs = { path = "../webauthn-authenticator-rs" }
|
||||
# webauthn-rs = { path = "../webauthn-rs/webauthn-rs" }
|
||||
webauthn-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "7a8e6c6b351ab7544f08cf8ba48424baacee1360" }
|
||||
# webauthn-rs-core = { path = "../webauthn-rs/webauthn-rs-core" }
|
||||
webauthn-rs-core = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "7a8e6c6b351ab7544f08cf8ba48424baacee1360" }
|
||||
# webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" }
|
||||
webauthn-rs-proto = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "7a8e6c6b351ab7544f08cf8ba48424baacee1360" }
|
||||
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
|
||||
webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "7a8e6c6b351ab7544f08cf8ba48424baacee1360" }
|
||||
|
||||
# compact_jwt = { path = "../compact_jwt" }
|
||||
# compact_jwt = { git = "https://github.com/kanidm/compact-jwt.git" }
|
||||
|
|
|
@ -10,6 +10,6 @@ tls_key = "/tmp/kanidm/key.pem"
|
|||
# log_level = "quiet"
|
||||
log_level = "verbose"
|
||||
|
||||
domain = "idm.example.com"
|
||||
origin = "https://idm.example.com:8443"
|
||||
domain = "localhost"
|
||||
origin = "https://localhost:8443"
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ reqwest = { version = "^0.11.11", features=["cookies", "json", "native-tls"] }
|
|||
kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha.8" }
|
||||
serde = { version = "^1.0.139", features = ["derive"] }
|
||||
serde_json = "^1.0.82"
|
||||
tokio = { version = "^1.20.0", features = ["rt", "net", "time", "macros", "sync", "signal"] }
|
||||
toml = "^0.5.9"
|
||||
uuid = { version = "^1.1.2", features = ["serde", "v4"] }
|
||||
url = { version = "^2.2.2", features = ["serde"] }
|
||||
webauthn-rs = "^0.3.2"
|
||||
tokio = { version = "^1.20.0", features = ["rt", "net", "time", "macros", "sync", "signal"] }
|
||||
webauthn-rs-proto = { version = "0.4.2-beta.3", features = ["wasm"] }
|
||||
|
||||
|
|
|
@ -38,9 +38,8 @@ use uuid::Uuid;
|
|||
pub use reqwest::StatusCode;
|
||||
|
||||
use kanidm_proto::v1::*;
|
||||
use webauthn_rs::proto::{
|
||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||
RequestChallengeResponse,
|
||||
use webauthn_rs_proto::{
|
||||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
||||
};
|
||||
|
||||
pub const APPLICATION_JSON: &str = "application/json";
|
||||
|
@ -85,7 +84,7 @@ pub struct KanidmClientBuilder {
|
|||
pub struct KanidmClient {
|
||||
pub(crate) client: reqwest::Client,
|
||||
pub(crate) addr: String,
|
||||
pub(crate) origin: String,
|
||||
pub(crate) origin: Url,
|
||||
pub(crate) builder: KanidmClientBuilder,
|
||||
pub(crate) bearer_token: RwLock<Option<String>>,
|
||||
pub(crate) auth_session_id: RwLock<Option<String>>,
|
||||
|
@ -374,10 +373,11 @@ impl KanidmClientBuilder {
|
|||
|
||||
// Now get the origin.
|
||||
#[allow(clippy::expect_used)]
|
||||
let uri = Url::parse(&address).expect("can not fail");
|
||||
let uri = Url::parse(&address).expect("failed to parse address");
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let origin = uri.origin().unicode_serialization();
|
||||
let origin =
|
||||
Url::parse(&uri.origin().ascii_serialization()).expect("failed to parse origin");
|
||||
|
||||
Ok(KanidmClient {
|
||||
client,
|
||||
|
@ -391,8 +391,8 @@ impl KanidmClientBuilder {
|
|||
}
|
||||
|
||||
impl KanidmClient {
|
||||
pub fn get_origin(&self) -> &str {
|
||||
self.origin.as_str()
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> &str {
|
||||
|
@ -919,12 +919,29 @@ impl KanidmClient {
|
|||
r
|
||||
}
|
||||
|
||||
pub async fn auth_step_webauthn_complete(
|
||||
pub async fn auth_step_securitykey_complete(
|
||||
&self,
|
||||
pkc: PublicKeyCredential,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let auth_req = AuthRequest {
|
||||
step: AuthStep::Cred(AuthCredential::Webauthn(pkc)),
|
||||
step: AuthStep::Cred(AuthCredential::SecurityKey(pkc)),
|
||||
};
|
||||
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_passkey_complete(
|
||||
&self,
|
||||
pkc: PublicKeyCredential,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let auth_req = AuthRequest {
|
||||
step: AuthStep::Cred(AuthCredential::Passkey(pkc)),
|
||||
};
|
||||
let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/auth", auth_req).await;
|
||||
|
||||
|
@ -1091,7 +1108,7 @@ impl KanidmClient {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn auth_webauthn_begin(
|
||||
pub async fn auth_passkey_begin(
|
||||
&self,
|
||||
ident: &str,
|
||||
) -> Result<RequestChallengeResponse, ClientError> {
|
||||
|
@ -1100,28 +1117,25 @@ impl KanidmClient {
|
|||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if !mechs.contains(&AuthMech::Webauthn) {
|
||||
if !mechs.contains(&AuthMech::Passkey) {
|
||||
debug!("Webauthn mech not presented");
|
||||
return Err(ClientError::AuthenticationFailed);
|
||||
}
|
||||
|
||||
let state = match self.auth_step_begin(AuthMech::Webauthn).await {
|
||||
let state = match self.auth_step_begin(AuthMech::Passkey).await {
|
||||
Ok(mut s) => s.pop(),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// State is now a set of auth continues.
|
||||
match state {
|
||||
Some(AuthAllowed::Webauthn(r)) => Ok(r),
|
||||
Some(AuthAllowed::Passkey(r)) => Ok(r),
|
||||
_ => Err(ClientError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth_webauthn_complete(
|
||||
&self,
|
||||
pkc: PublicKeyCredential,
|
||||
) -> Result<(), ClientError> {
|
||||
let r = self.auth_step_webauthn_complete(pkc).await?;
|
||||
pub async fn auth_passkey_complete(&self, pkc: PublicKeyCredential) -> Result<(), ClientError> {
|
||||
let r = self.auth_step_passkey_complete(pkc).await?;
|
||||
match r.state {
|
||||
AuthState::Success(_token) => Ok(()),
|
||||
_ => Err(ClientError::AuthenticationFailed),
|
||||
|
@ -1504,6 +1518,7 @@ impl KanidmClient {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn idm_account_primary_credential_register_webauthn(
|
||||
&self,
|
||||
id: &str,
|
||||
|
@ -1561,6 +1576,7 @@ impl KanidmClient {
|
|||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn idm_account_primary_credential_generate_backup_code(
|
||||
&self,
|
||||
|
@ -1722,6 +1738,36 @@ impl KanidmClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_passkey_init(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = CURequest::PasskeyInit;
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_passkey_finish(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
label: String,
|
||||
registration: RegisterPublicKeyCredential,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = CURequest::PasskeyFinish(label, registration);
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_passkey_remove(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
uuid: Uuid,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = CURequest::PasskeyRemove(uuid);
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_commit(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
|
|
|
@ -10,12 +10,16 @@ documentation = "https://docs.rs/kanidm_proto/latest/kanidm_proto/"
|
|||
homepage = "https://github.com/kanidm/kanidm/"
|
||||
repository = "https://github.com/kanidm/kanidm/"
|
||||
|
||||
[features]
|
||||
wasm = ["webauthn-rs-proto/wasm"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0.139", features = ["derive"] }
|
||||
serde_json = "^1.0.82"
|
||||
uuid = { version = "^1.1.2", features = ["serde"] }
|
||||
base32 = "^0.4.0"
|
||||
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }
|
||||
base64urlsafedata = "0.1.0"
|
||||
webauthn-rs-proto = "0.4.2-beta.3"
|
||||
# Can not upgrade due to breaking timezone apis.
|
||||
time = { version = "=0.2.27", features = ["serde", "std"] }
|
||||
url = { version = "^2.2.2", features = ["serde"] }
|
||||
|
|
|
@ -11,3 +11,5 @@
|
|||
pub mod messages;
|
||||
pub mod oauth2;
|
||||
pub mod v1;
|
||||
|
||||
pub use webauthn_rs_proto as webauthn;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use url::Url;
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub enum CodeChallengeMethod {
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::cmp::Ordering;
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::{
|
||||
use webauthn_rs_proto::{
|
||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||
RequestChallengeResponse,
|
||||
};
|
||||
|
@ -202,11 +202,8 @@ pub enum AuthType {
|
|||
UnixPassword,
|
||||
Password,
|
||||
GeneratedPassword,
|
||||
Webauthn,
|
||||
PasswordMfa,
|
||||
// PasswordWebauthn,
|
||||
// WebauthnVerified,
|
||||
// PasswordWebauthnVerified,
|
||||
Passkey,
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthType {
|
||||
|
@ -216,8 +213,8 @@ impl fmt::Display for AuthType {
|
|||
AuthType::UnixPassword => write!(f, "unixpassword"),
|
||||
AuthType::Password => write!(f, "password"),
|
||||
AuthType::GeneratedPassword => write!(f, "generatedpassword"),
|
||||
AuthType::Webauthn => write!(f, "webauthn"),
|
||||
AuthType::PasswordMfa => write!(f, "passwordmfa"),
|
||||
AuthType::Passkey => write!(f, "passkey"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -380,7 +377,7 @@ pub struct AccountOrgPersonExtend {
|
|||
pub enum CredentialDetailType {
|
||||
Password,
|
||||
GeneratedPassword,
|
||||
Webauthn(Vec<String>),
|
||||
Passkey(Vec<String>),
|
||||
/// totp, webauthn
|
||||
PasswordMfa(bool, Vec<String>, usize),
|
||||
}
|
||||
|
@ -388,7 +385,6 @@ pub enum CredentialDetailType {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CredentialDetail {
|
||||
pub uuid: Uuid,
|
||||
pub claims: Vec<String>,
|
||||
pub type_: CredentialDetailType,
|
||||
}
|
||||
|
||||
|
@ -404,11 +400,11 @@ impl fmt::Display for CredentialDetail {
|
|||
match &self.type_ {
|
||||
CredentialDetailType::Password => writeln!(f, "password: set"),
|
||||
CredentialDetailType::GeneratedPassword => writeln!(f, "generated password: set"),
|
||||
CredentialDetailType::Webauthn(labels) => {
|
||||
CredentialDetailType::Passkey(labels) => {
|
||||
if labels.is_empty() {
|
||||
writeln!(f, "webauthn: no authenticators")
|
||||
writeln!(f, "passkeys: none registered")
|
||||
} else {
|
||||
writeln!(f, "webauthn:")?;
|
||||
writeln!(f, "passkeys:")?;
|
||||
for label in labels {
|
||||
writeln!(f, " * {}", label)?;
|
||||
}
|
||||
|
@ -441,6 +437,12 @@ impl fmt::Display for CredentialDetail {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct PasskeyDetail {
|
||||
pub uuid: Uuid,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CredentialStatus {
|
||||
pub creds: Vec<CredentialDetail>,
|
||||
|
@ -597,8 +599,10 @@ pub enum AuthCredential {
|
|||
Anonymous,
|
||||
Password(String),
|
||||
Totp(u32),
|
||||
Webauthn(PublicKeyCredential),
|
||||
SecurityKey(PublicKeyCredential),
|
||||
BackupCode(String),
|
||||
// Should this just be discoverable?
|
||||
Passkey(PublicKeyCredential),
|
||||
}
|
||||
|
||||
impl fmt::Debug for AuthCredential {
|
||||
|
@ -607,8 +611,9 @@ impl fmt::Debug for AuthCredential {
|
|||
AuthCredential::Anonymous => write!(fmt, "Anonymous"),
|
||||
AuthCredential::Password(_) => write!(fmt, "Password(_)"),
|
||||
AuthCredential::Totp(_) => write!(fmt, "TOTP(_)"),
|
||||
AuthCredential::Webauthn(_) => write!(fmt, "Webauthn(_)"),
|
||||
AuthCredential::SecurityKey(_) => write!(fmt, "SecurityKey(_)"),
|
||||
AuthCredential::BackupCode(_) => write!(fmt, "BackupCode(_)"),
|
||||
AuthCredential::Passkey(_) => write!(fmt, "Passkey(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -619,9 +624,7 @@ pub enum AuthMech {
|
|||
Anonymous,
|
||||
Password,
|
||||
PasswordMfa,
|
||||
Webauthn,
|
||||
// WebauthnVerified,
|
||||
// PasswordWebauthnVerified
|
||||
Passkey,
|
||||
}
|
||||
|
||||
impl PartialEq for AuthMech {
|
||||
|
@ -634,9 +637,9 @@ impl fmt::Display for AuthMech {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AuthMech::Anonymous => write!(f, "Anonymous (no credentials)"),
|
||||
AuthMech::Password => write!(f, "Passwold Only"),
|
||||
AuthMech::PasswordMfa => write!(f, "TOTP or Token, and Password"),
|
||||
AuthMech::Webauthn => write!(f, "Webauthn Token"),
|
||||
AuthMech::Password => write!(f, "Password"),
|
||||
AuthMech::PasswordMfa => write!(f, "TOTP/Backup Code and Password"),
|
||||
AuthMech::Passkey => write!(f, "Passkey"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -669,7 +672,8 @@ pub enum AuthAllowed {
|
|||
BackupCode,
|
||||
Password,
|
||||
Totp,
|
||||
Webauthn(RequestChallengeResponse),
|
||||
SecurityKey(RequestChallengeResponse),
|
||||
Passkey(RequestChallengeResponse),
|
||||
}
|
||||
|
||||
impl PartialEq for AuthAllowed {
|
||||
|
@ -695,9 +699,11 @@ impl Ord for AuthAllowed {
|
|||
(_, AuthAllowed::BackupCode) => Ordering::Greater,
|
||||
(AuthAllowed::Totp, _) => Ordering::Less,
|
||||
(_, AuthAllowed::Totp) => Ordering::Greater,
|
||||
(AuthAllowed::Webauthn(_), _) => Ordering::Less,
|
||||
(AuthAllowed::SecurityKey(_), _) => Ordering::Less,
|
||||
(_, AuthAllowed::SecurityKey(_)) => Ordering::Greater,
|
||||
(AuthAllowed::Passkey(_), _) => Ordering::Less,
|
||||
// Unreachable
|
||||
// (_, AuthAllowed::Webauthn(_)) => Ordering::Greater,
|
||||
// (_, AuthAllowed::Passkey(_)) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -716,7 +722,8 @@ impl fmt::Display for AuthAllowed {
|
|||
AuthAllowed::Password => write!(f, "Password"),
|
||||
AuthAllowed::BackupCode => write!(f, "Backup Code"),
|
||||
AuthAllowed::Totp => write!(f, "TOTP"),
|
||||
AuthAllowed::Webauthn(_) => write!(f, "Webauthn Token"),
|
||||
AuthAllowed::SecurityKey(_) => write!(f, "Security Token"),
|
||||
AuthAllowed::Passkey(_) => write!(f, "Passkey"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -751,12 +758,6 @@ pub enum SetCredentialRequest {
|
|||
TotpVerify(Uuid, u32),
|
||||
TotpAcceptSha1(Uuid),
|
||||
TotpRemove,
|
||||
// Start the rego.
|
||||
WebauthnBegin(String),
|
||||
// Finish it.
|
||||
WebauthnRegister(Uuid, RegisterPublicKeyCredential),
|
||||
// Remove
|
||||
WebauthnRemove(String),
|
||||
BackupCodeGenerate,
|
||||
BackupCodeRemove,
|
||||
}
|
||||
|
@ -817,7 +818,7 @@ pub enum SetCredentialResponse {
|
|||
Token(String),
|
||||
TotpCheck(Uuid, TotpSecret),
|
||||
TotpInvalidSha1(Uuid),
|
||||
WebauthnCreateChallenge(Uuid, CreationChallengeResponse),
|
||||
SecurityKeyCreateChallenge(Uuid, CreationChallengeResponse),
|
||||
BackupCodes(Vec<String>),
|
||||
}
|
||||
|
||||
|
@ -843,6 +844,9 @@ pub enum CURequest {
|
|||
TotpRemove,
|
||||
BackupCodeGenerate,
|
||||
BackupCodeRemove,
|
||||
PasskeyInit,
|
||||
PasskeyFinish(String, RegisterPublicKeyCredential),
|
||||
PasskeyRemove(Uuid),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CURequest {
|
||||
|
@ -857,6 +861,9 @@ impl fmt::Debug for CURequest {
|
|||
CURequest::TotpRemove => "CURequest::TotpRemove",
|
||||
CURequest::BackupCodeGenerate => "CURequest::BackupCodeGenerate",
|
||||
CURequest::BackupCodeRemove => "CURequest::BackupCodeRemove",
|
||||
CURequest::PasskeyInit => "CURequest::PasskeyInit",
|
||||
CURequest::PasskeyFinish(_, _) => "CURequest::PasskeyFinish",
|
||||
CURequest::PasskeyRemove(_) => "CURequest::PasskeyRemove",
|
||||
};
|
||||
writeln!(f, "{}", t)
|
||||
}
|
||||
|
@ -870,6 +877,7 @@ pub enum CURegState {
|
|||
TotpTryAgain,
|
||||
TotpInvalidSha1,
|
||||
BackupCodes(Vec<String>),
|
||||
Passkey(CreationChallengeResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
@ -878,6 +886,7 @@ pub struct CUStatus {
|
|||
pub displayname: String,
|
||||
pub can_commit: bool,
|
||||
pub primary: Option<CredentialDetail>,
|
||||
pub passkeys: Vec<PasskeyDetail>,
|
||||
pub mfaregstate: CURegState,
|
||||
}
|
||||
|
||||
|
|
|
@ -48,9 +48,13 @@ zxcvbn = "^2.2.1"
|
|||
|
||||
dialoguer = "^0.10.1"
|
||||
|
||||
webauthn-authenticator-rs = "^0.3.0-alpha.12"
|
||||
# webauthn-authenticator-rs = { version = "0.4.2-beta.3", features = ["u2fhid"] }
|
||||
webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-rs.git", rev = "7a8e6c6b351ab7544f08cf8ba48424baacee1360", features = ["u2fhid"] }
|
||||
|
||||
tokio = { version = "^1.20.0", features = ["rt", "macros"] }
|
||||
|
||||
url = { version = "^2.2.2", features = ["serde"] }
|
||||
uuid = "^1.1.2"
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "^3.2", features = ["derive"] }
|
||||
|
|
|
@ -15,6 +15,9 @@ use std::fmt::{self, Debug};
|
|||
use std::str::FromStr;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use webauthn_authenticator_rs::{u2fhid::U2FHid, WebauthnAuthenticator};
|
||||
|
||||
impl AccountOpt {
|
||||
pub fn debug(&self) -> bool {
|
||||
|
@ -617,6 +620,8 @@ enum CUAction {
|
|||
TotpRemove,
|
||||
BackupCodes,
|
||||
Remove,
|
||||
Passkey,
|
||||
PasskeyRemove,
|
||||
End,
|
||||
Commit,
|
||||
}
|
||||
|
@ -628,13 +633,17 @@ impl fmt::Display for CUAction {
|
|||
r#"
|
||||
help (h, ?) - Display this help
|
||||
status (ls, st) - Show the status of the credential
|
||||
end (quit, exit, x, q) - End, without saving any changes
|
||||
commit (save) - Commit the changes to the credential
|
||||
-- Password and MFA
|
||||
password (passwd, pass, pw) - Set a new password
|
||||
totp - Generate a new totp, requires a password to be set
|
||||
totp remove (totp rm, trm) - Remove the TOTP of this account
|
||||
backup codes (bcg, bcode) - (Re)generate backup codes for this account
|
||||
remove (rm) - Remove only the primary credential
|
||||
end (quit, exit, x, q) - End, without saving any changes
|
||||
commit (save) - Commit the changes to the credential
|
||||
remove (rm) - Remove only the password based credential
|
||||
-- Passkeys
|
||||
passkey (pk) - Add a new Passkey
|
||||
passkey remove (passkey rm, pkrm) - Remove a Passkey
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
@ -648,13 +657,15 @@ impl FromStr for CUAction {
|
|||
match s.as_str() {
|
||||
"help" | "h" | "?" => Ok(CUAction::Help),
|
||||
"status" | "ls" | "st" => Ok(CUAction::Status),
|
||||
"end" | "quit" | "exit" | "x" | "q" => Ok(CUAction::End),
|
||||
"commit" | "save" => Ok(CUAction::Commit),
|
||||
"password" | "passwd" | "pass" | "pw" => Ok(CUAction::Password),
|
||||
"totp" => Ok(CUAction::Totp),
|
||||
"totp remove" | "totp rm" | "trm" => Ok(CUAction::TotpRemove),
|
||||
"backup codes" | "bcode" | "bcg" => Ok(CUAction::BackupCodes),
|
||||
"remove" | "rm" => Ok(CUAction::Remove),
|
||||
"end" | "quit" | "exit" | "x" | "q" => Ok(CUAction::End),
|
||||
"commit" | "save" => Ok(CUAction::Commit),
|
||||
"passkey" | "pk" => Ok(CUAction::Passkey),
|
||||
"passkey remove" | "passkey rm" | "pkrm" => Ok(CUAction::PasskeyRemove),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -823,6 +834,57 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
|
|||
// Done!
|
||||
}
|
||||
|
||||
async fn passkey_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
|
||||
let pk_reg = match client
|
||||
.idm_account_credential_update_passkey_init(session_token)
|
||||
.await
|
||||
{
|
||||
Ok(CUStatus {
|
||||
mfaregstate: CURegState::Passkey(pk_reg),
|
||||
..
|
||||
}) => pk_reg,
|
||||
Ok(status) => {
|
||||
debug!(?status);
|
||||
eprintln!("An error occured -> InvalidState");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("An error occured -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Setup and connect to the webauthn handler ...
|
||||
let mut wa = WebauthnAuthenticator::new(U2FHid::new());
|
||||
|
||||
eprintln!("Your authenticator will now flash for you to interact with.");
|
||||
eprintln!("You may be asked to enter the PIN for your device.");
|
||||
|
||||
let rego = match wa.do_registration(client.get_origin().clone(), pk_reg) {
|
||||
Ok(rego) => rego,
|
||||
Err(e) => {
|
||||
error!("Error Signing -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let label: String = Input::new()
|
||||
.with_prompt("\nEnter a label for this Passkey # ")
|
||||
.allow_empty(false)
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
match client
|
||||
.idm_account_credential_update_passkey_finish(session_token, label, rego)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("success"),
|
||||
Err(e) => {
|
||||
eprintln!("An error occured -> {:?}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// For webauthn later
|
||||
|
||||
/*
|
||||
|
@ -880,6 +942,7 @@ fn display_status(status: CUStatus) {
|
|||
can_commit,
|
||||
primary,
|
||||
mfaregstate: _,
|
||||
passkeys,
|
||||
} = status;
|
||||
|
||||
println!("spn: {}", spn);
|
||||
|
@ -891,6 +954,14 @@ fn display_status(status: CUStatus) {
|
|||
println!("Primary Credential:");
|
||||
println!(" not set");
|
||||
}
|
||||
println!("Passkeys:");
|
||||
if passkeys.is_empty() {
|
||||
println!(" not set");
|
||||
} else {
|
||||
for pk in passkeys {
|
||||
println!(" {} ({})", pk.tag, pk.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
// We may need to be able to display if there are dangling
|
||||
// curegstates, but the cli ui statemachine can match the
|
||||
|
@ -1035,6 +1106,54 @@ async fn credential_update_exec(
|
|||
println!("Primary credential was NOT removed");
|
||||
}
|
||||
}
|
||||
CUAction::Passkey => passkey_enroll_prompt(&session_token, &client).await,
|
||||
CUAction::PasskeyRemove => {
|
||||
// TODO: make this a scrollable selector with a "cancel" option as the default
|
||||
match client
|
||||
.idm_account_credential_update_status(&session_token)
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.passkeys.is_empty() {
|
||||
println!("No passkeys are configured for this user");
|
||||
return
|
||||
}
|
||||
println!("Current passkeys:");
|
||||
for pk in status.passkeys {
|
||||
println!(" {} ({})", pk.tag, pk.uuid);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("An error occured pulling existing credentials -> {:?}", e);
|
||||
}
|
||||
}
|
||||
let uuid_s: String = Input::new()
|
||||
.with_prompt("\nEnter the UUID of the Passkey to remove (blank to stop) # ")
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
if input.is_empty() || Uuid::parse_str(input).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("This is not a valid UUID")
|
||||
}
|
||||
})
|
||||
.allow_empty(true)
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
// Remeber, if it's NOT a valid uuid, it must have been empty as a termination.
|
||||
if let Ok(uuid) = Uuid::parse_str(&uuid_s) {
|
||||
if let Err(e) = client
|
||||
.idm_account_credential_update_passkey_remove(&session_token, uuid)
|
||||
.await
|
||||
{
|
||||
eprintln!("An error occured -> {:?}", e);
|
||||
} else {
|
||||
println!("success");
|
||||
}
|
||||
} else {
|
||||
println!("Passkeys were NOT changed");
|
||||
}
|
||||
}
|
||||
CUAction::End => {
|
||||
println!("Changes were NOT saved.");
|
||||
break;
|
||||
|
|
|
@ -11,7 +11,9 @@ use std::io::ErrorKind;
|
|||
use std::io::{self, BufReader, BufWriter, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use webauthn_authenticator_rs::{u2fhid::U2FHid, RequestChallengeResponse, WebauthnAuthenticator};
|
||||
use webauthn_authenticator_rs::{
|
||||
prelude::RequestChallengeResponse, u2fhid::U2FHid, WebauthnAuthenticator,
|
||||
};
|
||||
|
||||
use dialoguer::{theme::ColorfulTheme, Select};
|
||||
|
||||
|
@ -190,7 +192,7 @@ impl LoginOpt {
|
|||
client.auth_step_totp(totp).await
|
||||
}
|
||||
|
||||
async fn do_webauthn(
|
||||
async fn do_passkey(
|
||||
&self,
|
||||
client: &mut KanidmClient,
|
||||
pkr: RequestChallengeResponse,
|
||||
|
@ -198,13 +200,30 @@ impl LoginOpt {
|
|||
let mut wa = WebauthnAuthenticator::new(U2FHid::new());
|
||||
println!("Your authenticator will now flash for you to interact with it.");
|
||||
let auth = wa
|
||||
.do_authentication(client.get_origin(), pkr)
|
||||
.do_authentication(client.get_origin().clone(), pkr)
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to interact with webauthn device. -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
client.auth_step_webauthn_complete(auth).await
|
||||
client.auth_step_passkey_complete(auth).await
|
||||
}
|
||||
|
||||
async fn do_securitykey(
|
||||
&self,
|
||||
client: &mut KanidmClient,
|
||||
pkr: RequestChallengeResponse,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let mut wa = WebauthnAuthenticator::new(U2FHid::new());
|
||||
println!("Your authenticator will now flash for you to interact with it.");
|
||||
let auth = wa
|
||||
.do_authentication(client.get_origin().clone(), pkr)
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to interact with webauthn device. -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
client.auth_step_securitykey_complete(auth).await
|
||||
}
|
||||
|
||||
pub async fn exec(&self) {
|
||||
|
@ -297,7 +316,10 @@ impl LoginOpt {
|
|||
AuthAllowed::Password => self.do_password(&mut client).await,
|
||||
AuthAllowed::BackupCode => self.do_backup_code(&mut client).await,
|
||||
AuthAllowed::Totp => self.do_totp(&mut client).await,
|
||||
AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()).await,
|
||||
AuthAllowed::Passkey(chal) => self.do_passkey(&mut client, chal.clone()).await,
|
||||
AuthAllowed::SecurityKey(chal) => {
|
||||
self.do_securitykey(&mut client, chal.clone()).await
|
||||
}
|
||||
};
|
||||
|
||||
// Now update state.
|
||||
|
|
|
@ -18,6 +18,7 @@ path = "src/lib.rs"
|
|||
async-std = { version = "^1.12.0", features = ["tokio1"] }
|
||||
async-trait = "^0.1.53"
|
||||
base64 = "^0.13.0"
|
||||
base64urlsafedata = "0.1.0"
|
||||
chrono = "^0.4.19"
|
||||
compact_jwt = "^0.2.3"
|
||||
compiled-uuid = "0.1.2"
|
||||
|
@ -53,14 +54,15 @@ tokio = { version = "^1.20.0", features = ["net", "sync", "time"] }
|
|||
tokio-util = { version = "^0.7.3", features = ["codec"] }
|
||||
toml = "^0.5.9"
|
||||
touch = "^0.0.1"
|
||||
tracing = { version = "^0.1.35", features = ["attributes"] }
|
||||
tracing = { version = "^0.1.35", features = ["attributes", "max_level_trace", "release_max_level_debug"] }
|
||||
tracing-serde = "^0.1.3"
|
||||
tracing-subscriber = { version = "^0.3.14", features = ["env-filter"] }
|
||||
url = { version = "^2.2.2", features = ["serde"] }
|
||||
urlencoding = "2.1.0"
|
||||
uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
|
||||
validator = { version = "^0.15.0", features = ["phone"] }
|
||||
webauthn-rs = "^0.3.2"
|
||||
webauthn-rs = { version = "0.4.2-beta.3", features = ["resident_key_support"] }
|
||||
webauthn-rs-core = "0.4.2-beta.3"
|
||||
zxcvbn = "^2.2.1"
|
||||
|
||||
# because windows really can't build without the bundled one
|
||||
|
@ -85,7 +87,7 @@ users = "^0.11.0"
|
|||
[dev-dependencies]
|
||||
criterion = { version = "^0.3.6", features = ["html_reports"] }
|
||||
# For testing webauthn
|
||||
webauthn-authenticator-rs = "^0.3.2"
|
||||
webauthn-authenticator-rs = "0.4.2-beta.3"
|
||||
|
||||
[build-dependencies]
|
||||
profiles = { path = "../../profiles" }
|
||||
|
|
|
@ -79,7 +79,7 @@ impl QueryServerReadV1 {
|
|||
// ! Ideally, this function takes &self, uat, req, and then a `uuid` argument that is a `&str` of the hyphenated uuid.
|
||||
// ! Then we just don't skip uuid, and we don't have to do the custom `fields(..)` stuff in this macro call.
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "search",
|
||||
skip(self, uat, req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -126,7 +126,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "auth",
|
||||
skip(self, sessionid, req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -171,7 +171,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "online_backup",
|
||||
skip(self, msg, outpath, versions)
|
||||
fields(uuid = ?msg.eventid)
|
||||
|
@ -290,7 +290,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "whoami",
|
||||
skip(self, uat, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -353,8 +353,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalsearch",
|
||||
level = "info",
|
||||
name = "search2",
|
||||
skip(self, uat, filter, attrs, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -401,8 +401,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalsearchrecycled",
|
||||
level = "info",
|
||||
name = "search_recycled",
|
||||
skip(self, uat, filter, attrs, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -450,8 +450,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalradiusread",
|
||||
level = "info",
|
||||
name = "radius_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -515,8 +515,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalradiustokenread",
|
||||
level = "info",
|
||||
name = "radius_token_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -567,8 +567,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalunixusertokenread",
|
||||
level = "info",
|
||||
name = "unix_user_token_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -622,8 +622,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalunixgrouptokenread",
|
||||
level = "info",
|
||||
name = "unix_group_token_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -676,8 +676,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalsshkeyread",
|
||||
level = "info",
|
||||
name = "ssh_key_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -743,8 +743,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internalsshkeytagread",
|
||||
level = "info",
|
||||
name = "ssh_key_tag_read",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -813,8 +813,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountunixauth",
|
||||
level = "info",
|
||||
name = "idm_account_unix_auth",
|
||||
skip(self, uat, uuid_or_name, cred, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -867,8 +867,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialstatus",
|
||||
level = "info",
|
||||
name = "idm_credential_status",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -918,8 +918,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmbackupcodeview",
|
||||
level = "info",
|
||||
name = "idm_backup_code_view",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -969,8 +969,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdatestatus",
|
||||
level = "info",
|
||||
name = "idm_credential_update_status",
|
||||
skip(self, session_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1001,8 +1001,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdate",
|
||||
level = "info",
|
||||
name = "idm_credential_update",
|
||||
skip(self, session_token, scr, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1103,6 +1103,33 @@ impl QueryServerReadV1 {
|
|||
);
|
||||
e
|
||||
}),
|
||||
CURequest::PasskeyInit => idms_cred_update
|
||||
.credential_passkey_init(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_passkey_init",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::PasskeyFinish(label, rpkc) => idms_cred_update
|
||||
.credential_passkey_finish(&session_token, ct, label, rpkc)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_passkey_init",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::PasskeyRemove(uuid) => idms_cred_update
|
||||
.credential_passkey_remove(&session_token, ct, uuid)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_passkey_init",
|
||||
);
|
||||
e
|
||||
}),
|
||||
}
|
||||
.map(|sta| sta.into())
|
||||
});
|
||||
|
@ -1110,7 +1137,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_authorise",
|
||||
skip(self, uat, auth_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1143,7 +1170,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_authorise_permit",
|
||||
skip(self, uat, consent_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1175,7 +1202,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_authorise_reject",
|
||||
skip(self, uat, consent_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1207,7 +1234,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_token_exchange",
|
||||
skip(self, client_authz, token_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1228,7 +1255,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_token_introspect",
|
||||
skip(self, client_authz, intr_req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1249,7 +1276,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_openid_userinfo",
|
||||
skip(self, client_id, client_authz, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1269,7 +1296,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_openid_discovery",
|
||||
skip(self, client_id, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1287,7 +1314,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_openid_publickey",
|
||||
skip(self, client_id, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1305,8 +1332,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "domain_display_name",
|
||||
level = "info",
|
||||
name = "get_domain_display_name",
|
||||
skip(self, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1319,7 +1346,7 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "auth_valid",
|
||||
skip(self, uat, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1345,8 +1372,8 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "ldaprequest",
|
||||
level = "info",
|
||||
name = "ldap_request",
|
||||
skip(self, eventid, protomsg, uat)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
|
|
@ -18,8 +18,7 @@ use crate::event::{
|
|||
};
|
||||
use crate::idm::event::{
|
||||
AcceptSha1TotpEvent, GeneratePasswordEvent, GenerateTotpEvent, PasswordChangeEvent,
|
||||
RegenerateRadiusSecretEvent, RemoveTotpEvent, RemoveWebauthnEvent, UnixPasswordChangeEvent,
|
||||
VerifyTotpEvent, WebauthnDoRegisterEvent, WebauthnInitRegisterEvent,
|
||||
RegenerateRadiusSecretEvent, RemoveTotpEvent, UnixPasswordChangeEvent, VerifyTotpEvent,
|
||||
};
|
||||
use crate::modify::{Modify, ModifyInvalid, ModifyList};
|
||||
use crate::value::{PartialValue, Value};
|
||||
|
@ -168,7 +167,7 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "create",
|
||||
skip(self, uat, req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -210,11 +209,17 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "modify",
|
||||
skip(self, uat, req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_modify(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
req: ModifyRequest,
|
||||
_eventid: Uuid,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
|
||||
let res = spanned!("actors::v1_write::handle<ModifyMessage>", {
|
||||
|
@ -246,7 +251,7 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "delete",
|
||||
skip(self, uat, req, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -285,12 +290,18 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "patch",
|
||||
skip(self, uat, filter, update, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_internalpatch(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
update: ProtoEntry,
|
||||
_eventid: Uuid,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
// Given a protoEntry, turn this into a modification set.
|
||||
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
|
||||
|
@ -333,8 +344,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "internal_delete",
|
||||
level = "info",
|
||||
name = "delete2",
|
||||
skip(self, uat, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -373,8 +384,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "reviverecycled",
|
||||
level = "info",
|
||||
name = "revive_recycled",
|
||||
skip(self, uat, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -415,8 +426,8 @@ impl QueryServerWriteV1 {
|
|||
|
||||
// === IDM native types for modifications
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "credentialset",
|
||||
level = "info",
|
||||
name = "credential_set",
|
||||
skip(self, uat, uuid_or_name, sac, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -561,7 +572,8 @@ impl QueryServerWriteV1 {
|
|||
.remove_account_totp(&rte)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
SetCredentialRequest::WebauthnBegin(label) => {
|
||||
/*
|
||||
SetCredentialRequest::SecurityKeyBegin(label) => {
|
||||
let wre = WebauthnInitRegisterEvent::from_parts(
|
||||
// &idms_prox_write.qs_write,
|
||||
ident,
|
||||
|
@ -579,7 +591,7 @@ impl QueryServerWriteV1 {
|
|||
.reg_account_webauthn_init(&wre, ct)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
SetCredentialRequest::WebauthnRegister(uuid, rpkc) => {
|
||||
SetCredentialRequest::SecurityKeyRegister(uuid, rpkc) => {
|
||||
let wre = WebauthnDoRegisterEvent::from_parts(
|
||||
// &idms_prox_write.qs_write,
|
||||
ident,
|
||||
|
@ -598,7 +610,7 @@ impl QueryServerWriteV1 {
|
|||
.reg_account_webauthn_complete(&wre)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
SetCredentialRequest::WebauthnRemove(label) => {
|
||||
SetCredentialRequest::SecurityKeyRemove(label) => {
|
||||
let rwe = RemoveWebauthnEvent::from_parts(
|
||||
// &idms_prox_write.qs_write,
|
||||
ident,
|
||||
|
@ -616,6 +628,7 @@ impl QueryServerWriteV1 {
|
|||
.remove_account_webauthn(&rwe)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
*/
|
||||
SetCredentialRequest::BackupCodeGenerate => {
|
||||
let gbe = GenerateBackupCodeEvent::from_parts(
|
||||
// &idms_prox_write.qs_write,
|
||||
|
@ -657,8 +670,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdate",
|
||||
level = "info",
|
||||
name = "idm_credential_update",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -710,8 +723,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdateintent",
|
||||
level = "info",
|
||||
name = "idm_credential_update_intent",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -762,8 +775,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialexchangeintent",
|
||||
level = "info",
|
||||
name = "idm_credential_exchange_intent",
|
||||
skip(self, intent_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -802,8 +815,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdatecommit",
|
||||
level = "info",
|
||||
name = "idm_credential_update_commit",
|
||||
skip(self, session_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -834,8 +847,40 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountsetpassword",
|
||||
level = "info",
|
||||
name = "idm_credential_update_cancel",
|
||||
skip(self, session_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialupdatecancel(
|
||||
&self,
|
||||
session_token: CUSessionToken,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateCancel>", {
|
||||
let session_token = CredentialUpdateSessionToken {
|
||||
token_enc: session_token.token,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.cancel_credential_update(session_token, ct)
|
||||
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin commit_credential_cancel",
|
||||
);
|
||||
e
|
||||
})
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "idm_account_set_password",
|
||||
skip(self, uat, cleartext, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -875,8 +920,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "regenerateradius",
|
||||
level = "info",
|
||||
name = "regenerate_radius_secret",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -931,8 +976,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "purgeattribute",
|
||||
level = "info",
|
||||
name = "purge_attribute",
|
||||
skip(self, uat, uuid_or_name, attr, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -987,8 +1032,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "removeattributevalues",
|
||||
level = "info",
|
||||
name = "remove_attribute_values",
|
||||
skip(self, uat, uuid_or_name, attr, values, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1051,8 +1096,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "appendattribute",
|
||||
level = "info",
|
||||
name = "append_attribute",
|
||||
skip(self, uat, uuid_or_name, attr, values, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1080,8 +1125,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "setattribute",
|
||||
level = "info",
|
||||
name = "set_attribute",
|
||||
skip(self, uat, uuid_or_name, attr, values, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1112,8 +1157,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "sshkeycreate",
|
||||
level = "info",
|
||||
name = "ssh_key_create",
|
||||
skip(self, uat, uuid_or_name, tag, key, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1137,8 +1182,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountpersonextend",
|
||||
level = "info",
|
||||
name = "idm_account_person_extend",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1193,8 +1238,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountpersonset",
|
||||
level = "info",
|
||||
name = "idm_account_person_set",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1248,8 +1293,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountunixextend",
|
||||
level = "info",
|
||||
name = "idm_account_unix_extend",
|
||||
skip(self, uat, uuid_or_name, ux, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1295,8 +1340,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmgroupunixextend",
|
||||
level = "info",
|
||||
name = "idm_group_unix_extend",
|
||||
skip(self, uat, uuid_or_name, gx, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1330,8 +1375,8 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountunixsetcred",
|
||||
level = "info",
|
||||
name = "idm_account_unix_set_cred",
|
||||
skip(self, uat, uuid_or_name, cred, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
|
@ -1384,7 +1429,7 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_scopemap_create",
|
||||
skip(self, uat, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1453,7 +1498,7 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "oauth2_scopemap_delete",
|
||||
skip(self, uat, filter, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
|
@ -1514,7 +1559,7 @@ impl QueryServerWriteV1 {
|
|||
|
||||
// ===== These below are internal only event types. =====
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "purge_tombstone_event",
|
||||
skip(self, msg)
|
||||
fields(uuid = ?msg.eventid)
|
||||
|
@ -1535,7 +1580,7 @@ impl QueryServerWriteV1 {
|
|||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
level = "info",
|
||||
name = "purge_recycled_event",
|
||||
skip(self, msg)
|
||||
fields(uuid = ?msg.eventid)
|
||||
|
@ -1556,7 +1601,7 @@ impl QueryServerWriteV1 {
|
|||
|
||||
pub(crate) async fn handle_delayedaction(&self, da: DelayedAction) {
|
||||
let eventid = Uuid::new_v4();
|
||||
let nspan = span!(Level::TRACE, "delayedaction", uuid = ?eventid);
|
||||
let nspan = span!(Level::INFO, "process_delayed_action", uuid = ?eventid);
|
||||
let _span = nspan.enter();
|
||||
|
||||
trace!("Begin delayed action ...");
|
||||
|
|
|
@ -3,7 +3,11 @@ use serde::{Deserialize, Serialize};
|
|||
use std::time::Duration;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::{COSEKey, UserVerificationPolicy};
|
||||
use webauthn_rs_core::proto::{COSEKey, UserVerificationPolicy};
|
||||
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
use webauthn_rs::prelude::SecurityKey as SecurityKeyV4;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DbCidV1 {
|
||||
|
@ -100,41 +104,82 @@ impl std::fmt::Debug for DbBackupCodeV1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbCredTypeV1 {
|
||||
Pw,
|
||||
GPw,
|
||||
PwMfa,
|
||||
// PwWn,
|
||||
Wn,
|
||||
// WnVer,
|
||||
// PwWnVer,
|
||||
}
|
||||
|
||||
// We have to allow this as serde expects &T for the fn sig.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
fn dbcred_type_default_pw() -> DbCredTypeV1 {
|
||||
DbCredTypeV1::Pw
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DbCredV1 {
|
||||
#[serde(default = "dbcred_type_default_pw")]
|
||||
pub type_: DbCredTypeV1,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub backup_code: Option<DbBackupCodeV1>,
|
||||
pub claims: Vec<String>,
|
||||
pub uuid: Uuid,
|
||||
#[serde(tag = "type_")]
|
||||
pub enum DbCred {
|
||||
// These are the old v1 versions.
|
||||
Pw {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
claims: Vec<String>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
GPw {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
claims: Vec<String>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
PwMfa {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
claims: Vec<String>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
Wn {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
claims: Vec<String>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
|
||||
TmpWn {
|
||||
webauthn: Vec<(String, PasskeyV4)>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
|
||||
#[serde(rename = "V2Pw")]
|
||||
V2Password { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V2GPw")]
|
||||
V2GenPassword { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V2PwMfa")]
|
||||
V2PasswordMfa {
|
||||
password: DbPasswordV1,
|
||||
totp: Option<DbTotpV1>,
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
webauthn: Vec<(String, SecurityKeyV4)>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -142,7 +187,17 @@ pub struct DbValueCredV1 {
|
|||
#[serde(rename = "t")]
|
||||
pub tag: String,
|
||||
#[serde(rename = "d")]
|
||||
pub data: DbCredV1,
|
||||
pub data: DbCred,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValuePasskeyV1 {
|
||||
V4 { u: Uuid, t: String, k: PasskeyV4 },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueDeviceKeyV1 {
|
||||
V4 { u: Uuid, t: String, k: DeviceKeyV4 },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -312,6 +367,10 @@ pub enum DbValueSetV2 {
|
|||
RestrictedString(Vec<String>),
|
||||
#[serde(rename = "IT")]
|
||||
IntentToken(Vec<(String, DbValueIntentTokenStateV1)>),
|
||||
#[serde(rename = "PK")]
|
||||
Passkey(Vec<DbValuePasskeyV1>),
|
||||
#[serde(rename = "DK")]
|
||||
DeviceKey(Vec<DbValueDeviceKeyV1>),
|
||||
#[serde(rename = "TE")]
|
||||
TrustedDeviceEnrollment(Vec<Uuid>),
|
||||
#[serde(rename = "AS")]
|
||||
|
@ -348,6 +407,8 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::PublicBinary(set) => set.len(),
|
||||
DbValueSetV2::RestrictedString(set) => set.len(),
|
||||
DbValueSetV2::IntentToken(set) => set.len(),
|
||||
DbValueSetV2::Passkey(set) => set.len(),
|
||||
DbValueSetV2::DeviceKey(set) => set.len(),
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) => set.len(),
|
||||
DbValueSetV2::AuthSession(set) => set.len(),
|
||||
}
|
||||
|
@ -360,7 +421,41 @@ impl DbValueSetV2 {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::be::dbvalue::DbCredV1;
|
||||
use super::DbCred;
|
||||
use super::{DbBackupCodeV1, DbPasswordV1, DbTotpV1, DbWebauthnV1};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn dbcred_type_default_pw() -> DbCredTypeV1 {
|
||||
DbCredTypeV1::Pw
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbCredTypeV1 {
|
||||
Pw,
|
||||
GPw,
|
||||
PwMfa,
|
||||
// PwWn,
|
||||
Wn,
|
||||
// WnVer,
|
||||
// PwWnVer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DbCredV1 {
|
||||
#[serde(default = "dbcred_type_default_pw")]
|
||||
pub type_: DbCredTypeV1,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub password: Option<DbPasswordV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub webauthn: Option<Vec<DbWebauthnV1>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totp: Option<DbTotpV1>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub backup_code: Option<DbBackupCodeV1>,
|
||||
pub claims: Vec<String>,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dbcred_pre_totp_decode() {
|
||||
|
@ -377,6 +472,16 @@ mod tests {
|
|||
*/
|
||||
let s = "o2hwYXNzd29yZKFmUEJLREYygwCBAIEAZmNsYWltc4BkdXVpZFAjkHFm4q5M86UcNRi4hBjN";
|
||||
let data = base64::decode(s).unwrap();
|
||||
let _dbcred: DbCredV1 = serde_cbor::from_slice(data.as_slice()).unwrap();
|
||||
let dbcred: DbCredV1 = serde_cbor::from_slice(data.as_slice()).unwrap();
|
||||
|
||||
// Test converting to the new enum format
|
||||
let x = vec![dbcred];
|
||||
|
||||
let json = serde_json::to_string(&x).unwrap();
|
||||
eprintln!("{}", json);
|
||||
|
||||
let _e_dbcred: Vec<DbCred> = serde_json::from_str(&json).unwrap();
|
||||
|
||||
// assert!(dbcred == e_dbcred);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ impl Configuration {
|
|||
maximum_request: 256 * 1024, // 256k
|
||||
// log type
|
||||
// log path
|
||||
// TODO #63: default true in prd
|
||||
// default true in prd
|
||||
secure_cookies: !cfg!(test),
|
||||
tls_config: None,
|
||||
cookie_key: [0; 32],
|
||||
|
|
|
@ -100,7 +100,9 @@ pub const JSON_IDM_SELF_ACP_READ_V1: &str = r#"{
|
|||
"uuid",
|
||||
"account_expire",
|
||||
"account_valid_from",
|
||||
"primary_credential"
|
||||
"primary_credential",
|
||||
"passkeys",
|
||||
"devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -118,10 +120,10 @@ pub const JSON_IDM_SELF_ACP_WRITE_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"eq\": [\"class\",\"account\"]}, \"self\"]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password"
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password", "passkeys", "devicekeys"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password"
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password", "passkeys", "devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -432,7 +434,7 @@ pub const JSON_IDM_ACP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -454,10 +456,10 @@ pub const JSON_IDM_ACP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -488,7 +490,9 @@ pub const JSON_IDM_ACP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
|
|||
"ssh_publickey",
|
||||
"mail",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
"account_valid_from",
|
||||
"passkeys",
|
||||
"devicekeys"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "account"
|
||||
|
@ -587,7 +591,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -609,10 +613,10 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -913,7 +917,9 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
|
|||
"primary_credential",
|
||||
"ssh_publickey",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
"account_valid_from",
|
||||
"passkeys",
|
||||
"devicekeys"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "account"
|
||||
|
|
|
@ -928,6 +928,68 @@ pub const JSON_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_PASSKEYS: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A set of registered passkeys"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"passkeys"
|
||||
],
|
||||
"syntax": [
|
||||
"PASSKEY"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000099"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_DEVICEKEYS: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A set of registered device keys"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"devicekeys"
|
||||
],
|
||||
"syntax": [
|
||||
"DEVICEKEY"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000100"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
|
@ -1031,6 +1093,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
|
|||
],
|
||||
"systemmay": [
|
||||
"primary_credential",
|
||||
"passkeys",
|
||||
"devicekeys",
|
||||
"credential_update_intent_token",
|
||||
"ssh_publickey",
|
||||
"radius_secret",
|
||||
|
|
|
@ -171,6 +171,8 @@ pub const _UUID_SCHEMA_CLASS_OAUTH2_CONSENT_SCOPE_MAP: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffff00000097");
|
||||
pub const _UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000098");
|
||||
pub const _UUID_SCHEMA_ATTR_PASSKEYS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000099");
|
||||
pub const _UUID_SCHEMA_ATTR_DEVICEKEYS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000100");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::be::dbvalue::DbBackupCodeV1;
|
||||
use crate::be::dbvalue::{DbCredTypeV1, DbCredV1, DbPasswordV1, DbWebauthnV1};
|
||||
use crate::be::dbvalue::{DbCred, DbPasswordV1};
|
||||
use hashbrown::HashMap as Map;
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_proto::v1::{BackupCodesView, CredentialDetail, CredentialDetailType, OperationError};
|
||||
|
@ -10,13 +10,15 @@ use rand::prelude::*;
|
|||
use std::convert::TryFrom;
|
||||
use std::time::{Duration, Instant};
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::Credential as WebauthnCredential;
|
||||
use webauthn_rs::proto::{Counter, CredentialID};
|
||||
|
||||
use webauthn_rs_core::proto::Credential as WebauthnCredential;
|
||||
use webauthn_rs_core::proto::CredentialV3;
|
||||
|
||||
use webauthn_rs::prelude::{AuthenticationResult, Passkey, SecurityKey};
|
||||
|
||||
pub mod policy;
|
||||
pub mod softlock;
|
||||
pub mod totp;
|
||||
pub mod webauthn;
|
||||
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::softlock::CredSoftLockPolicy;
|
||||
|
@ -260,7 +262,6 @@ impl BackupCodes {
|
|||
pub struct Credential {
|
||||
// policy: Policy,
|
||||
pub(crate) type_: CredentialType,
|
||||
pub(crate) claims: Vec<String>,
|
||||
// Uuid of Credential, used by auth session to lock this specific credential
|
||||
// if required.
|
||||
pub(crate) uuid: Uuid,
|
||||
|
@ -277,30 +278,26 @@ pub enum CredentialType {
|
|||
// Anonymous,
|
||||
Password(Password),
|
||||
GeneratedPassword(Password),
|
||||
Webauthn(Map<String, WebauthnCredential>),
|
||||
PasswordMfa(
|
||||
Password,
|
||||
Option<Totp>,
|
||||
Map<String, WebauthnCredential>,
|
||||
Map<String, SecurityKey>,
|
||||
Option<BackupCodes>,
|
||||
),
|
||||
// PasswordWebauthn(Password, Map<String, WebauthnCredential>),
|
||||
// WebauthnVerified(Map<String, WebauthnCredential>),
|
||||
// PasswordWebauthnVerified(Password, Map<String, WebauthnCredential>),
|
||||
Webauthn(Map<String, Passkey>),
|
||||
}
|
||||
|
||||
impl From<&Credential> for CredentialDetail {
|
||||
fn from(value: &Credential) -> Self {
|
||||
CredentialDetail {
|
||||
uuid: value.uuid,
|
||||
claims: value.claims.clone(),
|
||||
type_: match &value.type_ {
|
||||
CredentialType::Password(_) => CredentialDetailType::Password,
|
||||
CredentialType::GeneratedPassword(_) => CredentialDetailType::GeneratedPassword,
|
||||
CredentialType::Webauthn(wan) => {
|
||||
let mut labels: Vec<_> = wan.keys().cloned().collect();
|
||||
labels.sort_unstable();
|
||||
CredentialDetailType::Webauthn(labels)
|
||||
CredentialDetailType::Passkey(labels)
|
||||
}
|
||||
CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
|
||||
let mut labels: Vec<_> = wan.keys().cloned().collect();
|
||||
|
@ -316,73 +313,164 @@ impl From<&Credential> for CredentialDetail {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DbCredV1> for Credential {
|
||||
impl TryFrom<DbCred> for Credential {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: DbCredV1) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: DbCred) -> Result<Self, Self::Error> {
|
||||
// Work out what the policy is?
|
||||
let DbCredV1 {
|
||||
type_,
|
||||
password,
|
||||
webauthn,
|
||||
totp,
|
||||
backup_code,
|
||||
claims,
|
||||
uuid,
|
||||
} = value;
|
||||
|
||||
let v_password = match password {
|
||||
Some(dbp) => Some(Password::try_from(dbp)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let v_totp = match totp {
|
||||
Some(dbt) => Some(Totp::try_from(dbt)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let v_webauthn = webauthn.map(|dbw| {
|
||||
dbw.into_iter()
|
||||
.map(|wc| {
|
||||
(
|
||||
wc.label,
|
||||
WebauthnCredential {
|
||||
cred_id: wc.id,
|
||||
cred: wc.cred,
|
||||
counter: wc.counter,
|
||||
verified: wc.verified,
|
||||
registration_policy: wc.registration_policy,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
let v_backup_code = match backup_code {
|
||||
Some(dbb) => Some(BackupCodes::try_from(dbb)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let type_ = match type_ {
|
||||
DbCredTypeV1::Pw => v_password.map(CredentialType::Password),
|
||||
DbCredTypeV1::GPw => v_password.map(CredentialType::GeneratedPassword),
|
||||
// In the future this could use .zip
|
||||
DbCredTypeV1::PwMfa => match (v_password, v_webauthn) {
|
||||
(Some(pw), Some(wn)) => {
|
||||
Some(CredentialType::PasswordMfa(pw, v_totp, wn, v_backup_code))
|
||||
match value {
|
||||
DbCred::V2Password {
|
||||
password: db_password,
|
||||
uuid,
|
||||
}
|
||||
| DbCred::Pw {
|
||||
password: Some(db_password),
|
||||
webauthn: None,
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims: _,
|
||||
uuid,
|
||||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
let type_ = CredentialType::Password(v_password);
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
DbCredTypeV1::Wn => v_webauthn.map(CredentialType::Webauthn),
|
||||
}
|
||||
.filter(|v| v.is_valid())
|
||||
.ok_or(())?;
|
||||
}
|
||||
DbCred::V2GenPassword {
|
||||
password: db_password,
|
||||
uuid,
|
||||
}
|
||||
| DbCred::GPw {
|
||||
password: Some(db_password),
|
||||
webauthn: None,
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims: _,
|
||||
uuid,
|
||||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
let type_ = CredentialType::GeneratedPassword(v_password);
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
DbCred::PwMfa {
|
||||
password: Some(db_password),
|
||||
webauthn: Some(db_webauthn),
|
||||
totp,
|
||||
backup_code,
|
||||
claims: _,
|
||||
uuid,
|
||||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
|
||||
Ok(Credential {
|
||||
type_,
|
||||
claims,
|
||||
uuid,
|
||||
})
|
||||
let v_totp = match totp {
|
||||
Some(dbt) => Some(Totp::try_from(dbt)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let v_webauthn = db_webauthn
|
||||
.into_iter()
|
||||
.map(|wc| {
|
||||
(
|
||||
wc.label,
|
||||
SecurityKey::from(WebauthnCredential::from(CredentialV3 {
|
||||
cred_id: wc.id,
|
||||
cred: wc.cred,
|
||||
counter: wc.counter,
|
||||
verified: wc.verified,
|
||||
registration_policy: wc.registration_policy,
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let v_backup_code = match backup_code {
|
||||
Some(dbb) => Some(BackupCodes::try_from(dbb)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let type_ =
|
||||
CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
|
||||
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
DbCred::Wn {
|
||||
password: None,
|
||||
webauthn: Some(db_webauthn),
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims: _,
|
||||
uuid,
|
||||
} => {
|
||||
let v_webauthn = db_webauthn
|
||||
.into_iter()
|
||||
.map(|wc| {
|
||||
(
|
||||
wc.label,
|
||||
Passkey::from(WebauthnCredential::from(CredentialV3 {
|
||||
cred_id: wc.id,
|
||||
cred: wc.cred,
|
||||
counter: wc.counter,
|
||||
verified: wc.verified,
|
||||
registration_policy: wc.registration_policy,
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let type_ = CredentialType::Webauthn(v_webauthn);
|
||||
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
DbCred::TmpWn { .. } => {
|
||||
todo!()
|
||||
}
|
||||
DbCred::V2PasswordMfa {
|
||||
password: db_password,
|
||||
totp: Some(db_totp),
|
||||
backup_code,
|
||||
webauthn: db_webauthn,
|
||||
uuid,
|
||||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
|
||||
let v_totp = Some(Totp::try_from(db_totp)?);
|
||||
|
||||
let v_backup_code = match backup_code {
|
||||
Some(dbb) => Some(BackupCodes::try_from(dbb)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let v_webauthn = db_webauthn.into_iter().collect();
|
||||
|
||||
let type_ =
|
||||
CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
|
||||
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Database content may be corrupt - invalid credential");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,12 +492,11 @@ impl Credential {
|
|||
}
|
||||
|
||||
/// Create a new credential that contains a CredentialType::Webauthn
|
||||
pub fn new_webauthn_only(label: String, cred: WebauthnCredential) -> Self {
|
||||
pub fn new_passkey_only(label: String, cred: Passkey) -> Self {
|
||||
let mut webauthn_map = Map::new();
|
||||
webauthn_map.insert(label, cred);
|
||||
Credential {
|
||||
type_: CredentialType::Webauthn(webauthn_map),
|
||||
claims: Vec::new(),
|
||||
uuid: Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
|
@ -427,10 +514,10 @@ impl Credential {
|
|||
/// Extend this credential with another alternate webauthn credential. This is especially
|
||||
/// useful for `PasswordMfa` where you can have many webauthn credentials and a password
|
||||
/// generally so that one is a backup.
|
||||
pub fn append_webauthn(
|
||||
pub fn append_securitykey(
|
||||
&self,
|
||||
label: String,
|
||||
cred: WebauthnCredential,
|
||||
cred: SecurityKey,
|
||||
) -> Result<Self, OperationError> {
|
||||
let type_ = match &self.type_ {
|
||||
CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
|
||||
|
@ -448,32 +535,25 @@ impl Credential {
|
|||
}
|
||||
CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
|
||||
}
|
||||
CredentialType::Webauthn(map) => {
|
||||
let mut nmap = map.clone();
|
||||
if nmap.insert(label.clone(), cred).is_some() {
|
||||
return Err(OperationError::InvalidAttribute(format!(
|
||||
"Webauthn label '{:?}' already exists",
|
||||
label
|
||||
)));
|
||||
}
|
||||
CredentialType::Webauthn(nmap)
|
||||
}
|
||||
// Ignore
|
||||
CredentialType::Webauthn(map) => CredentialType::Webauthn(map.clone()),
|
||||
};
|
||||
|
||||
// Check stuff
|
||||
Ok(Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove a webauthn token identified by `label` from this Credential.
|
||||
pub fn remove_webauthn(&self, label: &str) -> Result<Self, OperationError> {
|
||||
pub fn remove_securitykey(&self, label: &str) -> Result<Self, OperationError> {
|
||||
let type_ = match &self.type_ {
|
||||
CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => {
|
||||
CredentialType::Password(_)
|
||||
| CredentialType::GeneratedPassword(_)
|
||||
| CredentialType::Webauthn(_) => {
|
||||
return Err(OperationError::InvalidAttribute(
|
||||
"Webauthn is not present on this credential".to_string(),
|
||||
"SecurityKey is not present on this credential".to_string(),
|
||||
));
|
||||
}
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
|
||||
|
@ -500,28 +580,11 @@ impl Credential {
|
|||
CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
|
||||
}
|
||||
}
|
||||
CredentialType::Webauthn(map) => {
|
||||
let mut nmap = map.clone();
|
||||
if nmap.remove(label).is_none() {
|
||||
return Err(OperationError::InvalidAttribute(format!(
|
||||
"Removing Webauthn token with label '{:?}': does not exist",
|
||||
label
|
||||
)));
|
||||
}
|
||||
if nmap.is_empty() {
|
||||
return Err(OperationError::InvalidAttribute(format!(
|
||||
"Removing Webauthn token with label '{:?}': unable to remove, this is the last webauthn token",
|
||||
label
|
||||
)));
|
||||
}
|
||||
CredentialType::Webauthn(nmap)
|
||||
}
|
||||
};
|
||||
|
||||
// Check stuff
|
||||
Ok(Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
})
|
||||
}
|
||||
|
@ -529,68 +592,60 @@ impl Credential {
|
|||
#[allow(clippy::ptr_arg)]
|
||||
/// After a successful authentication with Webauthn, we need to advance the credentials
|
||||
/// counter value to prevent certain classes of replay attacks.
|
||||
pub fn update_webauthn_counter(
|
||||
pub fn update_webauthn_properties(
|
||||
&self,
|
||||
cid: &CredentialID,
|
||||
counter: Counter,
|
||||
auth_result: &AuthenticationResult,
|
||||
) -> Result<Option<Self>, OperationError> {
|
||||
let nmap = match &self.type_ {
|
||||
CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
|
||||
// No action required
|
||||
return Ok(None);
|
||||
}
|
||||
CredentialType::PasswordMfa(_, _, map, _) | CredentialType::Webauthn(map) => map
|
||||
.iter()
|
||||
.find_map(|(k, v)| {
|
||||
if &v.cred_id == cid && v.counter < counter {
|
||||
Some(k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|label| {
|
||||
let mut webauthn_map = map.clone();
|
||||
|
||||
if let Some(cred) = webauthn_map.get_mut(label) {
|
||||
cred.counter = counter
|
||||
};
|
||||
webauthn_map
|
||||
}),
|
||||
};
|
||||
|
||||
let map = match nmap {
|
||||
Some(map) => map,
|
||||
None => {
|
||||
// No action needed.
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let type_ = match &self.type_ {
|
||||
CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
|
||||
// Should not be possible!
|
||||
return Err(OperationError::InvalidState);
|
||||
// -- this does occur when we have mixed pw/passkey
|
||||
// and we need to do an update, so we just mask this no Ok(None).
|
||||
// return Err(OperationError::InvalidState);
|
||||
return Ok(None);
|
||||
}
|
||||
CredentialType::Webauthn(_) => CredentialType::Webauthn(map),
|
||||
CredentialType::PasswordMfa(pw, totp, _, backup_code) => {
|
||||
CredentialType::PasswordMfa(pw.clone(), totp.clone(), map, backup_code.clone())
|
||||
CredentialType::Webauthn(map) => {
|
||||
let mut nmap = map.clone();
|
||||
nmap.values_mut().for_each(|pk| {
|
||||
pk.update_credential(auth_result);
|
||||
});
|
||||
CredentialType::Webauthn(nmap)
|
||||
}
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
|
||||
let mut nmap = map.clone();
|
||||
nmap.values_mut().for_each(|sk| {
|
||||
sk.update_credential(auth_result);
|
||||
});
|
||||
CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get a reference to the contained webuthn credentials, if any.
|
||||
pub fn webauthn_ref(&self) -> Result<&Map<String, WebauthnCredential>, OperationError> {
|
||||
pub fn securitykey_ref(&self) -> Result<&Map<String, SecurityKey>, OperationError> {
|
||||
match &self.type_ {
|
||||
CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => Err(
|
||||
OperationError::InvalidAccountState("non-webauthn cred type?".to_string()),
|
||||
),
|
||||
CredentialType::PasswordMfa(_, _, map, _) | CredentialType::Webauthn(map) => Ok(map),
|
||||
CredentialType::Webauthn(_)
|
||||
| CredentialType::Password(_)
|
||||
| CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
|
||||
"non-webauthn cred type?".to_string(),
|
||||
)),
|
||||
CredentialType::PasswordMfa(_, _, map, _) => Ok(map),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn passkey_ref(&self) -> Result<&Map<String, Passkey>, OperationError> {
|
||||
match &self.type_ {
|
||||
CredentialType::PasswordMfa(_, _, _, _)
|
||||
| CredentialType::Password(_)
|
||||
| CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
|
||||
"non-webauthn cred type?".to_string(),
|
||||
)),
|
||||
CredentialType::Webauthn(map) => Ok(map),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,66 +667,26 @@ impl Credential {
|
|||
}
|
||||
|
||||
/// Extract this credential into it's Serialisable Database form, ready for persistence.
|
||||
pub fn to_db_valuev1(&self) -> DbCredV1 {
|
||||
let claims = self.claims.clone();
|
||||
pub fn to_db_valuev1(&self) -> DbCred {
|
||||
let uuid = self.uuid;
|
||||
match &self.type_ {
|
||||
CredentialType::Password(pw) => DbCredV1 {
|
||||
type_: DbCredTypeV1::Pw,
|
||||
password: Some(pw.to_dbpasswordv1()),
|
||||
webauthn: None,
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims,
|
||||
CredentialType::Password(pw) => DbCred::V2Password {
|
||||
password: pw.to_dbpasswordv1(),
|
||||
uuid,
|
||||
},
|
||||
CredentialType::GeneratedPassword(pw) => DbCredV1 {
|
||||
type_: DbCredTypeV1::GPw,
|
||||
password: Some(pw.to_dbpasswordv1()),
|
||||
webauthn: None,
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims,
|
||||
CredentialType::GeneratedPassword(pw) => DbCred::V2GenPassword {
|
||||
password: pw.to_dbpasswordv1(),
|
||||
uuid,
|
||||
},
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCredV1 {
|
||||
type_: DbCredTypeV1::PwMfa,
|
||||
password: Some(pw.to_dbpasswordv1()),
|
||||
webauthn: Some(
|
||||
map.iter()
|
||||
.map(|(k, v)| DbWebauthnV1 {
|
||||
label: k.clone(),
|
||||
id: v.cred_id.clone(),
|
||||
cred: v.cred.clone(),
|
||||
counter: v.counter,
|
||||
verified: v.verified,
|
||||
registration_policy: v.registration_policy,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCred::V2PasswordMfa {
|
||||
password: pw.to_dbpasswordv1(),
|
||||
totp: totp.as_ref().map(|t| t.to_dbtotpv1()),
|
||||
backup_code: backup_code.as_ref().map(|b| b.to_dbbackupcodev1()),
|
||||
claims,
|
||||
webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
||||
uuid,
|
||||
},
|
||||
CredentialType::Webauthn(map) => DbCredV1 {
|
||||
type_: DbCredTypeV1::Wn,
|
||||
password: None,
|
||||
webauthn: Some(
|
||||
map.iter()
|
||||
.map(|(k, v)| DbWebauthnV1 {
|
||||
label: k.clone(),
|
||||
id: v.cred_id.clone(),
|
||||
cred: v.cred.clone(),
|
||||
counter: v.counter,
|
||||
verified: v.verified,
|
||||
registration_policy: v.registration_policy,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
totp: None,
|
||||
backup_code: None,
|
||||
claims,
|
||||
CredentialType::Webauthn(map) => DbCred::TmpWn {
|
||||
webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
||||
uuid,
|
||||
},
|
||||
}
|
||||
|
@ -685,13 +700,11 @@ impl Credential {
|
|||
CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
|
||||
CredentialType::PasswordMfa(pw, totp.clone(), wan.clone(), backup_code.clone())
|
||||
}
|
||||
CredentialType::Webauthn(wan) => {
|
||||
CredentialType::PasswordMfa(pw, None, wan.clone(), None)
|
||||
}
|
||||
// Ignore
|
||||
CredentialType::Webauthn(wan) => CredentialType::Webauthn(wan.clone()),
|
||||
};
|
||||
Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}
|
||||
}
|
||||
|
@ -715,7 +728,6 @@ impl Credential {
|
|||
};
|
||||
Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}
|
||||
}
|
||||
|
@ -734,7 +746,6 @@ impl Credential {
|
|||
};
|
||||
Credential {
|
||||
type_,
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}
|
||||
}
|
||||
|
@ -742,7 +753,6 @@ impl Credential {
|
|||
pub(crate) fn new_from_generatedpassword(pw: Password) -> Self {
|
||||
Credential {
|
||||
type_: CredentialType::GeneratedPassword(pw),
|
||||
claims: Vec::new(),
|
||||
uuid: Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
|
@ -750,7 +760,6 @@ impl Credential {
|
|||
pub(crate) fn new_from_password(pw: Password) -> Self {
|
||||
Credential {
|
||||
type_: CredentialType::Password(pw),
|
||||
claims: Vec::new(),
|
||||
uuid: Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
|
@ -786,7 +795,6 @@ impl Credential {
|
|||
wan.clone(),
|
||||
Some(backup_codes),
|
||||
),
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}),
|
||||
_ => Err(OperationError::InvalidAccountState(
|
||||
|
@ -806,7 +814,6 @@ impl Credential {
|
|||
backup_codes.remove(code_to_remove);
|
||||
Ok(Credential {
|
||||
type_: CredentialType::PasswordMfa(pw, totp, wan, Some(backup_codes)),
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
})
|
||||
}
|
||||
|
@ -825,7 +832,6 @@ impl Credential {
|
|||
match &self.type_ {
|
||||
CredentialType::PasswordMfa(pw, totp, wan, _) => Ok(Credential {
|
||||
type_: CredentialType::PasswordMfa(pw.clone(), totp.clone(), wan.clone(), None),
|
||||
claims: self.claims.clone(),
|
||||
uuid: self.uuid,
|
||||
}),
|
||||
_ => Err(OperationError::InvalidAccountState(
|
||||
|
@ -851,26 +857,6 @@ impl Credential {
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn add_claim(&mut self) {
|
||||
}
|
||||
|
||||
pub fn remove_claim(&mut self) {
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
pub fn modify_password(&mut self) {
|
||||
// Change the password
|
||||
}
|
||||
|
||||
pub fn add_webauthn_token() {
|
||||
}
|
||||
|
||||
pub fn remove_webauthn_token() {
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
impl CredentialType {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
use url::Url;
|
||||
use webauthn_rs::WebauthnConfig;
|
||||
|
||||
pub struct WebauthnDomainConfig {
|
||||
pub rp_name: String,
|
||||
pub origin: Url,
|
||||
pub rp_id: String,
|
||||
}
|
||||
|
||||
impl WebauthnConfig for WebauthnDomainConfig {
|
||||
fn get_relying_party_name(&self) -> &str {
|
||||
self.rp_name.as_str()
|
||||
}
|
||||
|
||||
fn get_origin(&self) -> &Url {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
fn get_relying_party_id(&self) -> &str {
|
||||
self.rp_id.as_str()
|
||||
}
|
||||
|
||||
fn allow_subdomains_origin(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
|
@ -55,6 +55,10 @@ use std::sync::Arc;
|
|||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
// use std::convert::TryFrom;
|
||||
// use std::str::FromStr;
|
||||
|
||||
|
@ -1887,6 +1891,18 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
.and_then(|vs| vs.to_credential_single())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Get the set of passkeys on this account, if any are present.
|
||||
pub fn get_ava_passkeys(&self, attr: &str) -> Option<&BTreeMap<Uuid, (String, PasskeyV4)>> {
|
||||
self.attrs.get(attr).and_then(|vs| vs.as_passkey_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Get the set of devicekeys on this account, if any are present.
|
||||
pub fn get_ava_devicekeys(&self, attr: &str) -> Option<&BTreeMap<Uuid, (String, DeviceKeyV4)>> {
|
||||
self.attrs.get(attr).and_then(|vs| vs.as_devicekey_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single secret value, if valid to transform this value.
|
||||
pub fn get_ava_single_secret(&self, attr: &str) -> Option<&str> {
|
||||
|
|
|
@ -39,6 +39,9 @@ use uuid::Uuid;
|
|||
#[cfg(test)]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(test)]
|
||||
use webauthn_rs::prelude::PublicKeyCredential;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchResult {
|
||||
entries: Vec<ProtoEntry>,
|
||||
|
@ -764,6 +767,14 @@ impl AuthEventStep {
|
|||
cred: AuthCredential::BackupCode(code.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn cred_step_passkey(sid: Uuid, passkey_response: PublicKeyCredential) -> Self {
|
||||
AuthEventStep::Cred(AuthEventStepCred {
|
||||
sessionid: sid,
|
||||
cred: AuthCredential::Passkey(passkey_response),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -836,6 +847,14 @@ impl AuthEvent {
|
|||
step: AuthEventStep::cred_step_backup_code(sid, code),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn cred_step_passkey(sid: Uuid, passkey_response: PublicKeyCredential) -> Self {
|
||||
AuthEvent {
|
||||
ident: None,
|
||||
step: AuthEventStep::cred_step_passkey(sid, passkey_response),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probably should be a struct with the session id present.
|
||||
|
|
|
@ -6,6 +6,10 @@ use kanidm_proto::v1::OperationError;
|
|||
use kanidm_proto::v1::{AuthType, UserAuthToken};
|
||||
use kanidm_proto::v1::{BackupCodesView, CredentialStatus};
|
||||
|
||||
use webauthn_rs::prelude::CredentialID;
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::Totp;
|
||||
|
@ -18,8 +22,7 @@ use std::collections::BTreeMap;
|
|||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::Credential as WebauthnCredential;
|
||||
use webauthn_rs::proto::{Counter, CredentialID};
|
||||
use webauthn_rs::prelude::AuthenticationResult;
|
||||
|
||||
lazy_static! {
|
||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
||||
|
@ -53,6 +56,16 @@ macro_rules! try_from_entry {
|
|||
.get_ava_single_credential("primary_credential")
|
||||
.map(|v| v.clone());
|
||||
|
||||
let passkeys = $value
|
||||
.get_ava_passkeys("passkeys")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let devicekeys = $value
|
||||
.get_ava_devicekeys("devicekeys")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let spn = $value.get_ava_single_proto_string("spn").ok_or(
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string()),
|
||||
)?;
|
||||
|
@ -88,6 +101,8 @@ macro_rules! try_from_entry {
|
|||
displayname,
|
||||
groups,
|
||||
primary,
|
||||
passkeys,
|
||||
devicekeys,
|
||||
valid_from,
|
||||
expire,
|
||||
radius_secret,
|
||||
|
@ -114,6 +129,8 @@ pub(crate) struct Account {
|
|||
#[allow(dead_code)]
|
||||
pub groups: Vec<Group>,
|
||||
pub primary: Option<Credential>,
|
||||
pub passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
|
||||
pub devicekeys: BTreeMap<Uuid, (String, DeviceKeyV4)>,
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
pub radius_secret: Option<String>,
|
||||
|
@ -334,58 +351,42 @@ impl Account {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gen_webauthn_mod(
|
||||
&self,
|
||||
label: String,
|
||||
cred: WebauthnCredential,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let ncred = match &self.primary {
|
||||
Some(primary) => primary.append_webauthn(label, cred)?,
|
||||
None => Credential::new_webauthn_only(label, cred),
|
||||
};
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
|
||||
}
|
||||
|
||||
pub(crate) fn gen_webauthn_remove_mod(
|
||||
&self,
|
||||
label: &str,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
match &self.primary {
|
||||
// Change the cred
|
||||
Some(primary) => {
|
||||
let ncred = primary.remove_webauthn(label)?;
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
|
||||
}
|
||||
None => {
|
||||
// No credential exists, we can't remove what is not real.
|
||||
Err(OperationError::InvalidState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
pub(crate) fn gen_webauthn_counter_mod(
|
||||
&self,
|
||||
cid: &CredentialID,
|
||||
counter: Counter,
|
||||
&mut self,
|
||||
auth_result: &AuthenticationResult,
|
||||
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
|
||||
//
|
||||
let mut ml = Vec::with_capacity(2);
|
||||
// Where is the credential we need to update?
|
||||
let opt_ncred = match self.primary.as_ref() {
|
||||
Some(primary) => primary.update_webauthn_counter(cid, counter)?,
|
||||
Some(primary) => primary.update_webauthn_properties(auth_result)?,
|
||||
None => None,
|
||||
};
|
||||
|
||||
match opt_ncred {
|
||||
Some(ncred) => {
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(Some(ModifyList::new_purge_and_set(
|
||||
"primary_credential",
|
||||
vcred,
|
||||
)))
|
||||
if let Some(ncred) = opt_ncred {
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
ml.push(Modify::Purged("primary_credential".into()));
|
||||
ml.push(Modify::Present("primary_credential".into(), vcred));
|
||||
}
|
||||
|
||||
// Is it a passkey?
|
||||
self.passkeys.iter_mut().for_each(|(u, (t, k))| {
|
||||
if let Some(true) = k.update_credential(auth_result) {
|
||||
ml.push(Modify::Removed(
|
||||
"passkeys".into(),
|
||||
PartialValue::Passkey(*u),
|
||||
));
|
||||
|
||||
ml.push(Modify::Present(
|
||||
"passkeys".into(),
|
||||
Value::Passkey(*u, t.clone(), k.clone()),
|
||||
));
|
||||
}
|
||||
None => Ok(None),
|
||||
});
|
||||
|
||||
if ml.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(ModifyList::new_list(ml)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -489,6 +490,11 @@ impl Account {
|
|||
.ok_or(OperationError::InvalidState)
|
||||
.and_then(|cred| cred.get_backup_code_view())
|
||||
}
|
||||
|
||||
pub(crate) fn existing_credential_id_list(&self) -> Option<Vec<CredentialID>> {
|
||||
// TODO!!!
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Need to also add a "to UserAuthToken" ...
|
||||
|
|
|
@ -18,14 +18,19 @@ use crate::idm::delayed::{DelayedAction, PasswordUpgrade, WebauthnCounterIncreme
|
|||
// use crossbeam::channel::Sender;
|
||||
use tokio::sync::mpsc::UnboundedSender as Sender;
|
||||
|
||||
use crate::credential::webauthn::WebauthnDomainConfig;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
// use webauthn_rs::proto::Credential as WebauthnCredential;
|
||||
use compact_jwt::{Jws, JwsSigner};
|
||||
use std::collections::BTreeMap;
|
||||
pub use std::collections::BTreeSet as Set;
|
||||
use webauthn_rs::proto::RequestChallengeResponse;
|
||||
use webauthn_rs::{AuthenticationState, Webauthn};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use webauthn_rs::prelude::{
|
||||
PasskeyAuthentication, RequestChallengeResponse, SecurityKeyAuthentication, Webauthn,
|
||||
};
|
||||
// use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
// Each CredHandler takes one or more credentials and determines if the
|
||||
// handlers requirements can be 100% fufilled. This is where MFA or other
|
||||
|
@ -62,7 +67,7 @@ struct CredMfa {
|
|||
pw: Password,
|
||||
pw_state: CredVerifyState,
|
||||
totp: Option<Totp>,
|
||||
wan: Option<(RequestChallengeResponse, AuthenticationState)>,
|
||||
wan: Option<(RequestChallengeResponse, SecurityKeyAuthentication)>,
|
||||
backup_code: Option<BackupCodes>,
|
||||
mfa_state: CredVerifyState,
|
||||
}
|
||||
|
@ -71,7 +76,7 @@ struct CredMfa {
|
|||
/// The state of a webauthn credential during authentication
|
||||
struct CredWebauthn {
|
||||
chal: RequestChallengeResponse,
|
||||
wan_state: AuthenticationState,
|
||||
wan_state: PasskeyAuthentication,
|
||||
state: CredVerifyState,
|
||||
}
|
||||
|
||||
|
@ -81,26 +86,27 @@ struct CredWebauthn {
|
|||
#[derive(Clone, Debug)]
|
||||
enum CredHandler {
|
||||
Anonymous,
|
||||
// AppPassword (?)
|
||||
Password(Password, bool),
|
||||
PasswordMfa(Box<CredMfa>),
|
||||
Webauthn(CredWebauthn),
|
||||
// Webauthn + Password
|
||||
Passkey(CredWebauthn),
|
||||
}
|
||||
|
||||
impl CredHandler {
|
||||
impl TryFrom<(&Credential, &Webauthn)> for CredHandler {
|
||||
type Error = ();
|
||||
|
||||
/// Given a credential and some external configuration, Generate the credential handler
|
||||
/// that will be used for this session. This credential handler is a "self contained"
|
||||
/// unit that defines what is possible to use during this authentication session to prevent
|
||||
/// inconsistency.
|
||||
fn try_from(c: &Credential, webauthn: &Webauthn<WebauthnDomainConfig>) -> Result<Self, ()> {
|
||||
fn try_from((c, webauthn): (&Credential, &Webauthn)) -> Result<Self, Self::Error> {
|
||||
match &c.type_ {
|
||||
CredentialType::Password(pw) => Ok(CredHandler::Password(pw.clone(), false)),
|
||||
CredentialType::GeneratedPassword(pw) => Ok(CredHandler::Password(pw.clone(), true)),
|
||||
CredentialType::PasswordMfa(pw, maybe_totp, maybe_wan, maybe_backup_code) => {
|
||||
let wan = if !maybe_wan.is_empty() {
|
||||
let sks: Vec<_> = maybe_wan.values().cloned().collect();
|
||||
webauthn
|
||||
.generate_challenge_authenticate(maybe_wan.values().cloned().collect())
|
||||
.start_securitykey_authentication(&sks)
|
||||
.map(Some)
|
||||
.map_err(|e| {
|
||||
security_info!(
|
||||
|
@ -129,23 +135,61 @@ impl CredHandler {
|
|||
|
||||
Ok(CredHandler::PasswordMfa(cmfa))
|
||||
}
|
||||
CredentialType::Webauthn(wan) => webauthn
|
||||
.generate_challenge_authenticate(wan.values().cloned().collect())
|
||||
.map(|(chal, wan_state)| {
|
||||
CredHandler::Webauthn(CredWebauthn {
|
||||
chal,
|
||||
wan_state,
|
||||
state: CredVerifyState::Init,
|
||||
CredentialType::Webauthn(wan) => {
|
||||
let pks: Vec<_> = wan.values().cloned().collect();
|
||||
webauthn
|
||||
.start_passkey_authentication(&pks)
|
||||
.map(|(chal, wan_state)| {
|
||||
CredHandler::Passkey(CredWebauthn {
|
||||
chal,
|
||||
wan_state,
|
||||
state: CredVerifyState::Init,
|
||||
})
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
security_info!(?e, "Unable to create webauthn authentication challenge");
|
||||
// maps to unit.
|
||||
}),
|
||||
.map_err(|e| {
|
||||
security_info!(?e, "Unable to create webauthn authentication challenge");
|
||||
// maps to unit.
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&BTreeMap<Uuid, (String, PasskeyV4)>, &Webauthn)> for CredHandler {
|
||||
type Error = ();
|
||||
|
||||
/// Given a credential and some external configuration, Generate the credential handler
|
||||
/// that will be used for this session. This credential handler is a "self contained"
|
||||
/// unit that defines what is possible to use during this authentication session to prevent
|
||||
/// inconsistency.
|
||||
fn try_from(
|
||||
(wan, webauthn): (&BTreeMap<Uuid, (String, PasskeyV4)>, &Webauthn),
|
||||
) -> Result<Self, Self::Error> {
|
||||
if wan.is_empty() {
|
||||
security_info!("Account does not have any passkeys");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let pks: Vec<_> = wan.values().map(|(_, k)| k).cloned().collect();
|
||||
webauthn
|
||||
.start_passkey_authentication(&pks)
|
||||
.map(|(chal, wan_state)| {
|
||||
CredHandler::Passkey(CredWebauthn {
|
||||
chal,
|
||||
wan_state,
|
||||
state: CredVerifyState::Init,
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
security_info!(
|
||||
?e,
|
||||
"Unable to create passkey webauthn authentication challenge"
|
||||
);
|
||||
// maps to unit.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CredHandler {
|
||||
/// Determine if this password factor requires an upgrade of it's cryptographic type. If
|
||||
/// so, send an asynchronous event into the queue that will allow the password to have it's
|
||||
|
@ -232,7 +276,7 @@ impl CredHandler {
|
|||
cred: &AuthCredential,
|
||||
ts: &Duration,
|
||||
pw_mfa: &mut CredMfa,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &Webauthn,
|
||||
who: Uuid,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
pw_badlist_set: Option<&HashSet<String>>,
|
||||
|
@ -246,24 +290,23 @@ impl CredHandler {
|
|||
pw_mfa.wan.as_ref(),
|
||||
pw_mfa.backup_code.as_ref(),
|
||||
) {
|
||||
(AuthCredential::Webauthn(resp), _, Some((_, wan_state)), _) => {
|
||||
match webauthn.authenticate_credential(resp, wan_state) {
|
||||
Ok((cid, auth_data)) => {
|
||||
(AuthCredential::SecurityKey(resp), _, Some((_, wan_state)), _) => {
|
||||
match webauthn.finish_securitykey_authentication(resp, wan_state) {
|
||||
Ok(auth_result) => {
|
||||
pw_mfa.mfa_state = CredVerifyState::Success;
|
||||
// Success. Determine if we need to update the counter
|
||||
// async from r.
|
||||
if auth_data.counter != 0 {
|
||||
if auth_result.needs_update() {
|
||||
// Do async
|
||||
if let Err(_e) =
|
||||
async_tx.send(DelayedAction::WebauthnCounterIncrement(
|
||||
WebauthnCounterIncrement {
|
||||
target_uuid: who,
|
||||
cid: cid.clone(),
|
||||
counter: auth_data.counter,
|
||||
auth_result,
|
||||
},
|
||||
))
|
||||
{
|
||||
admin_warn!("unable to queue delayed webauthn counter increment, continuing ... ");
|
||||
admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
|
||||
};
|
||||
};
|
||||
CredState::Continue(vec![AuthAllowed::Password])
|
||||
|
@ -369,7 +412,7 @@ impl CredHandler {
|
|||
pub fn validate_webauthn(
|
||||
cred: &AuthCredential,
|
||||
wan_cred: &mut CredWebauthn,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &Webauthn,
|
||||
who: Uuid,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
) -> CredState {
|
||||
|
@ -379,26 +422,25 @@ impl CredHandler {
|
|||
}
|
||||
|
||||
match cred {
|
||||
AuthCredential::Webauthn(resp) => {
|
||||
AuthCredential::Passkey(resp) => {
|
||||
// lets see how we go.
|
||||
match webauthn.authenticate_credential(resp, &wan_cred.wan_state) {
|
||||
Ok((cid, auth_data)) => {
|
||||
match webauthn.finish_passkey_authentication(resp, &wan_cred.wan_state) {
|
||||
Ok(auth_result) => {
|
||||
wan_cred.state = CredVerifyState::Success;
|
||||
// Success. Determine if we need to update the counter
|
||||
// async from r.
|
||||
if auth_data.counter != 0 {
|
||||
if auth_result.needs_update() {
|
||||
// Do async
|
||||
if let Err(_e) = async_tx.send(DelayedAction::WebauthnCounterIncrement(
|
||||
WebauthnCounterIncrement {
|
||||
target_uuid: who,
|
||||
cid: cid.clone(),
|
||||
counter: auth_data.counter,
|
||||
auth_result,
|
||||
},
|
||||
)) {
|
||||
admin_warn!("unable to queue delayed webauthn counter increment, continuing ... ");
|
||||
admin_warn!("unable to queue delayed webauthn property update, continuing ... ");
|
||||
};
|
||||
};
|
||||
CredState::Success(AuthType::Webauthn)
|
||||
CredState::Success(AuthType::Passkey)
|
||||
}
|
||||
Err(e) => {
|
||||
wan_cred.state = CredVerifyState::Fail;
|
||||
|
@ -425,7 +467,7 @@ impl CredHandler {
|
|||
ts: &Duration,
|
||||
who: Uuid,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &Webauthn,
|
||||
pw_badlist_set: Option<&HashSet<String>>,
|
||||
) -> CredState {
|
||||
match self {
|
||||
|
@ -442,7 +484,7 @@ impl CredHandler {
|
|||
async_tx,
|
||||
pw_badlist_set,
|
||||
),
|
||||
CredHandler::Webauthn(ref mut wan_cred) => {
|
||||
CredHandler::Passkey(ref mut wan_cred) => {
|
||||
Self::validate_webauthn(cred, wan_cred, webauthn, who, async_tx)
|
||||
}
|
||||
}
|
||||
|
@ -463,10 +505,10 @@ impl CredHandler {
|
|||
pw_mfa
|
||||
.wan
|
||||
.iter()
|
||||
.map(|(chal, _)| AuthAllowed::Webauthn(chal.clone())),
|
||||
.map(|(chal, _)| AuthAllowed::SecurityKey(chal.clone())),
|
||||
)
|
||||
.collect(),
|
||||
CredHandler::Webauthn(webauthn) => vec![AuthAllowed::Webauthn(webauthn.chal.clone())],
|
||||
CredHandler::Passkey(webauthn) => vec![AuthAllowed::Passkey(webauthn.chal.clone())],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,7 +518,7 @@ impl CredHandler {
|
|||
(CredHandler::Anonymous, AuthMech::Anonymous)
|
||||
| (CredHandler::Password(_, _), AuthMech::Password)
|
||||
| (CredHandler::PasswordMfa(_), AuthMech::PasswordMfa)
|
||||
| (CredHandler::Webauthn(_), AuthMech::Webauthn) => true,
|
||||
| (CredHandler::Passkey(_), AuthMech::Passkey) => true,
|
||||
(_, _) => false,
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +528,7 @@ impl CredHandler {
|
|||
CredHandler::Anonymous => AuthMech::Anonymous,
|
||||
CredHandler::Password(_, _) => AuthMech::Password,
|
||||
CredHandler::PasswordMfa(_) => AuthMech::PasswordMfa,
|
||||
CredHandler::Webauthn(_) => AuthMech::Webauthn,
|
||||
CredHandler::Passkey(_) => AuthMech::Passkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -533,11 +575,7 @@ impl AuthSession {
|
|||
/// Create a new auth session, based on the available credential handlers of the account.
|
||||
/// the session is a whole encapsulated unit of what we need to proceed, so that subsequent
|
||||
/// or interleved write operations do not cause inconsistency in this process.
|
||||
pub fn new(
|
||||
account: Account,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
ct: Duration,
|
||||
) -> (Option<Self>, AuthState) {
|
||||
pub fn new(account: Account, webauthn: &Webauthn, ct: Duration) -> (Option<Self>, AuthState) {
|
||||
// During this setup, determine the credential handler that we'll be using
|
||||
// for this session. This is currently based on presentation of an application
|
||||
// id.
|
||||
|
@ -548,25 +586,31 @@ impl AuthSession {
|
|||
if account.is_anonymous() {
|
||||
AuthSessionState::Init(vec![CredHandler::Anonymous])
|
||||
} else {
|
||||
// Now we see if they have one ...
|
||||
match &account.primary {
|
||||
Some(cred) => {
|
||||
// TODO: Make it possible to have multiple creds.
|
||||
// Probably means new authsession has to be failable
|
||||
CredHandler::try_from(cred, webauthn)
|
||||
.map(|ch| AuthSessionState::Init(vec![ch]))
|
||||
.unwrap_or_else(|_| {
|
||||
security_critical!(
|
||||
"corrupt credentials, unable to start credhandler"
|
||||
);
|
||||
AuthSessionState::Denied("invalid credential state")
|
||||
})
|
||||
}
|
||||
None => {
|
||||
security_info!("account has no primary credentials");
|
||||
AuthSessionState::Denied("invalid credential state")
|
||||
// What's valid to use in this context?
|
||||
let mut handlers = Vec::new();
|
||||
|
||||
if let Some(cred) = &account.primary {
|
||||
// TODO: Make it possible to have multiple creds.
|
||||
// Probably means new authsession has to be failable
|
||||
if let Ok(ch) = CredHandler::try_from((cred, webauthn)) {
|
||||
handlers.push(ch);
|
||||
} else {
|
||||
security_critical!(
|
||||
"corrupt credentials, unable to start primary credhandler"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(ch) = CredHandler::try_from((&account.passkeys, webauthn)) {
|
||||
handlers.push(ch);
|
||||
};
|
||||
|
||||
if handlers.is_empty() {
|
||||
security_info!("account has no primary credentials");
|
||||
AuthSessionState::Denied("invalid credential state")
|
||||
} else {
|
||||
AuthSessionState::Init(handlers)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
security_info!("account expired");
|
||||
|
@ -591,8 +635,16 @@ impl AuthSession {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_account(&self) -> &Account {
|
||||
&self.account
|
||||
pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
|
||||
match &self.state {
|
||||
AuthSessionState::InProgress(CredHandler::Password(_, _))
|
||||
| AuthSessionState::InProgress(CredHandler::PasswordMfa(_)) => {
|
||||
Ok(self.account.primary_cred_uuid())
|
||||
}
|
||||
AuthSessionState::InProgress(CredHandler::Anonymous)
|
||||
| AuthSessionState::InProgress(CredHandler::Passkey(_)) => Ok(None),
|
||||
_ => Err(OperationError::InvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the users indicated and preferred authentication mechanism that they want to proceed
|
||||
|
@ -602,7 +654,7 @@ impl AuthSession {
|
|||
&mut self,
|
||||
mech: &AuthMech,
|
||||
// time: &Duration,
|
||||
// webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
// webauthn: &WebauthnCore,
|
||||
) -> Result<AuthState, OperationError> {
|
||||
// Given some auth mech, select which credential(s) are apropriate
|
||||
// and attempt to use them.
|
||||
|
@ -668,7 +720,7 @@ impl AuthSession {
|
|||
cred: &AuthCredential,
|
||||
time: &Duration,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &Webauthn,
|
||||
pw_badlist_set: Option<&HashSet<String>>,
|
||||
uat_jwt_signer: &JwsSigner,
|
||||
) -> Result<AuthState, OperationError> {
|
||||
|
@ -780,7 +832,6 @@ impl AuthSession {
|
|||
mod tests {
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||
use crate::credential::webauthn::WebauthnDomainConfig;
|
||||
use crate::credential::{BackupCodes, Credential};
|
||||
use crate::idm::authsession::{
|
||||
AuthSession, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG,
|
||||
|
@ -796,10 +847,9 @@ mod tests {
|
|||
use crate::utils::{duration_from_epoch_now, readable_password_from_random};
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthMech};
|
||||
use std::time::Duration;
|
||||
use webauthn_rs::Webauthn;
|
||||
|
||||
use tokio::sync::mpsc::unbounded_channel as unbounded;
|
||||
use webauthn_authenticator_rs::{softtok::U2FSoft, WebauthnAuthenticator};
|
||||
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
|
||||
|
||||
use compact_jwt::JwsSigner;
|
||||
|
||||
|
@ -809,12 +859,13 @@ mod tests {
|
|||
s
|
||||
}
|
||||
|
||||
fn create_webauthn() -> Webauthn<WebauthnDomainConfig> {
|
||||
Webauthn::new(WebauthnDomainConfig {
|
||||
rp_name: "example.com".to_string(),
|
||||
origin: url::Url::parse("https://idm.example.com").unwrap(),
|
||||
rp_id: "example.com".to_string(),
|
||||
})
|
||||
fn create_webauthn() -> webauthn_rs::Webauthn {
|
||||
webauthn_rs::WebauthnBuilder::new(
|
||||
"example.com",
|
||||
&url::Url::parse("https://idm.example.com").unwrap(),
|
||||
)
|
||||
.and_then(|builder| builder.build())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_jwt_signer() -> JwsSigner {
|
||||
|
@ -997,7 +1048,7 @@ mod tests {
|
|||
assert!(
|
||||
true == auth_mechs.iter().fold(false, |acc, x| match x {
|
||||
// TODO: How to return webauthn chal?
|
||||
AuthAllowed::Webauthn(chal) => {
|
||||
AuthAllowed::SecurityKey(chal) => {
|
||||
rchal = Some(chal.clone());
|
||||
true
|
||||
}
|
||||
|
@ -1256,24 +1307,24 @@ mod tests {
|
|||
let mut session = session.unwrap();
|
||||
|
||||
if let AuthState::Choose(auth_mechs) = state {
|
||||
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Webauthn)));
|
||||
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Passkey)));
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
|
||||
let state = session
|
||||
.start_session(&AuthMech::Webauthn)
|
||||
.expect("Failed to select Webauthn mech.");
|
||||
.start_session(&AuthMech::Passkey)
|
||||
.expect("Failed to select Passkey mech.");
|
||||
|
||||
let wan_chal = if let AuthState::Continue(auth_mechs) = state {
|
||||
assert!(auth_mechs.len() == 1);
|
||||
auth_mechs
|
||||
.into_iter()
|
||||
.fold(None, |_acc, x| match x {
|
||||
AuthAllowed::Webauthn(chal) => Some(chal),
|
||||
AuthAllowed::Passkey(chal) => Some(chal),
|
||||
_ => None,
|
||||
})
|
||||
.expect("No webauthn challenge found.")
|
||||
.expect("No securitykey challenge found.")
|
||||
} else {
|
||||
panic!();
|
||||
};
|
||||
|
@ -1282,27 +1333,57 @@ mod tests {
|
|||
}};
|
||||
}
|
||||
|
||||
fn setup_webauthn(
|
||||
fn setup_webauthn_passkey(
|
||||
name: &str,
|
||||
) -> (
|
||||
webauthn_rs::Webauthn<crate::credential::webauthn::WebauthnDomainConfig>,
|
||||
webauthn_authenticator_rs::WebauthnAuthenticator<U2FSoft>,
|
||||
webauthn_rs::proto::Credential,
|
||||
webauthn_rs::prelude::Webauthn,
|
||||
webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
|
||||
webauthn_rs::prelude::Passkey,
|
||||
) {
|
||||
let webauthn = create_webauthn();
|
||||
// Setup a soft token
|
||||
let mut wa = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let (chal, reg_state) = webauthn
|
||||
.generate_challenge_register(name, false)
|
||||
.expect("Failed to setup webauthn rego challenge");
|
||||
.start_passkey_registration(uuid, name, name, None)
|
||||
.expect("Failed to setup passkey rego challenge");
|
||||
|
||||
let r = wa
|
||||
.do_registration("https://idm.example.com", chal)
|
||||
.expect("Failed to create soft token");
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.expect("Failed to create soft passkey");
|
||||
|
||||
let (wan_cred, _) = webauthn
|
||||
.register_credential(&r, ®_state, |_| Ok(false))
|
||||
let wan_cred = webauthn
|
||||
.finish_passkey_registration(&r, ®_state)
|
||||
.expect("Failed to register soft token");
|
||||
|
||||
(webauthn, wa, wan_cred)
|
||||
}
|
||||
|
||||
fn setup_webauthn_securitykey(
|
||||
name: &str,
|
||||
) -> (
|
||||
webauthn_rs::prelude::Webauthn,
|
||||
webauthn_authenticator_rs::WebauthnAuthenticator<SoftPasskey>,
|
||||
webauthn_rs::prelude::SecurityKey,
|
||||
) {
|
||||
let webauthn = create_webauthn();
|
||||
// Setup a soft token
|
||||
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let (chal, reg_state) = webauthn
|
||||
.start_securitykey_registration(uuid, name, name, None, None, None)
|
||||
.expect("Failed to setup passkey rego challenge");
|
||||
|
||||
let r = wa
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.expect("Failed to create soft securitykey");
|
||||
|
||||
let wan_cred = webauthn
|
||||
.finish_securitykey_registration(&r, ®_state)
|
||||
.expect("Failed to register soft token");
|
||||
|
||||
(webauthn, wa, wan_cred)
|
||||
|
@ -1316,11 +1397,11 @@ mod tests {
|
|||
// create the ent
|
||||
let mut account = entry_str_to_account!(JSON_ADMIN_V1);
|
||||
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str());
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
|
||||
let jws_signer = create_jwt_signer();
|
||||
|
||||
// Now create the credential for the account.
|
||||
let cred = Credential::new_webauthn_only("soft".to_string(), wan_cred);
|
||||
let cred = Credential::new_passkey_only("soft".to_string(), wan_cred);
|
||||
account.primary = Some(cred);
|
||||
|
||||
// now check correct mech was offered.
|
||||
|
@ -1348,11 +1429,11 @@ mod tests {
|
|||
let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
|
||||
|
||||
let resp = wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::Passkey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1377,11 +1458,11 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication("https://idm.example.com", inv_chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::Passkey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1395,27 +1476,27 @@ mod tests {
|
|||
|
||||
// Use an incorrect softtoken.
|
||||
{
|
||||
let mut inv_wa = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
let mut inv_wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
let (chal, reg_state) = webauthn
|
||||
.generate_challenge_register(&account.name, false)
|
||||
.start_passkey_registration(account.uuid, &account.name, &account.displayname, None)
|
||||
.expect("Failed to setup webauthn rego challenge");
|
||||
|
||||
let r = inv_wa
|
||||
.do_registration("https://idm.example.com", chal)
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.expect("Failed to create soft token");
|
||||
|
||||
let (inv_cred, _) = webauthn
|
||||
.register_credential(&r, ®_state, |_| Ok(false))
|
||||
let inv_cred = webauthn
|
||||
.finish_passkey_registration(&r, ®_state)
|
||||
.expect("Failed to register soft token");
|
||||
|
||||
// Discard the auth_state, we only need the invalid challenge.
|
||||
let (chal, _auth_state) = webauthn
|
||||
.generate_challenge_authenticate(vec![inv_cred])
|
||||
.start_passkey_authentication(&vec![inv_cred])
|
||||
.expect("Failed to generate challenge for in inv softtoken");
|
||||
|
||||
// Create the response.
|
||||
let resp = inv_wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("Failed to use softtoken for response.");
|
||||
|
||||
let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
|
||||
|
@ -1423,7 +1504,7 @@ mod tests {
|
|||
// get this far, because the client should identify that the cred id's are
|
||||
// not inline.
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::Passkey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1447,7 +1528,7 @@ mod tests {
|
|||
// create the ent
|
||||
let mut account = entry_str_to_account!(JSON_ADMIN_V1);
|
||||
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str());
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
|
||||
let jws_signer = create_jwt_signer();
|
||||
let pw_good = "test_password";
|
||||
let pw_bad = "bad_password";
|
||||
|
@ -1456,7 +1537,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.append_webauthn("soft".to_string(), wan_cred)
|
||||
.append_securitykey("soft".to_string(), wan_cred)
|
||||
.unwrap();
|
||||
|
||||
account.primary = Some(cred);
|
||||
|
@ -1509,11 +1590,11 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication("https://idm.example.com", inv_chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1532,11 +1613,11 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1572,11 +1653,11 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1617,7 +1698,7 @@ mod tests {
|
|||
// create the ent
|
||||
let mut account = entry_str_to_account!(JSON_ADMIN_V1);
|
||||
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str());
|
||||
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
|
||||
let jws_signer = create_jwt_signer();
|
||||
|
||||
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||
|
@ -1636,7 +1717,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.append_webauthn("soft".to_string(), wan_cred)
|
||||
.append_securitykey("soft".to_string(), wan_cred)
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
|
||||
|
@ -1688,11 +1769,11 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication("https://idm.example.com", inv_chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1711,11 +1792,11 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
@ -1809,11 +1890,11 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication("https://idm.example.com", chal)
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Webauthn(resp),
|
||||
&AuthCredential::SecurityKey(resp),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
|
|
|
@ -6,13 +6,22 @@ use crate::idm::server::IdmServerProxyWriteTransaction;
|
|||
use crate::prelude::*;
|
||||
use crate::value::IntentTokenState;
|
||||
use hashbrown::HashSet;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||
|
||||
use kanidm_proto::v1::{CURegState, CUStatus, CredentialDetail, PasswordFeedback, TotpSecret};
|
||||
use kanidm_proto::v1::{
|
||||
CURegState, CUStatus, CredentialDetail, PasskeyDetail, PasswordFeedback, TotpSecret,
|
||||
};
|
||||
|
||||
use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
|
||||
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
use webauthn_rs::prelude::{
|
||||
CreationChallengeResponse, PasskeyRegistration, RegisterPublicKeyCredential,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::fmt;
|
||||
|
@ -21,8 +30,6 @@ use std::sync::Mutex;
|
|||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// use tokio::sync::Mutex;
|
||||
|
||||
use core::ops::Deref;
|
||||
|
||||
const MAXIMUM_CRED_UPDATE_TTL: Duration = Duration::from_secs(900);
|
||||
|
@ -54,11 +61,13 @@ pub struct CredentialUpdateSessionToken {
|
|||
}
|
||||
|
||||
/// The current state of MFA registration
|
||||
#[derive(Clone)]
|
||||
enum MfaRegState {
|
||||
None,
|
||||
TotpInit(Totp),
|
||||
TotpTryAgain(Totp),
|
||||
TotpInvalidSha1(Totp, Totp),
|
||||
Passkey(CreationChallengeResponse, PasskeyRegistration),
|
||||
}
|
||||
|
||||
impl fmt::Debug for MfaRegState {
|
||||
|
@ -68,35 +77,49 @@ impl fmt::Debug for MfaRegState {
|
|||
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
|
||||
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
|
||||
MfaRegState::TotpInvalidSha1(_, _) => "MfaRegState::TotpInvalidSha1",
|
||||
MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
|
||||
};
|
||||
write!(f, "{}", t)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CredentialUpdateSession {
|
||||
issuer: String,
|
||||
// Current credentials - these are on the Account!
|
||||
account: Account,
|
||||
//
|
||||
// What intent was used to initiate this session.
|
||||
intent_token_id: Option<String>,
|
||||
// Acc policy
|
||||
// The credentials as they are being updated
|
||||
|
||||
// The pw credential as they are being updated
|
||||
primary: Option<Credential>,
|
||||
|
||||
// Internal reg state.
|
||||
mfaregstate: MfaRegState,
|
||||
// trusted_devices: Map<Webauthn>?
|
||||
// Passkeys that have been configured.
|
||||
passkeys: BTreeMap<Uuid, (String, PasskeyV4)>,
|
||||
// Devicekeys
|
||||
_devicekeys: BTreeMap<Uuid, (String, DeviceKeyV4)>,
|
||||
|
||||
//
|
||||
// Internal reg state of any inprogress totp or webauthn credentials.
|
||||
mfaregstate: MfaRegState,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CredentialUpdateSession {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let primary: Option<CredentialDetail> = self.primary.as_ref().map(|c| c.into());
|
||||
let passkeys: Vec<PasskeyDetail> = self
|
||||
.passkeys
|
||||
.iter()
|
||||
.map(|(uuid, (tag, _pk))| PasskeyDetail {
|
||||
tag: tag.clone(),
|
||||
uuid: *uuid,
|
||||
})
|
||||
.collect();
|
||||
f.debug_struct("CredentialUpdateSession")
|
||||
.field("account.spn", &self.account.spn)
|
||||
.field("intent_token_id", &self.intent_token_id)
|
||||
.field("primary.detail()", &primary)
|
||||
.field("passkeys.list()", &passkeys)
|
||||
.field("mfaregstate", &self.mfaregstate)
|
||||
.finish()
|
||||
}
|
||||
|
@ -109,6 +132,7 @@ enum MfaRegStateStatus {
|
|||
TotpTryAgain,
|
||||
TotpInvalidSha1,
|
||||
BackupCodes(HashSet<String>),
|
||||
Passkey(CreationChallengeResponse),
|
||||
}
|
||||
|
||||
impl fmt::Debug for MfaRegStateStatus {
|
||||
|
@ -119,6 +143,7 @@ impl fmt::Debug for MfaRegStateStatus {
|
|||
MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
|
||||
MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
|
||||
MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
|
||||
MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
|
||||
};
|
||||
write!(f, "{}", t)
|
||||
}
|
||||
|
@ -133,6 +158,7 @@ pub struct CredentialUpdateSessionStatus {
|
|||
//
|
||||
can_commit: bool,
|
||||
primary: Option<CredentialDetail>,
|
||||
passkeys: Vec<PasskeyDetail>,
|
||||
// Any info the client needs about mfareg state.
|
||||
mfaregstate: MfaRegStateStatus,
|
||||
}
|
||||
|
@ -144,6 +170,7 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
|||
displayname: self.displayname.clone(),
|
||||
can_commit: self.can_commit,
|
||||
primary: self.primary,
|
||||
passkeys: self.passkeys,
|
||||
mfaregstate: match self.mfaregstate {
|
||||
MfaRegStateStatus::None => CURegState::None,
|
||||
MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
|
||||
|
@ -152,6 +179,7 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
|||
MfaRegStateStatus::BackupCodes(s) => {
|
||||
CURegState::BackupCodes(s.into_iter().collect())
|
||||
}
|
||||
MfaRegStateStatus::Passkey(r) => CURegState::Passkey(r),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -165,6 +193,14 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
|||
|
||||
can_commit: true,
|
||||
primary: session.primary.as_ref().map(|c| c.into()),
|
||||
passkeys: session
|
||||
.passkeys
|
||||
.iter()
|
||||
.map(|(uuid, (tag, _pk))| PasskeyDetail {
|
||||
tag: tag.clone(),
|
||||
uuid: *uuid,
|
||||
})
|
||||
.collect(),
|
||||
mfaregstate: match &session.mfaregstate {
|
||||
MfaRegState::None => MfaRegStateStatus::None,
|
||||
MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
|
||||
|
@ -172,6 +208,7 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
|||
),
|
||||
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
|
||||
MfaRegState::TotpInvalidSha1(_, _) => MfaRegStateStatus::TotpInvalidSha1,
|
||||
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +279,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let entry = self.qs_write.internal_search_uuid(&target)?;
|
||||
|
||||
security_info!(
|
||||
?entry,
|
||||
%entry,
|
||||
%target,
|
||||
"Initiating Credential Update Session",
|
||||
);
|
||||
|
@ -255,7 +292,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.get_accesscontrols()
|
||||
.effective_permission_check(
|
||||
&ident,
|
||||
Some(btreeset![AttrString::from("primary_credential")]),
|
||||
Some(btreeset![
|
||||
AttrString::from("primary_credential"),
|
||||
AttrString::from("passkeys"),
|
||||
AttrString::from("devicekeys")
|
||||
]),
|
||||
&[entry],
|
||||
)?;
|
||||
|
||||
|
@ -296,6 +337,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
|
||||
// - stash the current state of all associated credentials
|
||||
let primary = account.primary.clone();
|
||||
let passkeys = account.passkeys.clone();
|
||||
let devicekeys = account.devicekeys.clone();
|
||||
// Stash the issuer for some UI elements
|
||||
let issuer = self.qs_write.get_domain_display_name().to_string();
|
||||
|
||||
|
@ -305,6 +348,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
issuer,
|
||||
intent_token_id,
|
||||
primary,
|
||||
passkeys,
|
||||
_devicekeys: devicekeys,
|
||||
mfaregstate: MfaRegState::None,
|
||||
};
|
||||
|
||||
|
@ -497,7 +542,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let max_ttl = match account.credential_update_intent_tokens.get(&intent_id) {
|
||||
Some(IntentTokenState::Consumed { max_ttl: _ }) => {
|
||||
security_info!(
|
||||
?entry,
|
||||
%entry,
|
||||
%account.uuid,
|
||||
"Rejecting Update Session - Intent Token has already been exchanged",
|
||||
);
|
||||
|
@ -511,14 +556,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
if current_time > *session_ttl {
|
||||
// The former session has expired, continue.
|
||||
security_info!(
|
||||
?entry,
|
||||
%entry,
|
||||
%account.uuid,
|
||||
"Initiating Credential Update Session - Previous session {} has expired", session_id
|
||||
);
|
||||
*max_ttl
|
||||
} else {
|
||||
security_info!(
|
||||
?entry,
|
||||
%entry,
|
||||
%account.uuid,
|
||||
"Rejecting Update Session - Intent Token is in use {}. Try again later", session_id
|
||||
);
|
||||
|
@ -535,7 +580,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
return Err(OperationError::SessionExpired);
|
||||
} else {
|
||||
security_info!(
|
||||
?entry,
|
||||
%entry,
|
||||
%account.uuid,
|
||||
"Initiating Credential Update Session",
|
||||
);
|
||||
|
@ -621,11 +666,19 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
trace!(?removed);
|
||||
}
|
||||
|
||||
pub fn commit_credential_update(
|
||||
// This shares some common paths between commit and cancel.
|
||||
fn credential_update_commit_common(
|
||||
&mut self,
|
||||
cust: CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<(), OperationError> {
|
||||
) -> Result<
|
||||
(
|
||||
ModifyList<ModifyInvalid>,
|
||||
CredentialUpdateSession,
|
||||
CredentialUpdateSessionTokenInner,
|
||||
),
|
||||
OperationError,
|
||||
> {
|
||||
let session_token: CredentialUpdateSessionTokenInner = self
|
||||
.token_enc_key
|
||||
.decrypt(&cust.token_enc)
|
||||
|
@ -652,14 +705,28 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
let session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
let session = session_handle
|
||||
.try_lock()
|
||||
.map(|guard| (*guard).clone())
|
||||
.map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
trace!(?session);
|
||||
|
||||
let mut modlist = ModifyList::new();
|
||||
let modlist = ModifyList::new();
|
||||
|
||||
Ok((modlist, session, session_token))
|
||||
}
|
||||
|
||||
pub fn commit_credential_update(
|
||||
&mut self,
|
||||
cust: CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<(), OperationError> {
|
||||
let (mut modlist, session, session_token) =
|
||||
self.credential_update_commit_common(cust, ct)?;
|
||||
|
||||
// Setup mods for the various bits. We always assert an *exact* state.
|
||||
|
||||
|
@ -724,6 +791,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
};
|
||||
|
||||
// Need to update passkeys.
|
||||
modlist.push_mod(Modify::Purged(AttrString::from("passkeys")));
|
||||
// Add all the passkeys. If none, nothing will be added! This handles
|
||||
// the delete case quite cleanly :)
|
||||
session.passkeys.iter().for_each(|(uuid, (tag, pk))| {
|
||||
let v_pk = Value::Passkey(*uuid, tag.clone(), pk.clone());
|
||||
modlist.push_mod(Modify::Present(AttrString::from("passkeys"), v_pk));
|
||||
});
|
||||
// Are any other checks needed?
|
||||
|
||||
// Apply to the account!
|
||||
|
@ -740,9 +815,73 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
e
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_credential_update(
|
||||
&mut self,
|
||||
cust: CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<(), OperationError> {
|
||||
let (mut modlist, session, session_token) =
|
||||
self.credential_update_commit_common(cust, ct)?;
|
||||
|
||||
// If an intent token was used, remove it's former value, and add it as VALID since we didn't commit.
|
||||
if let Some(intent_token_id) = &session.intent_token_id {
|
||||
let entry = self.qs_write.internal_search_uuid(&session.account.uuid)?;
|
||||
let account = Account::try_from_entry_rw(entry.as_ref(), &mut self.qs_write)?;
|
||||
|
||||
let max_ttl = match account.credential_update_intent_tokens.get(intent_token_id) {
|
||||
Some(IntentTokenState::InProgress {
|
||||
max_ttl,
|
||||
session_id,
|
||||
session_ttl: _,
|
||||
}) => {
|
||||
if *session_id != session_token.sessionid {
|
||||
security_info!("Session originated from an intent token, but the intent token has initiated a conflicting second update session. Refusing to commit changes.");
|
||||
return Err(OperationError::InvalidState);
|
||||
} else {
|
||||
*max_ttl
|
||||
}
|
||||
}
|
||||
Some(IntentTokenState::Consumed { max_ttl: _ })
|
||||
| Some(IntentTokenState::Valid { max_ttl: _ })
|
||||
| None => {
|
||||
security_info!("Session originated from an intent token, but the intent token has transitioned to an invalid state. Refusing to commit changes.");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
};
|
||||
|
||||
modlist.push_mod(Modify::Removed(
|
||||
AttrString::from("credential_update_intent_token"),
|
||||
PartialValue::IntentToken(intent_token_id.clone()),
|
||||
));
|
||||
modlist.push_mod(Modify::Present(
|
||||
AttrString::from("credential_update_intent_token"),
|
||||
Value::IntentToken(intent_token_id.clone(), IntentTokenState::Valid { max_ttl }),
|
||||
));
|
||||
};
|
||||
|
||||
// Apply to the account!
|
||||
trace!(?modlist, "processing change");
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
// Filter as executed
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(session.account.uuid))),
|
||||
&modlist,
|
||||
)
|
||||
.map_err(|e| {
|
||||
request_error!(error = ?e);
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||
#[cfg(test)]
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
self.webauthn.get_origin()
|
||||
}
|
||||
|
||||
fn get_current_session(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
|
@ -1203,6 +1342,95 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub fn credential_passkey_init(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
if !matches!(session.mfaregstate, MfaRegState::None) {
|
||||
admin_info!("Invalid Passkey Init state, another update is in progress");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
|
||||
let (ccr, pk_reg) = self
|
||||
.webauthn
|
||||
.start_passkey_registration(
|
||||
session.account.uuid,
|
||||
&session.account.spn,
|
||||
&session.account.displayname,
|
||||
session.account.existing_credential_id_list(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!(?e, "Unable to start passkey registration");
|
||||
OperationError::Webauthn
|
||||
})?;
|
||||
|
||||
session.mfaregstate = MfaRegState::Passkey(ccr, pk_reg);
|
||||
// Now that it's in the state, it'll be in the status when returned.
|
||||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub fn credential_passkey_finish(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
label: String,
|
||||
reg: RegisterPublicKeyCredential,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
match &session.mfaregstate {
|
||||
MfaRegState::Passkey(_ccr, pk_reg) => {
|
||||
let passkey = self
|
||||
.webauthn
|
||||
.finish_passkey_registration(®, pk_reg)
|
||||
.map_err(|e| {
|
||||
error!(?e, "Unable to start passkey registration");
|
||||
OperationError::Webauthn
|
||||
})?;
|
||||
let pk_id = Uuid::new_v4();
|
||||
session.passkeys.insert(pk_id, (label, passkey));
|
||||
|
||||
// The reg is done.
|
||||
session.mfaregstate = MfaRegState::None;
|
||||
|
||||
Ok(session.deref().into())
|
||||
}
|
||||
_ => Err(OperationError::InvalidRequestState),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn credential_passkey_remove(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
uuid: Uuid,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// No-op if not present
|
||||
session.passkeys.remove(&uuid);
|
||||
|
||||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub fn credential_update_cancel_mfareg(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
|
@ -1245,13 +1473,16 @@ mod tests {
|
|||
};
|
||||
use crate::credential::totp::Totp;
|
||||
use crate::event::{AuthEvent, AuthResult, CreateEvent};
|
||||
use crate::idm::delayed::DelayedAction;
|
||||
use crate::idm::server::IdmServer;
|
||||
use crate::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
|
||||
|
||||
use crate::idm::AuthState;
|
||||
use compiled_uuid::uuid;
|
||||
use kanidm_proto::v1::{AuthMech, CredentialDetailType};
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthMech, CredentialDetailType};
|
||||
|
||||
use async_std::task;
|
||||
|
||||
|
@ -1609,6 +1840,71 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_testperson_passkey(
|
||||
idms: &IdmServer,
|
||||
wa: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
origin: Url,
|
||||
ct: Duration,
|
||||
) -> Option<String> {
|
||||
let mut idms_auth = idms.auth();
|
||||
|
||||
let auth_init = AuthEvent::named_init("testperson");
|
||||
|
||||
let r1 = task::block_on(idms_auth.auth(&auth_init, ct));
|
||||
let ar = r1.unwrap();
|
||||
let AuthResult {
|
||||
sessionid,
|
||||
state,
|
||||
delay: _,
|
||||
} = ar;
|
||||
|
||||
if !matches!(state, AuthState::Choose(_)) {
|
||||
debug!("Can't proceed - {:?}", state);
|
||||
return None;
|
||||
};
|
||||
|
||||
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey);
|
||||
|
||||
let r2 = task::block_on(idms_auth.auth(&auth_begin, ct));
|
||||
let ar = r2.unwrap();
|
||||
let AuthResult {
|
||||
sessionid,
|
||||
state,
|
||||
delay: _,
|
||||
} = ar;
|
||||
|
||||
trace!(?state);
|
||||
|
||||
let rcr = match state {
|
||||
AuthState::Continue(mut allowed) => match allowed.pop() {
|
||||
Some(AuthAllowed::Passkey(rcr)) => rcr,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
trace!(?rcr);
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(origin, rcr)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
|
||||
|
||||
let r3 = task::block_on(idms_auth.auth(&passkey_step, ct));
|
||||
debug!("r3 ==> {:?}", r3);
|
||||
idms_auth.commit().expect("Must not fail");
|
||||
|
||||
match r3 {
|
||||
Ok(AuthResult {
|
||||
sessionid: _,
|
||||
state: AuthState::Success(token),
|
||||
delay: _,
|
||||
}) => Some(token),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_credential_update_session_cleanup() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
|
@ -2024,7 +2320,7 @@ mod tests {
|
|||
//
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
.expect("Failed to update the primary cred totp");
|
||||
|
||||
// Check the status has the token.
|
||||
assert!(c_status.can_commit);
|
||||
|
@ -2054,6 +2350,104 @@ mod tests {
|
|||
// - remove webauthn
|
||||
// - test mulitple webauthn token.
|
||||
|
||||
#[test]
|
||||
fn test_idm_credential_update_onboarding_create_new_passkey() {
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let (cust, _) = setup_test_session(idms, ct);
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
let origin = cutxn.get_origin().clone();
|
||||
|
||||
// Create a soft passkey
|
||||
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
// Start the registration
|
||||
let c_status = cutxn
|
||||
.credential_passkey_init(&cust, ct)
|
||||
.expect("Failed to initiate passkey registration");
|
||||
|
||||
assert!(c_status.passkeys.is_empty());
|
||||
|
||||
let passkey_chal = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::Passkey(c) => Some(c),
|
||||
_ => None,
|
||||
}
|
||||
.expect("Unable to access passkey challenge, invalid state");
|
||||
|
||||
let passkey_resp = wa
|
||||
.do_registration(origin.clone(), passkey_chal)
|
||||
.expect("Failed to create soft passkey");
|
||||
|
||||
// Finish the registration
|
||||
let label = "softtoken".to_string();
|
||||
let c_status = cutxn
|
||||
.credential_passkey_finish(&cust, ct, label, passkey_resp)
|
||||
.expect("Failed to initiate passkey registration");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
// Shuld be none.
|
||||
c_status.primary.as_ref(),
|
||||
None
|
||||
));
|
||||
|
||||
// Check we have the passkey
|
||||
trace!(?c_status);
|
||||
assert!(c_status.passkeys.len() == 1);
|
||||
|
||||
// Get the UUID of the passkey here.
|
||||
let pk_uuid = c_status.passkeys.get(0).map(|pkd| pkd.uuid).unwrap();
|
||||
|
||||
// Commit
|
||||
drop(cutxn);
|
||||
commit_session(idms, ct, cust);
|
||||
|
||||
// Do an auth test
|
||||
assert!(check_testperson_passkey(idms, &mut wa, origin.clone(), ct).is_some());
|
||||
|
||||
// Since it authed, it should have updated the delayed queue.
|
||||
let da = idms_delayed
|
||||
.blocking_recv()
|
||||
.expect("No queued action found!");
|
||||
|
||||
match &da {
|
||||
DelayedAction::WebauthnCounterIncrement(wci) => {
|
||||
trace!("{:?}", wci.auth_result);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let mut idms_prox_write = idms.proxy_write(ct);
|
||||
assert!(idms_prox_write.process_delayedaction(da).is_ok());
|
||||
idms_prox_write.commit().expect("Failed to commit txn");
|
||||
|
||||
// Now test removing the token
|
||||
let (cust, _) = renew_test_session(idms, ct);
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
trace!(?c_status);
|
||||
assert!(c_status.primary.is_none());
|
||||
assert!(c_status.passkeys.len() == 1);
|
||||
|
||||
let c_status = cutxn
|
||||
.credential_passkey_remove(&cust, ct, pk_uuid)
|
||||
.expect("Failed to delete the primary cred");
|
||||
|
||||
trace!(?c_status);
|
||||
assert!(c_status.primary.is_none());
|
||||
assert!(c_status.passkeys.is_empty());
|
||||
|
||||
drop(cutxn);
|
||||
commit_session(idms, ct, cust);
|
||||
|
||||
// Must fail now!
|
||||
assert!(check_testperson_passkey(idms, &mut wa, origin, ct).is_none());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// W_ policy, assert can't remove MFA if it's enforced.
|
||||
|
||||
// enroll trusted device
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::{Counter, CredentialID};
|
||||
use webauthn_rs::prelude::AuthenticationResult;
|
||||
|
||||
pub(crate) enum DelayedAction {
|
||||
PwUpgrade(PasswordUpgrade),
|
||||
|
@ -21,8 +21,7 @@ pub(crate) struct UnixPasswordUpgrade {
|
|||
|
||||
pub(crate) struct WebauthnCounterIncrement {
|
||||
pub target_uuid: Uuid,
|
||||
pub counter: Counter,
|
||||
pub cid: CredentialID,
|
||||
pub auth_result: AuthenticationResult,
|
||||
}
|
||||
|
||||
pub(crate) struct BackupCodeRemoval {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use webauthn_rs::proto::RegisterPublicKeyCredential;
|
||||
|
||||
pub struct PasswordChangeEvent {
|
||||
pub ident: Identity,
|
||||
|
@ -383,107 +381,6 @@ impl RemoveTotpEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebauthnInitRegisterEvent {
|
||||
pub ident: Identity,
|
||||
pub target: Uuid,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl WebauthnInitRegisterEvent {
|
||||
pub fn from_parts(
|
||||
// qs: &QueryServerWriteTransaction,
|
||||
ident: Identity,
|
||||
target: Uuid,
|
||||
label: String,
|
||||
) -> Result<Self, OperationError> {
|
||||
Ok(WebauthnInitRegisterEvent {
|
||||
ident,
|
||||
target,
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, label: String) -> Self {
|
||||
let ident = Identity::from_internal();
|
||||
WebauthnInitRegisterEvent {
|
||||
ident,
|
||||
target,
|
||||
label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebauthnDoRegisterEvent {
|
||||
pub ident: Identity,
|
||||
pub target: Uuid,
|
||||
pub session: Uuid,
|
||||
pub chal: RegisterPublicKeyCredential,
|
||||
}
|
||||
|
||||
impl WebauthnDoRegisterEvent {
|
||||
pub fn from_parts(
|
||||
// qs: &QueryServerWriteTransaction,
|
||||
ident: Identity,
|
||||
target: Uuid,
|
||||
session: Uuid,
|
||||
chal: RegisterPublicKeyCredential,
|
||||
) -> Result<Self, OperationError> {
|
||||
Ok(WebauthnDoRegisterEvent {
|
||||
ident,
|
||||
target,
|
||||
session,
|
||||
chal,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, session: Uuid, chal: RegisterPublicKeyCredential) -> Self {
|
||||
let ident = Identity::from_internal();
|
||||
WebauthnDoRegisterEvent {
|
||||
ident,
|
||||
target,
|
||||
session,
|
||||
chal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoveWebauthnEvent {
|
||||
pub ident: Identity,
|
||||
pub target: Uuid,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl RemoveWebauthnEvent {
|
||||
pub fn from_parts(
|
||||
// qs: &QueryServerWriteTransaction,
|
||||
ident: Identity,
|
||||
target: Uuid,
|
||||
label: String,
|
||||
) -> Result<Self, OperationError> {
|
||||
Ok(RemoveWebauthnEvent {
|
||||
ident,
|
||||
target,
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, label: String) -> Self {
|
||||
let ident = Identity::from_internal();
|
||||
|
||||
RemoveWebauthnEvent {
|
||||
ident,
|
||||
target,
|
||||
label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CredentialStatusEvent {
|
||||
pub ident: Identity,
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||
use crate::credential::webauthn::WebauthnDomainConfig;
|
||||
use crate::identity::IdentityId;
|
||||
use crate::idm::account::Account;
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::TotpSecret;
|
||||
use kanidm_proto::v1::{OperationError, SetCredentialResponse};
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
use webauthn_rs::proto::Credential as WebauthnCredential;
|
||||
use webauthn_rs::proto::{CreationChallengeResponse, RegisterPublicKeyCredential};
|
||||
use webauthn_rs::RegistrationState as WebauthnRegistrationState;
|
||||
use webauthn_rs::Webauthn;
|
||||
|
||||
pub(crate) enum MfaRegCred {
|
||||
Totp(Totp),
|
||||
Webauthn(String, WebauthnCredential),
|
||||
}
|
||||
|
||||
pub(crate) enum MfaRegNext {
|
||||
Success,
|
||||
TotpCheck(TotpSecret),
|
||||
TotpInvalidSha1,
|
||||
WebauthnChallenge(CreationChallengeResponse),
|
||||
}
|
||||
|
||||
impl MfaRegNext {
|
||||
|
@ -33,9 +24,6 @@ impl MfaRegNext {
|
|||
MfaRegNext::Success => SetCredentialResponse::Success,
|
||||
MfaRegNext::TotpCheck(secret) => SetCredentialResponse::TotpCheck(u, secret),
|
||||
MfaRegNext::TotpInvalidSha1 => SetCredentialResponse::TotpInvalidSha1(u),
|
||||
MfaRegNext::WebauthnChallenge(ccr) => {
|
||||
SetCredentialResponse::WebauthnCreateChallenge(u, ccr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +33,6 @@ enum MfaRegState {
|
|||
TotpInit(Totp),
|
||||
TotpInvalidSha1(Totp),
|
||||
TotpDone,
|
||||
WebauthnInit(String, WebauthnRegistrationState),
|
||||
WebauthnDone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -164,59 +150,4 @@ impl MfaRegSession {
|
|||
_ => Err(OperationError::InvalidRequestState),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn webauthn_new(
|
||||
origin: IdentityId,
|
||||
account: Account,
|
||||
label: String,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
issuer: String,
|
||||
) -> Result<(Self, MfaRegNext), OperationError> {
|
||||
// Setup the registration.
|
||||
let (chal, reg_state) = webauthn
|
||||
.generate_challenge_register(&account.name, false)
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to generate webauthn challenge -> {:?}", e);
|
||||
OperationError::Webauthn
|
||||
})?;
|
||||
|
||||
let state = MfaRegState::WebauthnInit(label, reg_state);
|
||||
let s = MfaRegSession {
|
||||
origin,
|
||||
account,
|
||||
state,
|
||||
// this isn't used in webauthn... yet?
|
||||
issuer,
|
||||
};
|
||||
let next = MfaRegNext::WebauthnChallenge(chal);
|
||||
Ok((s, next))
|
||||
}
|
||||
|
||||
pub fn webauthn_step(
|
||||
&mut self,
|
||||
origin: &IdentityId,
|
||||
target: &Uuid,
|
||||
chal: &RegisterPublicKeyCredential,
|
||||
webauthn: &Webauthn<WebauthnDomainConfig>,
|
||||
) -> Result<(MfaRegNext, Option<MfaRegCred>), OperationError> {
|
||||
if &self.origin != origin || target != &self.account.uuid {
|
||||
// Verify that the same event source is the one continuing this attempt
|
||||
return Err(OperationError::InvalidRequestState);
|
||||
};
|
||||
|
||||
// Regardless of the outcome, we are done!
|
||||
let mut nstate = MfaRegState::WebauthnDone;
|
||||
mem::swap(&mut self.state, &mut nstate);
|
||||
|
||||
match nstate {
|
||||
MfaRegState::WebauthnInit(label, reg_state) => webauthn
|
||||
.register_credential(chal, ®_state, |_| Ok(false))
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to register webauthn credential -> {:?}", e);
|
||||
OperationError::Webauthn
|
||||
})
|
||||
.map(|(cred, _)| (MfaRegNext::Success, Some(MfaRegCred::Webauthn(label, cred)))),
|
||||
_ => Err(OperationError::InvalidRequestState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::idm::delayed::{DelayedAction, Oauth2ConsentGrant};
|
|||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::value::OAUTHSCOPE_RE;
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
pub use compact_jwt::{JwkKeySet, OidcToken};
|
||||
use compact_jwt::{JwsSigner, OidcClaims, OidcSubject};
|
||||
use concread::cowcell::*;
|
||||
|
@ -25,7 +26,6 @@ use time::OffsetDateTime;
|
|||
use tokio::sync::mpsc::UnboundedSender as Sender;
|
||||
use tracing::trace;
|
||||
use url::{Origin, Url};
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
pub use kanidm_proto::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
|
@ -1343,9 +1343,9 @@ mod tests {
|
|||
|
||||
use crate::event::{DeleteEvent, ModifyEvent};
|
||||
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use kanidm_proto::oauth2::*;
|
||||
use kanidm_proto::v1::{AuthType, UserAuthToken};
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
use compact_jwt::{JwaAlg, Jwk, JwkUse, JwsValidator, OidcSubject, OidcUnverified};
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::softlock::CredSoftLock;
|
||||
use crate::credential::webauthn::WebauthnDomainConfig;
|
||||
use crate::credential::BackupCodes;
|
||||
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
||||
use crate::identity::{IdentType, IdentUser, Limits};
|
||||
|
@ -10,9 +9,8 @@ use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
|
|||
use crate::idm::event::{
|
||||
AcceptSha1TotpEvent, CredentialStatusEvent, GeneratePasswordEvent, GenerateTotpEvent,
|
||||
LdapAuthEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
RemoveTotpEvent, RemoveWebauthnEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent,
|
||||
UnixUserAuthEvent, UnixUserTokenEvent, VerifyTotpEvent, WebauthnDoRegisterEvent,
|
||||
WebauthnInitRegisterEvent,
|
||||
RemoveTotpEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
|
||||
UnixUserTokenEvent, VerifyTotpEvent,
|
||||
};
|
||||
use crate::idm::mfareg::{MfaRegCred, MfaRegNext, MfaRegSession};
|
||||
use crate::idm::oauth2::{
|
||||
|
@ -71,7 +69,7 @@ use std::{sync::Arc, time::Duration};
|
|||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
use webauthn_rs::Webauthn;
|
||||
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
|
||||
|
||||
use super::delayed::BackupCodeRemoval;
|
||||
use super::event::{GenerateBackupCodeEvent, ReadBackupCodeEvent, RemoveBackupCodeEvent};
|
||||
|
@ -98,7 +96,7 @@ pub struct IdmServer {
|
|||
crypto_policy: CryptoPolicy,
|
||||
async_tx: Sender<DelayedAction>,
|
||||
/// [Webauthn] verifier/config
|
||||
webauthn: Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: Webauthn,
|
||||
pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
|
||||
oauth2rs: Arc<Oauth2ResourceServers>,
|
||||
|
||||
|
@ -118,7 +116,7 @@ pub struct IdmServerAuthTransaction<'a> {
|
|||
sid: Sid,
|
||||
// For flagging eventual actions.
|
||||
async_tx: Sender<DelayedAction>,
|
||||
webauthn: &'a Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &'a Webauthn,
|
||||
pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
|
||||
uat_jwt_signer: CowCellReadTxn<JwsSigner>,
|
||||
uat_jwt_validator: CowCellReadTxn<JwsValidator>,
|
||||
|
@ -127,7 +125,7 @@ pub struct IdmServerAuthTransaction<'a> {
|
|||
pub(crate) struct IdmServerCredUpdateTransaction<'a> {
|
||||
pub _qs_read: QueryServerReadTransaction<'a>,
|
||||
// sid: Sid,
|
||||
pub _webauthn: &'a Webauthn<WebauthnDomainConfig>,
|
||||
pub webauthn: &'a Webauthn,
|
||||
pub pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
|
||||
pub cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>,
|
||||
pub token_enc_key: CowCellReadTxn<Fernet>,
|
||||
|
@ -151,7 +149,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
|
|||
pub(crate) cred_update_sessions: BptreeMapWriteTxn<'a, Uuid, CredentialUpdateSessionMutex>,
|
||||
pub(crate) sid: Sid,
|
||||
crypto_policy: &'a CryptoPolicy,
|
||||
webauthn: &'a Webauthn<WebauthnDomainConfig>,
|
||||
webauthn: &'a Webauthn,
|
||||
pw_badlist_cache: CowCellWriteTxn<'a, HashSet<String>>,
|
||||
uat_jwt_signer: CowCellWriteTxn<'a, JwsSigner>,
|
||||
uat_jwt_validator: CowCellWriteTxn<'a, JwsValidator>,
|
||||
|
@ -181,10 +179,11 @@ impl IdmServer {
|
|||
let (async_tx, async_rx) = unbounded();
|
||||
|
||||
// Get the domain name, as the relying party id.
|
||||
let (rp_id, fernet_private_key, es256_private_key, pw_badlist_set, oauth2rs_set) = {
|
||||
let (rp_id, rp_name, fernet_private_key, es256_private_key, pw_badlist_set, oauth2rs_set) = {
|
||||
let qs_read = task::block_on(qs.read_async());
|
||||
(
|
||||
qs_read.get_domain_name().to_string(),
|
||||
qs_read.get_domain_display_name().to_string(),
|
||||
qs_read.get_domain_fernet_private_key()?,
|
||||
qs_read.get_domain_es256_private_key()?,
|
||||
qs_read.get_password_badlist()?,
|
||||
|
@ -217,14 +216,12 @@ impl IdmServer {
|
|||
}
|
||||
})?;
|
||||
|
||||
// Now clone to rp_name.
|
||||
let rp_name = rp_id.clone();
|
||||
|
||||
let webauthn = Webauthn::new(WebauthnDomainConfig {
|
||||
rp_name,
|
||||
origin: origin_url.clone(),
|
||||
rp_id,
|
||||
});
|
||||
let webauthn = WebauthnBuilder::new(&rp_id, &origin_url)
|
||||
.and_then(|builder| builder.allow_subdomains(true).rp_name(&rp_name).build())
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid Webauthn Configuration - {:?}", e);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
// Setup our auth token signing key.
|
||||
let fernet_key = Fernet::new(&fernet_private_key).ok_or_else(|| {
|
||||
|
@ -350,7 +347,7 @@ impl IdmServer {
|
|||
IdmServerCredUpdateTransaction {
|
||||
_qs_read: self.qs.read_async().await,
|
||||
// sid: Sid,
|
||||
_webauthn: &self.webauthn,
|
||||
webauthn: &self.webauthn,
|
||||
pw_badlist_cache: self.pw_badlist_cache.read(),
|
||||
cred_update_sessions: self.cred_update_sessions.read(),
|
||||
token_enc_key: self.token_enc_key.read(),
|
||||
|
@ -381,6 +378,11 @@ impl IdmServerDelayed {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn blocking_recv(&mut self) -> Option<DelayedAction> {
|
||||
self.async_rx.blocking_recv()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn try_recv(&mut self) -> Result<DelayedAction, OperationError> {
|
||||
let waker = futures_task::noop_waker();
|
||||
|
@ -614,11 +616,12 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
// Intent to take both trees to write.
|
||||
let _session_ticket = self.session_ticket.acquire().await;
|
||||
|
||||
// Check the credential that the auth_session will attempt to
|
||||
// use.
|
||||
// We don't actually check the softlock here. We just initialise
|
||||
// it under the write lock we currently have, so that we can validate
|
||||
// it once we understand what auth mech we will be using.
|
||||
//
|
||||
// NOTE: Very careful use of await here to avoid an issue with write.
|
||||
let maybe_slock_ref =
|
||||
let _maybe_slock_ref =
|
||||
account
|
||||
.primary_cred_uuid_and_policy()
|
||||
.map(|(cred_uuid, policy)| {
|
||||
|
@ -640,6 +643,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
slock_ref
|
||||
});
|
||||
|
||||
/*
|
||||
let mut maybe_slock = if let Some(slock_ref) = maybe_slock_ref.as_ref() {
|
||||
Some(slock_ref.lock().await)
|
||||
} else {
|
||||
|
@ -653,7 +657,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
} else {
|
||||
false
|
||||
};
|
||||
*/
|
||||
|
||||
/*
|
||||
let (auth_session, state) = if is_valid {
|
||||
AuthSession::new(account, self.webauthn, ct)
|
||||
} else {
|
||||
|
@ -664,6 +670,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
AuthState::Denied("Account is temporarily locked".to_string()),
|
||||
)
|
||||
};
|
||||
*/
|
||||
|
||||
let (auth_session, state) = AuthSession::new(account, self.webauthn, ct);
|
||||
|
||||
match auth_session {
|
||||
Some(auth_session) => {
|
||||
|
@ -715,8 +724,11 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
|
||||
let mut auth_session = auth_session_ref.lock().await;
|
||||
|
||||
let is_valid =
|
||||
if let Some(cred_uuid) = auth_session.get_account().primary_cred_uuid() {
|
||||
// Indicate to the session which auth mech we now want to proceed with.
|
||||
let auth_result = auth_session.start_session(&mech.mech);
|
||||
|
||||
let is_valid = match auth_session.get_credential_uuid()? {
|
||||
Some(cred_uuid) => {
|
||||
// From the auth_session, determine if the current account
|
||||
// credential that we are using has become softlocked or not.
|
||||
let softlock_read = self.softlocks.read();
|
||||
|
@ -729,13 +741,12 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
let r = if is_valid {
|
||||
// Indicate to the session which auth mech we now want to proceed with.
|
||||
auth_session.start_session(&mech.mech)
|
||||
let auth_result = if is_valid {
|
||||
auth_result
|
||||
} else {
|
||||
// Fail the session
|
||||
auth_session.end_session("Account is temporarily locked")
|
||||
|
@ -748,9 +759,8 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
delay,
|
||||
}
|
||||
});
|
||||
// softlock_write.commit();
|
||||
// session_write.commit();
|
||||
r
|
||||
|
||||
auth_result
|
||||
} // End AuthEventStep::Mech
|
||||
AuthEventStep::Cred(creds) => {
|
||||
// lperf_segment!("idm::server::auth<Creds>", || {
|
||||
|
@ -769,67 +779,60 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
|
||||
let mut auth_session = auth_session_ref.lock().await;
|
||||
|
||||
let maybe_slock_ref =
|
||||
auth_session
|
||||
.get_account()
|
||||
.primary_cred_uuid()
|
||||
.and_then(|cred_uuid| {
|
||||
let softlock_read = self.softlocks.read();
|
||||
|
||||
softlock_read.get(&cred_uuid).map(|s| s.clone())
|
||||
});
|
||||
let maybe_slock_ref = match auth_session.get_credential_uuid()? {
|
||||
Some(cred_uuid) => {
|
||||
let softlock_read = self.softlocks.read();
|
||||
softlock_read.get(&cred_uuid).map(|s| s.clone())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// From the auth_session, determine if the current account
|
||||
// credential that we are using has become softlocked or not.
|
||||
|
||||
let maybe_slock = if let Some(s) = maybe_slock_ref.as_ref() {
|
||||
let mut maybe_slock = if let Some(s) = maybe_slock_ref.as_ref() {
|
||||
Some(s.lock().await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let maybe_valid = if let Some(mut slock) = maybe_slock {
|
||||
let is_valid = if let Some(ref mut slock) = maybe_slock {
|
||||
// Apply the current time.
|
||||
slock.apply_time_step(ct);
|
||||
// Now check the results
|
||||
if slock.is_valid() {
|
||||
Some(slock)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
slock.is_valid()
|
||||
} else {
|
||||
None
|
||||
// No slock is present for this cred_uuid
|
||||
true
|
||||
};
|
||||
|
||||
let r = match maybe_valid {
|
||||
Some(mut slock) => {
|
||||
// Process the credentials here as required.
|
||||
// Basically throw them at the auth_session and see what
|
||||
// falls out.
|
||||
let pw_badlist_cache = Some(&(*self.pw_badlist_cache));
|
||||
auth_session
|
||||
.validate_creds(
|
||||
&creds.cred,
|
||||
&ct,
|
||||
&self.async_tx,
|
||||
self.webauthn,
|
||||
pw_badlist_cache,
|
||||
&*self.uat_jwt_signer,
|
||||
)
|
||||
.map(|aus| {
|
||||
// Inspect the result:
|
||||
// if it was a failure, we need to inc the softlock.
|
||||
if let AuthState::Denied(_) = &aus {
|
||||
// Update it.
|
||||
let r = if is_valid {
|
||||
// Process the credentials here as required.
|
||||
// Basically throw them at the auth_session and see what
|
||||
// falls out.
|
||||
let pw_badlist_cache = Some(&(*self.pw_badlist_cache));
|
||||
auth_session
|
||||
.validate_creds(
|
||||
&creds.cred,
|
||||
&ct,
|
||||
&self.async_tx,
|
||||
self.webauthn,
|
||||
pw_badlist_cache,
|
||||
&*self.uat_jwt_signer,
|
||||
)
|
||||
.map(|aus| {
|
||||
// Inspect the result:
|
||||
// if it was a failure, we need to inc the softlock.
|
||||
if let AuthState::Denied(_) = &aus {
|
||||
// Update it.
|
||||
if let Some(ref mut slock) = maybe_slock {
|
||||
slock.record_failure(ct);
|
||||
};
|
||||
aus
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
// Fail the session
|
||||
auth_session.end_session("Account is temporarily locked")
|
||||
}
|
||||
}
|
||||
};
|
||||
aus
|
||||
})
|
||||
} else {
|
||||
// Fail the session
|
||||
auth_session.end_session("Account is temporarily locked")
|
||||
}
|
||||
.map(|aus| {
|
||||
// TODO: Change this william!
|
||||
|
@ -1264,6 +1267,10 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
self.webauthn.get_origin()
|
||||
}
|
||||
|
||||
pub fn expire_mfareg_sessions(&mut self, ct: Duration) {
|
||||
// ct is current time - sub the timeout. and then split.
|
||||
let expire = ct - Duration::from_secs(MFAREG_SESSION_TIMEOUT);
|
||||
|
@ -1679,6 +1686,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.map(|_| cleartext)
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn reg_account_webauthn_init(
|
||||
&mut self,
|
||||
wre: &WebauthnInitRegisterEvent,
|
||||
|
@ -1731,10 +1739,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
if let (MfaRegNext::Success, Some(MfaRegCred::Webauthn(label, cred))) = (&next, wan_cred) {
|
||||
// Persist the credential
|
||||
let modlist = session.account.gen_webauthn_mod(label, cred).map_err(|e| {
|
||||
admin_error!("Failed to gen webauthn mod {:?}", e);
|
||||
e
|
||||
})?;
|
||||
let modlist = session
|
||||
.account
|
||||
.gen_securitykey_mod(label, cred)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to gen webauthn mod {:?}", e);
|
||||
e
|
||||
})?;
|
||||
// Perform the mod
|
||||
self.qs_write
|
||||
.impersonate_modify(
|
||||
|
@ -1767,7 +1778,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
let account = self.target_to_account(&rwe.target)?;
|
||||
let modlist = account
|
||||
.gen_webauthn_remove_mod(rwe.label.as_str())
|
||||
.gen_securitykey_remove_mod(rwe.label.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to gen webauthn remove mod {:?}", e);
|
||||
e
|
||||
|
@ -1788,6 +1799,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
})
|
||||
.map(|_| SetCredentialResponse::Success)
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn generate_account_totp(
|
||||
&mut self,
|
||||
|
@ -2014,11 +2026,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
&mut self,
|
||||
wci: &WebauthnCounterIncrement,
|
||||
) -> Result<(), OperationError> {
|
||||
let account = self.target_to_account(&wci.target_uuid)?;
|
||||
let mut account = self.target_to_account(&wci.target_uuid)?;
|
||||
|
||||
// Generate an optional mod and then attempt to apply it.
|
||||
let opt_modlist = account
|
||||
.gen_webauthn_counter_mod(&wci.cid, wci.counter)
|
||||
.gen_webauthn_counter_mod(&wci.auth_result)
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to generate webauthn counter mod {:?}", e);
|
||||
e
|
||||
|
@ -2170,12 +2182,11 @@ mod tests {
|
|||
use crate::credential::totp::Totp;
|
||||
use crate::credential::{Credential, Password};
|
||||
use crate::event::{AuthEvent, AuthResult, CreateEvent, ModifyEvent};
|
||||
use crate::idm::delayed::{BackupCodeRemoval, DelayedAction, WebauthnCounterIncrement};
|
||||
use crate::idm::delayed::{BackupCodeRemoval, DelayedAction};
|
||||
use crate::idm::event::{
|
||||
AcceptSha1TotpEvent, GenerateBackupCodeEvent, GenerateTotpEvent, PasswordChangeEvent,
|
||||
RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, RemoveTotpEvent, RemoveWebauthnEvent,
|
||||
UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
VerifyTotpEvent, WebauthnDoRegisterEvent, WebauthnInitRegisterEvent,
|
||||
RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, RemoveTotpEvent, UnixGroupTokenEvent,
|
||||
UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent, VerifyTotpEvent,
|
||||
};
|
||||
use crate::idm::AuthState;
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
|
@ -2192,7 +2203,6 @@ mod tests {
|
|||
use std::convert::TryFrom;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
use webauthn_authenticator_rs::{softtok::U2FSoft, WebauthnAuthenticator};
|
||||
|
||||
const TEST_PASSWORD: &'static str = "ntaoeuntnaoeuhraohuercahu😍";
|
||||
const TEST_PASSWORD_INC: &'static str = "ntaoentu nkrcgaeunhibwmwmqj;k wqjbkx ";
|
||||
|
@ -2405,13 +2415,7 @@ mod tests {
|
|||
} = ar;
|
||||
|
||||
debug_assert!(delay.is_none());
|
||||
match state {
|
||||
AuthState::Choose(_) => {}
|
||||
_ => {
|
||||
error!("Sessions was not initialised");
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
assert!(matches!(state, AuthState::Choose(_)));
|
||||
|
||||
// Now push that we want the Password Mech.
|
||||
let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
|
||||
|
@ -3469,6 +3473,20 @@ mod tests {
|
|||
idms_auth.auth(&admin_init, Duration::from_secs(TEST_CURRENT_TIME)),
|
||||
);
|
||||
let ar = r1.unwrap();
|
||||
let AuthResult {
|
||||
sessionid,
|
||||
state,
|
||||
delay: _,
|
||||
} = ar;
|
||||
assert!(matches!(state, AuthState::Choose(_)));
|
||||
|
||||
// Soft locks only apply once a mechanism is chosen
|
||||
let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
|
||||
|
||||
let r2 = task::block_on(
|
||||
idms_auth.auth(&admin_begin, Duration::from_secs(TEST_CURRENT_TIME)),
|
||||
);
|
||||
let ar = r2.unwrap();
|
||||
let AuthResult {
|
||||
sessionid: _,
|
||||
state,
|
||||
|
@ -3699,102 +3717,6 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_webauthn_registration_and_counter_inc() {
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = idms.proxy_write(ct.clone());
|
||||
|
||||
let mut wa_softtok = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
|
||||
let wrei = WebauthnInitRegisterEvent::new_internal(
|
||||
UUID_ADMIN.clone(),
|
||||
"softtoken".to_string(),
|
||||
);
|
||||
|
||||
let (sessionid, ccr) = match idms_prox_write.reg_account_webauthn_init(&wrei, ct) {
|
||||
Ok(SetCredentialResponse::WebauthnCreateChallenge(sessionid, ccr)) => {
|
||||
(sessionid, ccr)
|
||||
}
|
||||
_ => {
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
|
||||
let rego = wa_softtok
|
||||
.do_registration("https://idm.example.com", ccr)
|
||||
.expect("Failed to register to softtoken");
|
||||
|
||||
let wdre =
|
||||
WebauthnDoRegisterEvent::new_internal(UUID_ADMIN.clone(), sessionid, rego);
|
||||
|
||||
match idms_prox_write.reg_account_webauthn_complete(&wdre) {
|
||||
Ok(SetCredentialResponse::Success) => {}
|
||||
_ => {
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
|
||||
// Get the account now so we can peek at the registered credential.
|
||||
let account = idms_prox_write
|
||||
.target_to_account(&UUID_ADMIN)
|
||||
.expect("account must exist");
|
||||
|
||||
let cred = account.primary.expect("Must exist.");
|
||||
|
||||
let wcred = cred
|
||||
.webauthn_ref()
|
||||
.expect("must have webauthn")
|
||||
.values()
|
||||
.next()
|
||||
.map(|c| c.clone())
|
||||
.expect("must have a webauthn credential");
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
// ===
|
||||
// Assert we can increment the counter if needed.
|
||||
|
||||
// Assert the delayed action queue is empty
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
// Generate a fake counter increment
|
||||
let da = DelayedAction::WebauthnCounterIncrement(WebauthnCounterIncrement {
|
||||
target_uuid: UUID_ADMIN.clone(),
|
||||
counter: wcred.counter + 1,
|
||||
cid: wcred.cred_id,
|
||||
});
|
||||
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
|
||||
assert!(Ok(true) == r);
|
||||
|
||||
// Check we can remove the webauthn device - provided we set a pw.
|
||||
let mut idms_prox_write = idms.proxy_write(ct.clone());
|
||||
let rwe =
|
||||
RemoveWebauthnEvent::new_internal(UUID_ADMIN.clone(), "softtoken".to_string());
|
||||
// This fails because the acc is webauthn only.
|
||||
match idms_prox_write.remove_account_webauthn(&rwe) {
|
||||
Err(OperationError::InvalidAttribute(_)) => {
|
||||
//ok
|
||||
}
|
||||
_ => assert!(false),
|
||||
};
|
||||
// Reg a pw.
|
||||
let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
assert!(idms_prox_write.set_account_password(&pce).is_ok());
|
||||
// Now remove, it will work.
|
||||
idms_prox_write
|
||||
.remove_account_webauthn(&rwe)
|
||||
.expect("Failed to remove webauthn");
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
check_admin_password(idms, TEST_PASSWORD);
|
||||
// All done!
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_backup_code_removal_delayed_action() {
|
||||
run_idm_test!(
|
||||
|
@ -3803,7 +3725,7 @@ mod tests {
|
|||
let expire = ct - Duration::from_secs(AUTH_SESSION_TIMEOUT);
|
||||
let mut idms_prox_write = idms.proxy_write(ct.clone());
|
||||
|
||||
// let mut wa_softtok = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
// let mut wa_softtok = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
// The account must has primary credential + uses MFA before generating backup codes
|
||||
// Set a password.
|
||||
|
@ -3976,7 +3898,7 @@ mod tests {
|
|||
|
||||
// == webauthn
|
||||
let uat = account
|
||||
.to_userauthtoken(session_id, ct, AuthType::Webauthn)
|
||||
.to_userauthtoken(session_id, ct, AuthType::Passkey)
|
||||
.expect("Unable to create uat");
|
||||
let ident = idms_prox_write
|
||||
.process_uat_to_identity(&uat, ct)
|
||||
|
|
|
@ -75,7 +75,7 @@ pub mod prelude {
|
|||
};
|
||||
pub use crate::filter::{Filter, FilterInvalid, FC};
|
||||
pub use crate::modify::{m_pres, m_purge, m_remove};
|
||||
pub use crate::modify::{Modify, ModifyList};
|
||||
pub use crate::modify::{Modify, ModifyInvalid, ModifyList};
|
||||
|
||||
pub use crate::entry::{
|
||||
Entry, EntryCommitted, EntryInit, EntryInvalid, EntryInvalidCommitted, EntryNew,
|
||||
|
|
|
@ -101,7 +101,7 @@ pub struct SchemaAttribute {
|
|||
impl SchemaAttribute {
|
||||
pub fn try_from(value: &Entry<EntrySealed, EntryCommitted>) -> Result<Self, OperationError> {
|
||||
// Convert entry to a schema attribute.
|
||||
trace!("Converting -> {:?}", value);
|
||||
trace!("Converting -> {}", value);
|
||||
|
||||
// uuid
|
||||
let uuid = value.get_uuid();
|
||||
|
@ -194,6 +194,8 @@ impl SchemaAttribute {
|
|||
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||
SyntaxType::PrivateBinary => v.is_privatebinary(),
|
||||
SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)),
|
||||
SyntaxType::Passkey => matches!(v, PartialValue::Passkey(_)),
|
||||
SyntaxType::DeviceKey => matches!(v, PartialValue::DeviceKey(_)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -234,6 +236,8 @@ impl SchemaAttribute {
|
|||
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||
SyntaxType::PrivateBinary => v.is_privatebinary(),
|
||||
SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)),
|
||||
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
||||
SyntaxType::DeviceKey => matches!(v, Value::DeviceKey(_, _, _)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -304,7 +308,7 @@ pub struct SchemaClass {
|
|||
|
||||
impl SchemaClass {
|
||||
pub fn try_from(value: &Entry<EntrySealed, EntryCommitted>) -> Result<Self, OperationError> {
|
||||
trace!("Converting {:?}", value);
|
||||
trace!("Converting {}", value);
|
||||
// uuid
|
||||
let uuid = value.get_uuid();
|
||||
// Convert entry to a schema class.
|
||||
|
|
|
@ -523,6 +523,8 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute("Private Binary Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute("Intent Token Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute("Passkey Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::DeviceKey => Err(OperationError::InvalidAttribute("DeviceKey Values can not be supplied through modification".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -649,6 +651,16 @@ pub trait QueryServerTransaction<'a> {
|
|||
"Invalid Intent Token ID (uuid) syntax".to_string(),
|
||||
)
|
||||
}),
|
||||
SyntaxType::Passkey => PartialValue::new_passkey_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid Passkey UUID syntax".to_string())
|
||||
}),
|
||||
SyntaxType::DeviceKey => {
|
||||
PartialValue::new_devicekey_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(
|
||||
"Invalid DeviceKey UUID syntax".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -2282,7 +2294,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
filter: &Filter<FilterInvalid>,
|
||||
modlist: &ModifyList<ModifyInvalid>,
|
||||
) -> Result<(), OperationError> {
|
||||
spanned!("server::intenal_modify", {
|
||||
spanned!("server::internal_modify", {
|
||||
let f_valid = filter
|
||||
.validate(self.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
|
@ -2554,6 +2566,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER,
|
||||
JSON_SCHEMA_ATTR_CREDENTIAL_UPDATE_INTENT_TOKEN,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_CONSENT_SCOPE_MAP,
|
||||
JSON_SCHEMA_ATTR_PASSKEYS,
|
||||
JSON_SCHEMA_ATTR_DEVICEKEYS,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_ORGPERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
|
@ -2952,8 +2966,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
|
||||
/// Initiate a domain rename process. This is generally an internal function but it's
|
||||
/// exposed to the cli for admins to be able to initiate the process.
|
||||
pub fn domain_rename(&self) -> Result<(), OperationError> {
|
||||
unsafe { self.domain_rename_inner(self.d_info.d_name.as_str()) }
|
||||
pub fn domain_rename(&self, new_domain_name: &str) -> Result<(), OperationError> {
|
||||
// We can't use the d_info struct here, because this has the database version of the domain
|
||||
// name, not the in memory (config) version. We need to accept the domain's
|
||||
// new name from the caller so we can change this.
|
||||
unsafe { self.domain_rename_inner(new_domain_name) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
|
|
|
@ -22,6 +22,9 @@ use sshkeys::PublicKey as SshPublicKey;
|
|||
|
||||
use regex::Regex;
|
||||
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SPN_RE: Regex = {
|
||||
#[allow(clippy::expect_used)]
|
||||
|
@ -170,6 +173,8 @@ pub enum SyntaxType {
|
|||
OauthScopeMap,
|
||||
PrivateBinary,
|
||||
IntentToken,
|
||||
Passkey,
|
||||
DeviceKey,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -202,6 +207,8 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"OAUTH_SCOPE_MAP" => Ok(SyntaxType::OauthScopeMap),
|
||||
"PRIVATE_BINARY" => Ok(SyntaxType::PrivateBinary),
|
||||
"INTENT_TOKEN" => Ok(SyntaxType::IntentToken),
|
||||
"PASSKEY" => Ok(SyntaxType::Passkey),
|
||||
"DEVICEKEY" => Ok(SyntaxType::DeviceKey),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -235,6 +242,8 @@ impl TryFrom<usize> for SyntaxType {
|
|||
20 => Ok(SyntaxType::OauthScopeMap),
|
||||
21 => Ok(SyntaxType::PrivateBinary),
|
||||
22 => Ok(SyntaxType::IntentToken),
|
||||
23 => Ok(SyntaxType::Passkey),
|
||||
24 => Ok(SyntaxType::DeviceKey),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -266,6 +275,8 @@ impl SyntaxType {
|
|||
SyntaxType::OauthScopeMap => 20,
|
||||
SyntaxType::PrivateBinary => 21,
|
||||
SyntaxType::IntentToken => 22,
|
||||
SyntaxType::Passkey => 23,
|
||||
SyntaxType::DeviceKey => 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,6 +307,8 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::OauthScopeMap => "OAUTH_SCOPE_MAP",
|
||||
SyntaxType::PrivateBinary => "PRIVATE_BINARY",
|
||||
SyntaxType::IntentToken => "INTENT_TOKEN",
|
||||
SyntaxType::Passkey => "PASSKEY",
|
||||
SyntaxType::DeviceKey => "DEVICEKEY",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -341,6 +354,10 @@ pub enum PartialValue {
|
|||
// Float64(f64),
|
||||
RestrictedString(String),
|
||||
IntentToken(String),
|
||||
|
||||
Passkey(Uuid),
|
||||
DeviceKey(Uuid),
|
||||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
AuthSession(Uuid),
|
||||
}
|
||||
|
@ -659,6 +676,14 @@ impl PartialValue {
|
|||
Some(PartialValue::IntentToken(s))
|
||||
}
|
||||
|
||||
pub fn new_passkey_s(us: &str) -> Option<Self> {
|
||||
Uuid::parse_str(us).map(PartialValue::Passkey).ok()
|
||||
}
|
||||
|
||||
pub fn new_devicekey_s(us: &str) -> Option<Self> {
|
||||
Uuid::parse_str(us).map(PartialValue::DeviceKey).ok()
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||
|
@ -683,7 +708,10 @@ impl PartialValue {
|
|||
| PartialValue::Nsuniqueid(s)
|
||||
| PartialValue::EmailAddress(s)
|
||||
| PartialValue::RestrictedString(s) => s.clone(),
|
||||
PartialValue::Refer(u) | PartialValue::Uuid(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::Passkey(u)
|
||||
| PartialValue::DeviceKey(u)
|
||||
| PartialValue::Refer(u)
|
||||
| PartialValue::Uuid(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::Bool(b) => b.to_string(),
|
||||
PartialValue::Syntax(syn) => syn.to_string(),
|
||||
PartialValue::Index(it) => it.to_string(),
|
||||
|
@ -760,6 +788,9 @@ pub enum Value {
|
|||
// Float64(f64),
|
||||
RestrictedString(String),
|
||||
IntentToken(String, IntentTokenState),
|
||||
Passkey(Uuid, String, PasskeyV4),
|
||||
DeviceKey(Uuid, String, DeviceKeyV4),
|
||||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
AuthSession(Uuid),
|
||||
}
|
||||
|
|
|
@ -5,10 +5,15 @@ use crate::valueset::ValueSet;
|
|||
use std::collections::btree_map::Entry as BTreeEntry;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::be::dbvalue::{DbValueCredV1, DbValueIntentTokenStateV1};
|
||||
use crate::be::dbvalue::{
|
||||
DbValueCredV1, DbValueDeviceKeyV1, DbValueIntentTokenStateV1, DbValuePasskeyV1,
|
||||
};
|
||||
use crate::credential::Credential;
|
||||
use crate::valueset::IntentTokenState;
|
||||
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetCredential {
|
||||
map: BTreeMap<String, Credential>,
|
||||
|
@ -343,3 +348,315 @@ impl ValueSetT for ValueSetIntentToken {
|
|||
Some(&self.map)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetPasskey {
|
||||
map: BTreeMap<Uuid, (String, PasskeyV4)>,
|
||||
}
|
||||
|
||||
impl ValueSetPasskey {
|
||||
pub fn new(u: Uuid, t: String, k: PasskeyV4) -> Box<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(u, (t, k));
|
||||
Box::new(ValueSetPasskey { map })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, u: Uuid, t: String, k: PasskeyV4) -> bool {
|
||||
self.map.insert(u, (t, k)).is_none()
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValuePasskeyV1>) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.into_iter()
|
||||
.map(|k| match k {
|
||||
DbValuePasskeyV1::V4 { u, t, k } => Ok((u, (t, k))),
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Box::new(ValueSetPasskey { map }))
|
||||
}
|
||||
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||
where
|
||||
T: IntoIterator<Item = (Uuid, String, PasskeyV4)>,
|
||||
{
|
||||
let map = iter.into_iter().map(|(u, t, k)| (u, (t, k))).collect();
|
||||
Some(Box::new(ValueSetPasskey { map }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetPasskey {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::Passkey(u, t, k) => {
|
||||
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
|
||||
e.insert((t, k));
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Passkey(u) => self.map.remove(u).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Passkey(u) => self.map.contains_key(u),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.map
|
||||
.keys()
|
||||
.map(|u| u.as_hyphenated().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::Passkey
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(self.map.values().map(|(t, _)| t).cloned())
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::Passkey(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, (t, k))| DbValuePasskeyV1::V4 {
|
||||
u: *u,
|
||||
t: t.clone(),
|
||||
k: k.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(self.map.keys().cloned().map(PartialValue::Passkey))
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, (t, k))| Value::Passkey(*u, t.clone(), k.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
// Looks like we may not need this?
|
||||
if let Some(other) = other.as_passkey_map() {
|
||||
&self.map == other
|
||||
} else {
|
||||
// debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_passkey_map() {
|
||||
mergemaps!(self.map, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_passkey_single(&self) -> Option<&PasskeyV4> {
|
||||
if self.map.len() == 1 {
|
||||
self.map.values().take(1).next().map(|(_, k)| k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_passkey_map(&self) -> Option<&BTreeMap<Uuid, (String, PasskeyV4)>> {
|
||||
Some(&self.map)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetDeviceKey {
|
||||
map: BTreeMap<Uuid, (String, DeviceKeyV4)>,
|
||||
}
|
||||
|
||||
impl ValueSetDeviceKey {
|
||||
pub fn new(u: Uuid, t: String, k: DeviceKeyV4) -> Box<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(u, (t, k));
|
||||
Box::new(ValueSetDeviceKey { map })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, u: Uuid, t: String, k: DeviceKeyV4) -> bool {
|
||||
self.map.insert(u, (t, k)).is_none()
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueDeviceKeyV1>) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.into_iter()
|
||||
.map(|k| match k {
|
||||
DbValueDeviceKeyV1::V4 { u, t, k } => Ok((u, (t, k))),
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Box::new(ValueSetDeviceKey { map }))
|
||||
}
|
||||
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||
where
|
||||
T: IntoIterator<Item = (Uuid, String, DeviceKeyV4)>,
|
||||
{
|
||||
let map = iter.into_iter().map(|(u, t, k)| (u, (t, k))).collect();
|
||||
Some(Box::new(ValueSetDeviceKey { map }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetDeviceKey {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::DeviceKey(u, t, k) => {
|
||||
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
|
||||
e.insert((t, k));
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::DeviceKey(u) => self.map.remove(u).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::DeviceKey(u) => self.map.contains_key(u),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.map
|
||||
.keys()
|
||||
.map(|u| u.as_hyphenated().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::DeviceKey
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(self.map.values().map(|(t, _)| t).cloned())
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::DeviceKey(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, (t, k))| DbValueDeviceKeyV1::V4 {
|
||||
u: *u,
|
||||
t: t.clone(),
|
||||
k: k.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(self.map.keys().copied().map(PartialValue::DeviceKey))
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, (t, k))| Value::DeviceKey(*u, t.clone(), k.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
// Looks like we may not need this?
|
||||
if let Some(other) = other.as_devicekey_map() {
|
||||
&self.map == other
|
||||
} else {
|
||||
// debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_devicekey_map() {
|
||||
mergemaps!(self.map, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_devicekey_single(&self) -> Option<&DeviceKeyV4> {
|
||||
if self.map.len() == 1 {
|
||||
self.map.values().take(1).next().map(|(_, k)| k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_devicekey_map(&self) -> Option<&BTreeMap<Uuid, (String, DeviceKeyV4)>> {
|
||||
Some(&self.map)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ use dyn_clone::DynClone;
|
|||
use smolset::SmolSet;
|
||||
// use std::fmt::Debug;
|
||||
|
||||
use webauthn_rs::prelude::DeviceKey as DeviceKeyV4;
|
||||
use webauthn_rs::prelude::Passkey as PasskeyV4;
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
mod address;
|
||||
|
@ -43,7 +46,7 @@ pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
|
|||
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
|
||||
pub use self::bool::ValueSetBool;
|
||||
pub use self::cid::ValueSetCid;
|
||||
pub use self::cred::{ValueSetCredential, ValueSetIntentToken};
|
||||
pub use self::cred::{ValueSetCredential, ValueSetDeviceKey, ValueSetIntentToken, ValueSetPasskey};
|
||||
pub use self::datetime::ValueSetDateTime;
|
||||
pub use self::iname::ValueSetIname;
|
||||
pub use self::index::ValueSetIndex;
|
||||
|
@ -306,6 +309,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
None
|
||||
}
|
||||
|
||||
fn as_passkey_map(&self) -> Option<&BTreeMap<Uuid, (String, PasskeyV4)>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_devicekey_map(&self) -> Option<&BTreeMap<Uuid, (String, DeviceKeyV4)>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_value_single(&self) -> Option<Value> {
|
||||
if self.len() != 1 {
|
||||
None
|
||||
|
@ -442,6 +455,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_passkey_single(&self) -> Option<&PasskeyV4> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_devicekey_single(&self) -> Option<&DeviceKeyV4> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ValueSet {
|
||||
|
@ -539,6 +562,8 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
|||
Value::PublicBinary(t, b) => ValueSetPublicBinary::new(t, b),
|
||||
Value::IntentToken(u, s) => ValueSetIntentToken::new(u, s),
|
||||
Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a),
|
||||
Value::Passkey(u, t, k) => ValueSetPasskey::new(u, t, k),
|
||||
Value::DeviceKey(u, t, k) => ValueSetDeviceKey::new(u, t, k),
|
||||
_ => return Err(OperationError::InvalidValueState),
|
||||
};
|
||||
|
||||
|
@ -576,6 +601,8 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
|||
DbValueSetV2::PublicBinary(set) => ValueSetPublicBinary::from_dbvs2(set),
|
||||
DbValueSetV2::IntentToken(set) => ValueSetIntentToken::from_dbvs2(set),
|
||||
DbValueSetV2::EmailAddress(primary, set) => ValueSetEmailAddress::from_dbvs2(primary, set),
|
||||
DbValueSetV2::Passkey(set) => ValueSetPasskey::from_dbvs2(set),
|
||||
DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set),
|
||||
/*
|
||||
DbValueSetV2::PhoneNumber(set) =>
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) =>
|
||||
|
|
|
@ -48,7 +48,7 @@ tracing-subscriber = "^0.3.14"
|
|||
futures = "^0.3.21"
|
||||
# async-std = { version = "1.6", features = ["tokio1"] }
|
||||
|
||||
webauthn-authenticator-rs = "^0.3.2"
|
||||
webauthn-authenticator-rs = "0.4.2-beta.3"
|
||||
oauth2_ext = { package = "oauth2", version = "^4.1.0", default-features = false }
|
||||
base64 = "^0.13.0"
|
||||
|
||||
|
|
|
@ -702,6 +702,10 @@ pub fn create_https_server(
|
|||
.at("/_commit")
|
||||
.mapped_post(&mut routemap, credential_update_commit);
|
||||
|
||||
cred_route
|
||||
.at("/_cancel")
|
||||
.mapped_post(&mut routemap, credential_update_cancel);
|
||||
|
||||
let mut group_route = appserver.at("/v1/group");
|
||||
group_route
|
||||
.at("/")
|
||||
|
|
|
@ -500,6 +500,18 @@ pub async fn credential_update_commit(mut req: tide::Request<AppState>) -> tide:
|
|||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_cancel(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let session_token: CUSessionToken = req.body_json().await?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_idmcredentialupdatecancel(session_token, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn account_get_id_credential_status(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
|
|
@ -409,7 +409,7 @@ pub fn domain_rename_core(config: &Configuration) {
|
|||
}
|
||||
};
|
||||
|
||||
// setup the qs - *with out* init of the migrations and schema.
|
||||
// Setup the qs, and perform any migrations and changes we may have.
|
||||
let qs = match setup_qs(be, schema, config) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
|
@ -441,7 +441,7 @@ pub fn domain_rename_core(config: &Configuration) {
|
|||
}
|
||||
|
||||
let qs_write = task::block_on(qs.write_async(duration_from_epoch_now()));
|
||||
let r = qs_write.domain_rename().and_then(|_| qs_write.commit());
|
||||
let r = qs_write.domain_rename(new_domain_name).and_then(|_| qs_write.commit());
|
||||
|
||||
match r {
|
||||
Ok(_) => info!("Domain Rename Success!"),
|
||||
|
|
|
@ -45,6 +45,8 @@ pub async fn setup_async_test() -> KanidmClient {
|
|||
admin_password: ADMIN_TEST_PASSWORD.to_string(),
|
||||
});
|
||||
|
||||
let addr = format!("http://localhost:{}", port);
|
||||
|
||||
// Setup the config ...
|
||||
let mut config = Configuration::new();
|
||||
config.address = format!("127.0.0.1:{}", port);
|
||||
|
@ -52,6 +54,8 @@ pub async fn setup_async_test() -> KanidmClient {
|
|||
config.integration_test_config = Some(int_config);
|
||||
config.log_level = Some(LogLevel::Quiet as u32);
|
||||
config.role = ServerRole::WriteReplicaNoUI;
|
||||
config.domain = "localhost".to_string();
|
||||
config.origin = addr.clone();
|
||||
// config.log_level = Some(LogLevel::Verbose as u32);
|
||||
// config.log_level = Some(LogLevel::FullTrace as u32);
|
||||
config.threads = 1;
|
||||
|
@ -62,7 +66,6 @@ pub async fn setup_async_test() -> KanidmClient {
|
|||
// We have to yield now to guarantee that the tide elements are setup.
|
||||
task::yield_now().await;
|
||||
|
||||
let addr = format!("http://127.0.0.1:{}", port);
|
||||
let rsclient = KanidmClientBuilder::new()
|
||||
.address(addr)
|
||||
.no_proxy()
|
||||
|
|
|
@ -64,7 +64,7 @@ async fn test_oauth2_openid_basic_flow() {
|
|||
rsclient
|
||||
.idm_account_person_extend(
|
||||
"admin",
|
||||
Some(&["admin@idm.example.com".to_string()]),
|
||||
Some(&["admin@localhost".to_string()]),
|
||||
Some("Admin Istrator"),
|
||||
)
|
||||
.await
|
||||
|
@ -138,34 +138,32 @@ async fn test_oauth2_openid_basic_flow() {
|
|||
.await
|
||||
.expect("Failed to access response body");
|
||||
|
||||
tracing::trace!(?discovery);
|
||||
|
||||
// Most values are checked in idm/oauth2.rs, but we want to sanity check
|
||||
// the urls here as an extended function smoke test.
|
||||
assert!(
|
||||
discovery.issuer
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration").unwrap()
|
||||
discovery.issuer == Url::parse(&format!("{}/oauth2/openid/test_integration", url)).unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.authorization_endpoint
|
||||
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
|
||||
);
|
||||
assert!(discovery.authorization_endpoint == Url::parse(&format!("{}/ui/oauth2", url)).unwrap());
|
||||
|
||||
assert!(
|
||||
discovery.token_endpoint == Url::parse("https://idm.example.com/oauth2/token").unwrap()
|
||||
);
|
||||
assert!(discovery.token_endpoint == Url::parse(&format!("{}/oauth2/token", url)).unwrap());
|
||||
|
||||
assert!(
|
||||
discovery.userinfo_endpoint
|
||||
== Some(
|
||||
Url::parse("https://idm.example.com/oauth2/openid/test_integration/userinfo")
|
||||
.unwrap()
|
||||
Url::parse(&format!("{}/oauth2/openid/test_integration/userinfo", url)).unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.jwks_uri
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration/public_key.jwk")
|
||||
.unwrap()
|
||||
== Url::parse(&format!(
|
||||
"{}/oauth2/openid/test_integration/public_key.jwk",
|
||||
url
|
||||
))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Step 0 - get the jwks public key.
|
||||
|
@ -319,7 +317,7 @@ async fn test_oauth2_openid_basic_flow() {
|
|||
assert!(tir.active);
|
||||
assert!(tir.scope.is_some());
|
||||
assert!(tir.client_id.as_deref() == Some("test_integration"));
|
||||
assert!(tir.username.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(tir.username.as_deref() == Some("admin@localhost"));
|
||||
assert!(tir.token_type.as_deref() == Some("access_token"));
|
||||
assert!(tir.exp.is_some());
|
||||
assert!(tir.iat.is_some());
|
||||
|
@ -339,10 +337,8 @@ async fn test_oauth2_openid_basic_flow() {
|
|||
|
||||
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
||||
// token and the userinfo endpoints.
|
||||
assert!(
|
||||
oidc.iss == Url::parse("https://idm.example.com/oauth2/openid/test_integration").unwrap()
|
||||
);
|
||||
assert!(oidc.s_claims.email.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(oidc.iss == Url::parse(&format!("{}/oauth2/openid/test_integration", url)).unwrap());
|
||||
assert!(oidc.s_claims.email.as_deref() == Some("admin@localhost"));
|
||||
assert!(oidc.s_claims.email_verified == Some(true));
|
||||
|
||||
let response = client
|
||||
|
|
|
@ -9,7 +9,7 @@ use kanidm_proto::v1::{CURegState, CredentialDetailType, Entry, Filter, Modify,
|
|||
mod common;
|
||||
use crate::common::{setup_async_test, ADMIN_TEST_PASSWORD};
|
||||
|
||||
use webauthn_authenticator_rs::{softtok::U2FSoft, WebauthnAuthenticator};
|
||||
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
|
||||
|
||||
const ADMIN_TEST_PASSWORD_CHANGE: &str = "integration test admin new🎉";
|
||||
const UNIX_TEST_PASSWORD: &str = "unix test user password";
|
||||
|
@ -84,7 +84,7 @@ async fn test_server_whoami_anonymous() {
|
|||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.spn == "anonymous@idm.example.com");
|
||||
assert!(uat.spn == "anonymous@localhost");
|
||||
|
||||
// Do a check of the auth/valid endpoint, tells us if our token
|
||||
// is okay.
|
||||
|
@ -111,7 +111,7 @@ async fn test_server_whoami_admin_simple_password() {
|
|||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.spn == "admin@idm.example.com");
|
||||
assert!(uat.spn == "admin@localhost");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -301,7 +301,7 @@ async fn test_server_rest_group_lifecycle() {
|
|||
.await
|
||||
.unwrap();
|
||||
let members = rsclient.idm_group_get_members("demo_group").await.unwrap();
|
||||
assert!(members == Some(vec!["admin@idm.example.com".to_string()]));
|
||||
assert!(members == Some(vec!["admin@localhost".to_string()]));
|
||||
|
||||
// Set the list of members
|
||||
rsclient
|
||||
|
@ -312,8 +312,8 @@ async fn test_server_rest_group_lifecycle() {
|
|||
assert!(
|
||||
members
|
||||
== Some(vec![
|
||||
"admin@idm.example.com".to_string(),
|
||||
"demo_group@idm.example.com".to_string()
|
||||
"admin@localhost".to_string(),
|
||||
"demo_group@localhost".to_string()
|
||||
])
|
||||
);
|
||||
|
||||
|
@ -323,7 +323,7 @@ async fn test_server_rest_group_lifecycle() {
|
|||
.await
|
||||
.unwrap();
|
||||
let members = rsclient.idm_group_get_members("demo_group").await.unwrap();
|
||||
assert!(members == Some(vec!["admin@idm.example.com".to_string()]));
|
||||
assert!(members == Some(vec!["admin@localhost".to_string()]));
|
||||
|
||||
// purge members
|
||||
rsclient
|
||||
|
@ -346,7 +346,7 @@ async fn test_server_rest_group_lifecycle() {
|
|||
// They should have members
|
||||
let members = rsclient.idm_group_get_members("idm_admins").await.unwrap();
|
||||
println!("{:?}", members);
|
||||
assert!(members == Some(vec!["idm_admin@idm.example.com".to_string()]));
|
||||
assert!(members == Some(vec!["idm_admin@localhost".to_string()]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1062,201 +1062,6 @@ async fn test_server_rest_backup_code_auth_lifecycle() {
|
|||
.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_rest_webauthn_auth_lifecycle() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a new account
|
||||
rsclient
|
||||
.idm_account_create("demo_account", "Deeeeemo")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enroll a soft token to the account webauthn.
|
||||
let mut wa_softtok = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
|
||||
// Do the challenge
|
||||
let (sessionid, regchal) = rsclient
|
||||
.idm_account_primary_credential_register_webauthn("demo_account", "softtok")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rego = wa_softtok
|
||||
.do_registration("https://idm.example.com", regchal)
|
||||
.expect("Failed to register to softtoken");
|
||||
|
||||
// Enroll the cred after signing.
|
||||
rsclient
|
||||
.idm_account_primary_credential_complete_webuthn_registration(
|
||||
"demo_account",
|
||||
rego,
|
||||
sessionid,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// ====== Reg a second token.
|
||||
let mut wa_softtok_2 = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
|
||||
// Do the challenge
|
||||
let (sessionid, regchal) = rsclient
|
||||
.idm_account_primary_credential_register_webauthn("demo_account", "softtok_2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rego = wa_softtok_2
|
||||
.do_registration("https://idm.example.com", regchal)
|
||||
.expect("Failed to register to softtoken");
|
||||
|
||||
// Enroll the cred after signing.
|
||||
rsclient
|
||||
.idm_account_primary_credential_complete_webuthn_registration(
|
||||
"demo_account",
|
||||
rego,
|
||||
sessionid,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now do an auth
|
||||
let rsclient_good = rsclient.new_session().unwrap();
|
||||
|
||||
let pkr = rsclient_good
|
||||
.auth_webauthn_begin("demo_account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the auth chal.
|
||||
let auth = wa_softtok_2
|
||||
.do_authentication("https://idm.example.com", pkr)
|
||||
.expect("Failed to auth to softtoken");
|
||||
|
||||
// Submit the webauthn auth.
|
||||
rsclient_good
|
||||
.auth_webauthn_complete(auth)
|
||||
.await
|
||||
.expect("Failed to authenticate");
|
||||
|
||||
// ======== remove the second softtok.
|
||||
|
||||
rsclient
|
||||
.idm_account_primary_credential_remove_webauthn("demo_account", "softtok_2")
|
||||
.await
|
||||
.expect("failed to remove softtoken");
|
||||
|
||||
// All good, check first tok auth.
|
||||
|
||||
let rsclient_good = rsclient.new_session().unwrap();
|
||||
|
||||
let pkr = rsclient_good
|
||||
.auth_webauthn_begin("demo_account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the auth chal.
|
||||
let auth = wa_softtok
|
||||
.do_authentication("https://idm.example.com", pkr)
|
||||
.expect("Failed to auth to softtoken");
|
||||
|
||||
// Submit the webauthn auth.
|
||||
rsclient_good
|
||||
.auth_webauthn_complete(auth)
|
||||
.await
|
||||
.expect("Failed to authenticate");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_rest_webauthn_mfa_auth_lifecycle() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a new account
|
||||
rsclient
|
||||
.idm_account_create("demo_account", "Deeeeemo")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enroll a soft token to the account webauthn.
|
||||
let mut wa_softtok = WebauthnAuthenticator::new(U2FSoft::new());
|
||||
|
||||
// Do the challenge
|
||||
let (sessionid, regchal) = rsclient
|
||||
.idm_account_primary_credential_register_webauthn("demo_account", "softtok")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rego = wa_softtok
|
||||
.do_registration("https://idm.example.com", regchal)
|
||||
.expect("Failed to register to softtoken");
|
||||
|
||||
// Enroll the cred after signing.
|
||||
rsclient
|
||||
.idm_account_primary_credential_complete_webuthn_registration(
|
||||
"demo_account",
|
||||
rego,
|
||||
sessionid,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now do an auth
|
||||
let rsclient_good = rsclient.new_session().unwrap();
|
||||
|
||||
let pkr = rsclient_good
|
||||
.auth_webauthn_begin("demo_account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the auth chal.
|
||||
let auth = wa_softtok
|
||||
.do_authentication("https://idm.example.com", pkr)
|
||||
.expect("Failed to auth to softtoken");
|
||||
|
||||
// Submit the webauthn auth.
|
||||
rsclient_good
|
||||
.auth_webauthn_complete(auth)
|
||||
.await
|
||||
.expect("Failed to authenticate");
|
||||
|
||||
// Set a password to cause the state to change to PasswordMfa
|
||||
assert!(rsclient
|
||||
.idm_account_primary_credential_set_password("demo_account", "sohdi3iuHo6mai7noh0a")
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
// Now remove Webauthn ...
|
||||
rsclient
|
||||
.idm_account_primary_credential_remove_webauthn("demo_account", "softtok")
|
||||
.await
|
||||
.expect("failed to remove softtoken");
|
||||
|
||||
// Check pw only
|
||||
let rsclient_good = rsclient.new_session().unwrap();
|
||||
assert!(rsclient_good
|
||||
.auth_simple_password("demo_account", "sohdi3iuHo6mai7noh0a")
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_rest_oauth2_basic_lifecycle() {
|
||||
let rsclient = setup_async_test().await;
|
||||
|
@ -1560,3 +1365,92 @@ async fn test_server_credential_update_session_totp_pw() {
|
|||
.await;
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_credential_update_session_passkey() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create an account
|
||||
rsclient
|
||||
.idm_account_create("demo_account", "Demo Account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create an intent token for them
|
||||
let intent_token = rsclient
|
||||
.idm_account_credential_update_intent("demo_account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Logout, we don't need any auth now.
|
||||
let _ = rsclient.logout();
|
||||
// Exchange the intent token
|
||||
let (session_token, _status) = rsclient
|
||||
.idm_account_credential_update_exchange(intent_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _status = rsclient
|
||||
.idm_account_credential_update_status(&session_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Setup and update the passkey
|
||||
let mut wa = WebauthnAuthenticator::new(SoftPasskey::new());
|
||||
|
||||
let status = rsclient
|
||||
.idm_account_credential_update_passkey_init(&session_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let passkey_chal = match status.mfaregstate {
|
||||
CURegState::Passkey(c) => Some(c),
|
||||
_ => None,
|
||||
}
|
||||
.expect("Unable to access passkey challenge, invalid state");
|
||||
|
||||
eprintln!("{}", rsclient.get_origin());
|
||||
let passkey_resp = wa
|
||||
.do_registration(rsclient.get_origin().clone(), passkey_chal)
|
||||
.expect("Failed to create soft passkey");
|
||||
|
||||
let label = "Soft Passkey".to_string();
|
||||
|
||||
let status = rsclient
|
||||
.idm_account_credential_update_passkey_finish(&session_token, label, passkey_resp)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(status.can_commit);
|
||||
assert!(status.passkeys.len() == 1);
|
||||
|
||||
// Commit it
|
||||
rsclient
|
||||
.idm_account_credential_update_commit(&session_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Assert it now works.
|
||||
let _ = rsclient.logout();
|
||||
let res = rsclient
|
||||
.auth_passkey_begin("demo_account")
|
||||
.await
|
||||
.expect("Failed to start passkey auth");
|
||||
|
||||
let pkc = wa
|
||||
.do_authentication(rsclient.get_origin().clone(), res)
|
||||
.expect("Failed to authentication with soft passkey");
|
||||
|
||||
let res = rsclient.auth_passkey_complete(pkc).await;
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
|
|
@ -13,6 +13,18 @@ documentation = "https://docs.rs/kanidm/latest/kanidm/"
|
|||
homepage = "https://github.com/kanidm/kanidm/"
|
||||
repository = "https://github.com/kanidm/kanidm/"
|
||||
|
||||
[profile.release]
|
||||
# less code to include into binary
|
||||
panic = 'abort'
|
||||
# optimization over all codebase ( better optimization, slower build )
|
||||
codegen-units = 1
|
||||
# optimization for size ( more aggressive )
|
||||
opt-level = 'z'
|
||||
# optimization for size
|
||||
# opt-level = 's'
|
||||
# link time optimization using using whole-program analysis
|
||||
lto = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
@ -22,9 +34,8 @@ serde_json = "^1.0.82"
|
|||
|
||||
wasm-bindgen = { version = "^0.2.81", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = { version = "^0.4.30" }
|
||||
kanidm_proto = { path = "../kanidm_proto" }
|
||||
kanidm_proto = { path = "../kanidm_proto", features = ["wasm"] }
|
||||
|
||||
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }
|
||||
qrcode = { version = "^0.12.0", default-features = false, features = ["svg"] }
|
||||
|
||||
yew-router = "^0.16.0"
|
||||
|
@ -33,9 +44,13 @@ yew-agent = "^0.1.0"
|
|||
gloo = "^0.8.0"
|
||||
js-sys = "^0.3.58"
|
||||
|
||||
uuid = "^1.1.2"
|
||||
|
||||
compact_jwt = { version = "^0.2.3", default-features = false, features = ["unsafe_release_without_verify"] }
|
||||
# compact_jwt = { path = "../../compact_jwt" , default-features = false, features = ["unsafe_release_without_verify"] }
|
||||
|
||||
wee_alloc = "^0.4.5"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "^0.3.57"
|
||||
features = [
|
||||
|
|
6
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
6
kanidmd_web_ui/pkg/kanidmd_web_ui.d.ts
vendored
|
@ -12,9 +12,9 @@ export interface InitOutput {
|
|||
readonly __wbindgen_malloc: (a: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc0b84f4ddf4a8fd2: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hfed1f3471f1b926f: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h3b7aa7dd2123cac1: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc22c8c82b073edeb: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h88db5b93ed6c64b4: (a: number, b: number, c: number) => void;
|
||||
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha0cda2043cde760e: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
readonly __wbindgen_free: (a: number, b: number) => void;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
|
|
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
@ -5,9 +5,9 @@ export function run_app(a: number): void;
|
|||
export function __wbindgen_malloc(a: number): number;
|
||||
export function __wbindgen_realloc(a: number, b: number, c: number): number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export function _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc0b84f4ddf4a8fd2(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hfed1f3471f1b926f(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h3b7aa7dd2123cac1(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc22c8c82b073edeb(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h88db5b93ed6c64b4(a: number, b: number, c: number): void;
|
||||
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha0cda2043cde760e(a: number, b: number, c: number): void;
|
||||
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||
export function __wbindgen_free(a: number, b: number): void;
|
||||
export function __wbindgen_exn_store(a: number): void;
|
||||
|
|
|
@ -2,5 +2,7 @@ pub mod reset;
|
|||
|
||||
mod delete;
|
||||
mod eventbus;
|
||||
mod passkey;
|
||||
mod passkeyremove;
|
||||
mod pwmodal;
|
||||
mod totpmodal;
|
||||
|
|
392
kanidmd_web_ui/src/credential/passkey.rs
Normal file
392
kanidmd_web_ui/src/credential/passkey.rs
Normal file
|
@ -0,0 +1,392 @@
|
|||
use crate::error::*;
|
||||
use crate::utils;
|
||||
|
||||
use super::eventbus::{EventBus, EventBusMsg};
|
||||
use super::reset::ModalProps;
|
||||
|
||||
use gloo::console;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::Dispatched;
|
||||
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||
|
||||
use kanidm_proto::v1::{CURegState, CURequest, CUSessionToken, CUStatus};
|
||||
use kanidm_proto::webauthn::{CreationChallengeResponse, RegisterPublicKeyCredential};
|
||||
|
||||
pub struct PasskeyModalApp {
|
||||
state: State,
|
||||
label_val: String,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
Init,
|
||||
FetchingChallenge,
|
||||
ChallengeReady(CreationChallengeResponse),
|
||||
CredentialReady(RegisterPublicKeyCredential),
|
||||
Submitting,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
LabelCheck,
|
||||
Cancel,
|
||||
Submit,
|
||||
Generate,
|
||||
Success,
|
||||
ChallengeReady(CreationChallengeResponse),
|
||||
CredentialCreate,
|
||||
CredentialReady(RegisterPublicKeyCredential),
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
NavigatorError,
|
||||
}
|
||||
|
||||
impl From<FetchError> for Msg {
|
||||
fn from(fe: FetchError) -> Self {
|
||||
Msg::Error {
|
||||
emsg: fe.as_string(),
|
||||
kopid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PasskeyModalApp {
|
||||
fn reset_and_hide(&mut self) {
|
||||
utils::modal_hide_by_id("staticPasskeyCreate");
|
||||
self.state = State::Init;
|
||||
self.label_val = "".to_string();
|
||||
}
|
||||
|
||||
async fn submit_passkey_update(
|
||||
token: CUSessionToken,
|
||||
req: CURequest,
|
||||
) -> Result<Msg, FetchError> {
|
||||
let req_jsvalue = serde_json::to_string(&(req, token))
|
||||
.map(|s| JsValue::from(&s))
|
||||
.expect_throw("Failed to serialise pw curequest");
|
||||
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(RequestMode::SameOrigin);
|
||||
|
||||
opts.body(Some(&req_jsvalue));
|
||||
|
||||
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||
request
|
||||
.headers()
|
||||
.set("content-type", "application/json")
|
||||
.expect_throw("failed to set header");
|
||||
|
||||
let window = utils::window();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
||||
if status == 200 {
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
|
||||
|
||||
EventBus::dispatcher().send(EventBusMsg::UpdateStatus {
|
||||
status: status.clone(),
|
||||
});
|
||||
|
||||
Ok(match status.mfaregstate {
|
||||
CURegState::TotpCheck(_)
|
||||
| CURegState::TotpTryAgain
|
||||
| CURegState::TotpInvalidSha1
|
||||
| CURegState::BackupCodes(_) => Msg::Error {
|
||||
emsg: "Invalid Passkey reg state response".to_string(),
|
||||
kopid,
|
||||
},
|
||||
CURegState::Passkey(challenge) => Msg::ChallengeReady(challenge),
|
||||
CURegState::None => Msg::Success,
|
||||
})
|
||||
} else {
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||
Ok(Msg::Error { emsg, kopid })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PasskeyModalApp {
|
||||
type Message = Msg;
|
||||
type Properties = ModalProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
console::log!("passkey modal create");
|
||||
|
||||
PasskeyModalApp {
|
||||
state: State::Init,
|
||||
label_val: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||
console::log!("passkey modal::change");
|
||||
false
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
console::log!("passkey modal::update");
|
||||
match msg {
|
||||
Msg::LabelCheck => {
|
||||
let label = utils::get_value_from_element_id("passkey-label")
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
|
||||
self.label_val = label;
|
||||
}
|
||||
Msg::Submit => {
|
||||
if let State::CredentialReady(rpkc) = &self.state {
|
||||
let rpkc = rpkc.clone();
|
||||
let label = self.label_val.clone();
|
||||
// Init a fetch to get the challenge.
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_passkey_update(
|
||||
token_c,
|
||||
CURequest::PasskeyFinish(label, rpkc),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
self.state = State::Submitting;
|
||||
}
|
||||
// Error?
|
||||
}
|
||||
Msg::Success => {
|
||||
self.reset_and_hide();
|
||||
}
|
||||
Msg::Generate => {
|
||||
// Init a fetch to get the challenge.
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_passkey_update(token_c, CURequest::PasskeyInit).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
self.state = State::FetchingChallenge;
|
||||
}
|
||||
Msg::ChallengeReady(challenge) => {
|
||||
console::log!(format!("{:?}", challenge).as_str());
|
||||
self.state = State::ChallengeReady(challenge);
|
||||
}
|
||||
Msg::CredentialCreate => {
|
||||
if let State::ChallengeReady(ccr) = &self.state {
|
||||
let ccr = ccr.clone();
|
||||
let c_options: web_sys::CredentialCreationOptions = ccr.into();
|
||||
|
||||
// Create a promise that calls the browsers navigator.credentials.create api.
|
||||
let promise = utils::window()
|
||||
.navigator()
|
||||
.credentials()
|
||||
.create_with_options(&c_options)
|
||||
.expect_throw("Unable to create promise");
|
||||
let fut = JsFuture::from(promise);
|
||||
|
||||
// Wait on the promise, when complete it will issue a callback.
|
||||
ctx.link().send_future(async move {
|
||||
match fut.await {
|
||||
Ok(jsval) => {
|
||||
// Convert from the raw js value into the expected PublicKeyCredential
|
||||
let w_rpkc = web_sys::PublicKeyCredential::from(jsval);
|
||||
// Serialise the web_sys::pkc into the webauthn proto version, ready to
|
||||
// handle/transmit.
|
||||
let rpkc = RegisterPublicKeyCredential::from(w_rpkc);
|
||||
|
||||
// Update our state
|
||||
Msg::CredentialReady(rpkc)
|
||||
}
|
||||
Err(e) => {
|
||||
console::log!(format!("error -> {:?}", e).as_str());
|
||||
Msg::NavigatorError
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Msg::CredentialReady(rpkc) => {
|
||||
console::log!(format!("{:?}", rpkc).as_str());
|
||||
self.state = State::CredentialReady(rpkc);
|
||||
}
|
||||
Msg::NavigatorError => {
|
||||
// Do something useful, like prompt or have a breadcrumb. But it's
|
||||
// not a full error.
|
||||
}
|
||||
Msg::Cancel => {
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_passkey_update(token_c, CURequest::CancelMFAReg).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
self.state = State::FetchingChallenge;
|
||||
}
|
||||
Msg::Error { emsg, kopid } => {
|
||||
// Submit the error to the parent.
|
||||
EventBus::dispatcher().send(EventBusMsg::Error { emsg, kopid });
|
||||
self.reset_and_hide();
|
||||
}
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||
console::log!("passkey modal::rendered");
|
||||
}
|
||||
|
||||
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||
console::log!("passkey modal::destroy");
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
console::log!("passkey modal::view");
|
||||
|
||||
let label_val = self.label_val.clone();
|
||||
|
||||
let passkey_state = match &self.state {
|
||||
State::Init => {
|
||||
html! {
|
||||
<button id="passkey-generate" type="button" class="btn btn-secondary"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Generate
|
||||
})
|
||||
}
|
||||
>{ "Start Creating a New Passkey" }</button>
|
||||
}
|
||||
}
|
||||
State::Submitting | State::FetchingChallenge => {
|
||||
html! {
|
||||
<div class="spinner-border text-dark" role="status">
|
||||
<span class="visually-hidden">{ "Loading..." }</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
State::ChallengeReady(_challenge) => {
|
||||
// This works around a bug in safari :(
|
||||
html! {
|
||||
<button id="passkey-generate" type="button" class="btn btn-primary"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::CredentialCreate
|
||||
})
|
||||
}
|
||||
>{ "Do it!" }</button>
|
||||
}
|
||||
}
|
||||
State::CredentialReady(_) => {
|
||||
html! {
|
||||
<h3>{ "Passkey Created!" }</h3>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let submit_enabled =
|
||||
!label_val.is_empty() && matches!(self.state, State::CredentialReady(_));
|
||||
|
||||
let submit_state = match &self.state {
|
||||
State::CredentialReady(_rpkc) => {
|
||||
html! {
|
||||
<button id="passkey-submit" type="button" class="btn btn-primary"
|
||||
disabled={ !submit_enabled }
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Submit
|
||||
})
|
||||
}
|
||||
>{ "Submit" }</button>
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
html! {
|
||||
<button id="passkey-cancel" type="button" class="btn btn-secondary"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Cancel
|
||||
})
|
||||
}
|
||||
>{ "Cancel" }</button>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="modal fade" id="staticPasskeyCreate" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticPasskeyLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staticPasskeyLabel">{ "Add a New Passkey" }</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Cancel
|
||||
})
|
||||
}
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{ passkey_state }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="row g-3 needs-validation" novalidate=true
|
||||
onsubmit={ ctx.link().callback(move |e: FocusEvent| {
|
||||
console::log!("passkey modal::on form submit prevent default");
|
||||
e.prevent_default();
|
||||
if submit_enabled {
|
||||
Msg::Submit
|
||||
} else {
|
||||
Msg::Cancel
|
||||
}
|
||||
} ) }
|
||||
>
|
||||
<label for="passkey-label" class="form-label">{ "Enter Label for Passkey" }</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="passkey-label"
|
||||
placeholder=""
|
||||
value={ label_val }
|
||||
required=true
|
||||
oninput={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::LabelCheck
|
||||
})
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{ submit_state }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
243
kanidmd_web_ui/src/credential/passkeyremove.rs
Normal file
243
kanidmd_web_ui/src/credential/passkeyremove.rs
Normal file
|
@ -0,0 +1,243 @@
|
|||
use crate::error::*;
|
||||
use crate::utils;
|
||||
|
||||
use super::eventbus::{EventBus, EventBusMsg};
|
||||
use super::reset::PasskeyRemoveModalProps;
|
||||
|
||||
use gloo::console;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::Dispatched;
|
||||
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use kanidm_proto::v1::{CURegState, CURequest, CUSessionToken, CUStatus};
|
||||
|
||||
pub struct PasskeyRemoveModalApp {
|
||||
state: State,
|
||||
target: String,
|
||||
tag: String,
|
||||
uuid: Uuid,
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
Init,
|
||||
Submitting,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Cancel,
|
||||
Submit,
|
||||
Success,
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
}
|
||||
|
||||
impl From<FetchError> for Msg {
|
||||
fn from(fe: FetchError) -> Self {
|
||||
Msg::Error {
|
||||
emsg: fe.as_string(),
|
||||
kopid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PasskeyRemoveModalApp {
|
||||
pub fn render_button(tag: &str, uuid: Uuid) -> Html {
|
||||
let remove_tgt = format!("#staticPasskeyRemove-{}", uuid);
|
||||
let tag = tag.to_string();
|
||||
|
||||
html! {
|
||||
<li>
|
||||
<div class="row g-3">
|
||||
<p>{ tag }</p>
|
||||
<button type="button" class="btn btn-dark btn-sml" data-bs-toggle="modal" data-bs-target={ remove_tgt }>
|
||||
{ "Remove" }
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_and_hide(&mut self) {
|
||||
utils::modal_hide_by_id(&self.target);
|
||||
self.state = State::Init;
|
||||
}
|
||||
|
||||
async fn submit_passkey_update(
|
||||
token: CUSessionToken,
|
||||
req: CURequest,
|
||||
) -> Result<Msg, FetchError> {
|
||||
let req_jsvalue = serde_json::to_string(&(req, token))
|
||||
.map(|s| JsValue::from(&s))
|
||||
.expect_throw("Failed to serialise pw curequest");
|
||||
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(RequestMode::SameOrigin);
|
||||
|
||||
opts.body(Some(&req_jsvalue));
|
||||
|
||||
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||
request
|
||||
.headers()
|
||||
.set("content-type", "application/json")
|
||||
.expect_throw("failed to set header");
|
||||
|
||||
let window = utils::window();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
||||
if status == 200 {
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type");
|
||||
|
||||
EventBus::dispatcher().send(EventBusMsg::UpdateStatus {
|
||||
status: status.clone(),
|
||||
});
|
||||
|
||||
Ok(match status.mfaregstate {
|
||||
CURegState::TotpCheck(_)
|
||||
| CURegState::TotpTryAgain
|
||||
| CURegState::TotpInvalidSha1
|
||||
| CURegState::Passkey(_)
|
||||
| CURegState::BackupCodes(_) => Msg::Error {
|
||||
emsg: "Invalid Passkey reg state response".to_string(),
|
||||
kopid,
|
||||
},
|
||||
CURegState::None => Msg::Success,
|
||||
})
|
||||
} else {
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||
Ok(Msg::Error { emsg, kopid })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PasskeyRemoveModalApp {
|
||||
type Message = Msg;
|
||||
type Properties = PasskeyRemoveModalProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
console::log!("passkey remove modal create");
|
||||
|
||||
let tag = ctx.props().tag.clone();
|
||||
let uuid = ctx.props().uuid.clone();
|
||||
let target = format!("staticPasskeyRemove-{}", uuid);
|
||||
|
||||
PasskeyRemoveModalApp {
|
||||
state: State::Init,
|
||||
tag,
|
||||
uuid,
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||
console::log!("passkey remove modal::change");
|
||||
false
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
console::log!("passkey remove modal::update");
|
||||
match msg {
|
||||
Msg::Submit => {
|
||||
self.reset_and_hide();
|
||||
|
||||
// Do the call back.
|
||||
let token_c = ctx.props().token.clone();
|
||||
let uuid = self.uuid.clone();
|
||||
|
||||
ctx.link().send_future(async move {
|
||||
match Self::submit_passkey_update(token_c, CURequest::PasskeyRemove(uuid)).await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
self.state = State::Submitting;
|
||||
}
|
||||
Msg::Success | Msg::Cancel => {
|
||||
self.reset_and_hide();
|
||||
}
|
||||
Msg::Error { emsg, kopid } => {
|
||||
// Submit the error to the parent.
|
||||
EventBus::dispatcher().send(EventBusMsg::Error { emsg, kopid });
|
||||
self.reset_and_hide();
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||
console::log!("passkey remove modal::rendered");
|
||||
}
|
||||
|
||||
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||
console::log!("passkey remove modal::destroy");
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
console::log!("passkey remove modal::view");
|
||||
|
||||
let remove_tgt = self.target.clone();
|
||||
let remove_id = format!("staticPasskeyRemove-{}", self.uuid);
|
||||
let remove_label = format!("staticPasskeyRemoveLabel-{}", self.uuid);
|
||||
|
||||
let msg = format!("Delete the Passkey named '{}'?", self.tag);
|
||||
|
||||
let submit_enabled = matches!(self.state, State::Init);
|
||||
|
||||
html! {
|
||||
<div class="modal fade" id={ remove_id } data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby={ remove_tgt } aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id={ remove_label }>{ "Delete Passkey" }</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Cancel
|
||||
})
|
||||
}
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p>{ msg }</p>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="delete-cancel" type="button" class="btn btn-secondary"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Cancel
|
||||
})
|
||||
}
|
||||
>{ "Cancel" }</button>
|
||||
<button id="delete-submit" type="button" class="btn btn-danger"
|
||||
disabled={ !submit_enabled }
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Submit
|
||||
})
|
||||
}
|
||||
>{ "Submit" }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,14 +17,25 @@ use web_sys::{Request, RequestInit, RequestMode, Response};
|
|||
|
||||
use super::delete::DeleteApp;
|
||||
use super::eventbus::{EventBus, EventBusMsg};
|
||||
use super::passkey::PasskeyModalApp;
|
||||
use super::passkeyremove::PasskeyRemoveModalApp;
|
||||
use super::pwmodal::PwModalApp;
|
||||
use super::totpmodal::TotpModalApp;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct ModalProps {
|
||||
pub token: CUSessionToken,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct PasskeyRemoveModalProps {
|
||||
pub token: CUSessionToken,
|
||||
pub tag: String,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
TokenSubmit,
|
||||
BeginSession {
|
||||
|
@ -34,6 +45,7 @@ pub enum Msg {
|
|||
UpdateSession {
|
||||
status: CUStatus,
|
||||
},
|
||||
Cancel,
|
||||
Commit,
|
||||
Success,
|
||||
Error {
|
||||
|
@ -187,9 +199,22 @@ impl Component for CredentialResetApp {
|
|||
|
||||
Some(State::WaitingForCommit)
|
||||
}
|
||||
(Msg::Cancel, State::Main { token, status }) => {
|
||||
console::log!(format!("{:?}", status).as_str());
|
||||
let token_c = token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::cancel_session(token_c).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
Some(State::WaitingForCommit)
|
||||
}
|
||||
(Msg::Success, State::WaitingForCommit) => {
|
||||
let loc = models::pop_return_location();
|
||||
console::log!(format!("credential was updated, try going to -> {:?}", loc));
|
||||
console::log!(format!("Going to -> {:?}", loc));
|
||||
loc.goto(&ctx.link().history().expect_throw("failed to read history"));
|
||||
|
||||
None
|
||||
|
@ -292,7 +317,6 @@ impl CredentialResetApp {
|
|||
let pw_html = match &status.primary {
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
claims: _,
|
||||
type_: CredentialDetailType::Password,
|
||||
}) => {
|
||||
html! {
|
||||
|
@ -312,7 +336,6 @@ impl CredentialResetApp {
|
|||
}
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
claims: _,
|
||||
type_: CredentialDetailType::GeneratedPassword,
|
||||
}) => {
|
||||
html! {
|
||||
|
@ -326,23 +349,22 @@ impl CredentialResetApp {
|
|||
}
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
claims: _,
|
||||
type_: CredentialDetailType::Webauthn(_),
|
||||
type_: CredentialDetailType::Passkey(_),
|
||||
}) => {
|
||||
html! {
|
||||
<>
|
||||
<p>{ "Webauthn Only - Will migrate to trusted devices in a future update" }</p>
|
||||
<p>{ "Webauthn Only - Will migrate to Passkeys in a future update" }</p>
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||
{ "Delete this Password" }
|
||||
{ "Delete this Credential" }
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
claims: _,
|
||||
type_:
|
||||
// TODO: review this and find out why we aren't using these variables
|
||||
// Because I'm lazy 🙃
|
||||
CredentialDetailType::PasswordMfa(
|
||||
_totp_set,
|
||||
_security_key_labels,
|
||||
|
@ -373,6 +395,34 @@ impl CredentialResetApp {
|
|||
}
|
||||
};
|
||||
|
||||
let passkey_html = if status.passkeys.is_empty() {
|
||||
html! {
|
||||
<p>{ "No Passkeys Registered" }</p>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="container">
|
||||
<ul class="list-unstyled">
|
||||
{ for status.passkeys.iter()
|
||||
.map(|detail|
|
||||
PasskeyRemoveModalApp::render_button(&detail.tag, detail.uuid)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
let passkey_modals_html = html! {
|
||||
<>
|
||||
{ for status.passkeys.iter()
|
||||
.map(|detail|
|
||||
html! { <PasskeyRemoveModalApp token={ token.clone() } tag={ detail.tag.clone() } uuid={ detail.uuid } /> }
|
||||
)
|
||||
}
|
||||
</>
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="d-flex align-items-start form-cred-reset-body">
|
||||
<main class="w-100">
|
||||
|
@ -386,8 +436,10 @@ impl CredentialResetApp {
|
|||
<form class="needs-validation" novalidate=true>
|
||||
<hr class="my-4" />
|
||||
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTrustedDevice">
|
||||
{ "Add New Trusted Device" }
|
||||
{ passkey_html }
|
||||
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticPasskeyCreate">
|
||||
{ "Add Passkey" }
|
||||
</button>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
@ -395,7 +447,17 @@ impl CredentialResetApp {
|
|||
{ pw_html }
|
||||
|
||||
<hr class="my-4" />
|
||||
<button class="w-100 btn btn-success btn-lg" type="submit"
|
||||
|
||||
<button class="w-50 btn btn-danger btn-lg" type="submit"
|
||||
disabled=false
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::Cancel
|
||||
})
|
||||
}
|
||||
>{ "Cancel" }</button>
|
||||
<button class="w-50 btn btn-success btn-lg" type="submit"
|
||||
disabled={ !can_commit }
|
||||
onclick={
|
||||
ctx.link()
|
||||
|
@ -408,28 +470,7 @@ impl CredentialResetApp {
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal fade" id="staticTrustedDevice" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticTrustedDeviceLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="staticTrustedDeviceLabel">{ "Add a Trusted Device" }</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{ "Scan the following link to add a new device" }</p>
|
||||
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">{ "Loading..." }</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{ "Cancel" }</button>
|
||||
<button type="button" class="btn btn-primary">{ "Submit" }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PasskeyModalApp token={ token.clone() } />
|
||||
|
||||
<PwModalApp token={ token.clone() } />
|
||||
|
||||
|
@ -437,10 +478,10 @@ impl CredentialResetApp {
|
|||
|
||||
<DeleteApp token= { token.clone() }/>
|
||||
|
||||
{ passkey_modals_html }
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
// <DelPrimaryModalApp token={ token.clone() }/>
|
||||
}
|
||||
|
||||
fn view_error(&self, _ctx: &Context<Self>, msg: &str, kopid: Option<&str>) -> Html {
|
||||
|
@ -499,7 +540,7 @@ impl CredentialResetApp {
|
|||
}
|
||||
}
|
||||
|
||||
async fn commit_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
||||
async fn end_session(token: CUSessionToken, url: &str) -> Result<Msg, FetchError> {
|
||||
let req_jsvalue = serde_json::to_string(&token)
|
||||
.map(|s| JsValue::from(&s))
|
||||
.expect_throw("Failed to serialise session token");
|
||||
|
@ -510,7 +551,7 @@ impl CredentialResetApp {
|
|||
|
||||
opts.body(Some(&req_jsvalue));
|
||||
|
||||
let request = Request::new_with_str_and_init("/v1/credential/_commit", &opts)?;
|
||||
let request = Request::new_with_str_and_init(url, &opts)?;
|
||||
request
|
||||
.headers()
|
||||
.set("content-type", "application/json")
|
||||
|
@ -531,4 +572,12 @@ impl CredentialResetApp {
|
|||
Ok(Msg::Error { emsg, kopid })
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
||||
Self::end_session(token, "/v1/credential/_cancel").await
|
||||
}
|
||||
|
||||
async fn commit_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
||||
Self::end_session(token, "/v1/credential/_commit").await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ enum TotpCheck {
|
|||
|
||||
enum TotpValue {
|
||||
Init,
|
||||
Waiting,
|
||||
Secret(TotpSecret),
|
||||
}
|
||||
|
||||
|
@ -39,6 +40,7 @@ pub struct TotpModalApp {
|
|||
}
|
||||
|
||||
pub enum Msg {
|
||||
TotpGenerate,
|
||||
TotpCancel,
|
||||
TotpSubmit,
|
||||
TotpSecretReady(TotpSecret),
|
||||
|
@ -101,7 +103,7 @@ impl TotpModalApp {
|
|||
});
|
||||
|
||||
Ok(match status.mfaregstate {
|
||||
CURegState::BackupCodes(_) => Msg::Error {
|
||||
CURegState::Passkey(_) | CURegState::BackupCodes(_) => Msg::Error {
|
||||
emsg: "Invalid TOTP mfa reg state response".to_string(),
|
||||
kopid,
|
||||
},
|
||||
|
@ -122,19 +124,9 @@ impl Component for TotpModalApp {
|
|||
type Message = Msg;
|
||||
type Properties = ModalProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
console::log!("totp modal create");
|
||||
|
||||
// SEND OFF A REQUEST TO GET THE TOTP STRING
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_totp_update(token_c, CURequest::TotpGenerate).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
TotpModalApp {
|
||||
state: TotpState::Init,
|
||||
check: TotpCheck::Init,
|
||||
|
@ -186,6 +178,19 @@ impl Component for TotpModalApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
Msg::TotpGenerate => {
|
||||
// SEND OFF A REQUEST TO GET THE TOTP STRING
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_totp_update(token_c, CURequest::TotpGenerate).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
|
||||
self.secret = TotpValue::Waiting;
|
||||
}
|
||||
Msg::TotpSecretReady(secret) => {
|
||||
// THIS IS WHATS CALLED WHEN THE SECRET IS BACK
|
||||
self.secret = TotpValue::Secret(secret);
|
||||
|
@ -280,6 +285,18 @@ impl Component for TotpModalApp {
|
|||
|
||||
let totp_secret_state = match &self.secret {
|
||||
TotpValue::Init => {
|
||||
html! {
|
||||
<button id="totp-generate" type="button" class="btn btn-secondary"
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::TotpGenerate
|
||||
})
|
||||
}
|
||||
>{ "Generate TOTP" }</button>
|
||||
}
|
||||
}
|
||||
TotpValue::Waiting => {
|
||||
html! {
|
||||
<div class="spinner-border text-dark" role="status">
|
||||
<span class="visually-hidden">{ "Loading..." }</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#![recursion_limit = "256"]
|
||||
// #![deny(warnings)]
|
||||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
#![deny(clippy::todo)]
|
||||
#![deny(clippy::unimplemented)]
|
||||
|
@ -11,6 +11,9 @@
|
|||
#![deny(clippy::needless_pass_by_value)]
|
||||
#![deny(clippy::trivially_copy_pass_by_ref)]
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
|
|
|
@ -12,10 +12,9 @@ use crate::models;
|
|||
use crate::utils;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep,
|
||||
AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, AuthMech
|
||||
};
|
||||
|
||||
use webauthn_rs::proto::PublicKeyCredential;
|
||||
use kanidm_proto::webauthn::PublicKeyCredential;
|
||||
|
||||
pub struct LoginApp {
|
||||
inputvalue: String,
|
||||
|
@ -32,11 +31,17 @@ enum TotpState {
|
|||
|
||||
enum LoginState {
|
||||
Init(bool),
|
||||
// Select between different cred types
|
||||
Select(Vec<AuthMech>),
|
||||
// The choices of current ways to proceed.
|
||||
Continue(Vec<AuthAllowed>),
|
||||
// The different methods
|
||||
Password(bool),
|
||||
BackupCode(bool),
|
||||
Totp(TotpState),
|
||||
Webauthn(web_sys::CredentialRequestOptions),
|
||||
Passkey(web_sys::CredentialRequestOptions),
|
||||
SecurityKey(web_sys::CredentialRequestOptions),
|
||||
// Error, state handling.
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
UnknownUser,
|
||||
Denied(String),
|
||||
|
@ -50,10 +55,12 @@ pub enum LoginAppMsg {
|
|||
PasswordSubmit,
|
||||
BackupCodeSubmit,
|
||||
TotpSubmit,
|
||||
WebauthnSubmit(PublicKeyCredential),
|
||||
PasskeySubmit(PublicKeyCredential),
|
||||
SecurityKeySubmit(PublicKeyCredential),
|
||||
Start(String, AuthResponse),
|
||||
Next(AuthResponse),
|
||||
Continue(usize),
|
||||
Select(usize),
|
||||
// DoNothing,
|
||||
UnknownUser,
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
|
@ -175,6 +182,18 @@ impl LoginApp {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_mech_select(&self, ctx: &Context<Self>, idx: usize, allow: &AuthMech) -> Html {
|
||||
html! {
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-dark"
|
||||
onclick={ ctx.link().callback(move |_| LoginAppMsg::Select(idx)) }
|
||||
>{ allow.to_string() }</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
fn view_state(&self, ctx: &Context<Self>) -> Html {
|
||||
let inputvalue = self.inputvalue.clone();
|
||||
match &self.state {
|
||||
|
@ -213,6 +232,24 @@ impl LoginApp {
|
|||
</>
|
||||
}
|
||||
}
|
||||
LoginState::Select(mechs) => {
|
||||
html! {
|
||||
<>
|
||||
<div class="container">
|
||||
<p>
|
||||
{" Which credential would you like to use? "}
|
||||
</p>
|
||||
</div>
|
||||
<div class="container">
|
||||
<ul class="list-unstyled">
|
||||
{ for mechs.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, mech)| self.render_mech_select(ctx, idx, mech)) }
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
LoginState::Continue(allowed) => {
|
||||
html! {
|
||||
<>
|
||||
|
@ -331,7 +368,7 @@ impl LoginApp {
|
|||
</>
|
||||
}
|
||||
}
|
||||
LoginState::Webauthn(challenge) => {
|
||||
LoginState::SecurityKey(challenge) => {
|
||||
// Start the navigator parts.
|
||||
if let Some(win) = web_sys::window() {
|
||||
let promise = win
|
||||
|
@ -348,7 +385,7 @@ impl LoginApp {
|
|||
let data = PublicKeyCredential::from(
|
||||
web_sys::PublicKeyCredential::from(data),
|
||||
);
|
||||
linkc.send_message(LoginAppMsg::WebauthnSubmit(data));
|
||||
linkc.send_message(LoginAppMsg::SecurityKeySubmit(data));
|
||||
}
|
||||
Err(e) => {
|
||||
linkc.send_message(LoginAppMsg::Error {
|
||||
|
@ -368,7 +405,49 @@ impl LoginApp {
|
|||
html! {
|
||||
<div class="container">
|
||||
<p>
|
||||
{" Webauthn "}
|
||||
{" Security Key "}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
LoginState::Passkey(challenge) => {
|
||||
// Start the navigator parts.
|
||||
if let Some(win) = web_sys::window() {
|
||||
let promise = win
|
||||
.navigator()
|
||||
.credentials()
|
||||
.get_with_options(challenge)
|
||||
.expect_throw("Unable to create promise");
|
||||
let fut = JsFuture::from(promise);
|
||||
let linkc = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match fut.await {
|
||||
Ok(data) => {
|
||||
let data = PublicKeyCredential::from(
|
||||
web_sys::PublicKeyCredential::from(data),
|
||||
);
|
||||
linkc.send_message(LoginAppMsg::PasskeySubmit(data));
|
||||
}
|
||||
Err(e) => {
|
||||
linkc.send_message(LoginAppMsg::Error {
|
||||
emsg: format!("{:?}", e),
|
||||
kopid: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ctx.link().send_message(LoginAppMsg::Error {
|
||||
emsg: "failed to access navigator credentials".to_string(),
|
||||
kopid: None,
|
||||
});
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="container">
|
||||
<p>
|
||||
{" Passkey "}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
@ -570,10 +649,25 @@ impl Component for LoginApp {
|
|||
|
||||
true
|
||||
}
|
||||
LoginAppMsg::WebauthnSubmit(resp) => {
|
||||
console::log!("At webauthn step".to_string());
|
||||
LoginAppMsg::SecurityKeySubmit(resp) => {
|
||||
console::log!("At securitykey step".to_string());
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Cred(AuthCredential::Webauthn(resp)),
|
||||
step: AuthStep::Cred(AuthCredential::SecurityKey(resp)),
|
||||
};
|
||||
let session_id = self.session_id.clone();
|
||||
ctx.link().send_future(async {
|
||||
match Self::auth_step(authreq, session_id).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
// Do not submit here, we need to wait for the next ui transition.
|
||||
false
|
||||
}
|
||||
LoginAppMsg::PasskeySubmit(resp) => {
|
||||
console::log!("At passkey step".to_string());
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Cred(AuthCredential::Passkey(resp)),
|
||||
};
|
||||
let session_id = self.session_id.clone();
|
||||
ctx.link().send_future(async {
|
||||
|
@ -608,12 +702,8 @@ impl Component for LoginApp {
|
|||
// We do NOT need to change state or redraw
|
||||
false
|
||||
} else {
|
||||
// Offer the choices.
|
||||
console::log!("This is currently unimplemented".to_string());
|
||||
self.state = LoginState::Error {
|
||||
emsg: "Unimplemented".to_string(),
|
||||
kopid: None,
|
||||
};
|
||||
console::log!("multiple mechs exist".to_string());
|
||||
self.state = LoginState::Select(mechs);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -632,6 +722,42 @@ impl Component for LoginApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
LoginAppMsg::Select(idx) => {
|
||||
console::log!(format!("chose -> {:?}", idx));
|
||||
match &self.state {
|
||||
LoginState::Select(allowed) => {
|
||||
match allowed.get(idx) {
|
||||
Some(mech) => {
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Begin(mech.clone()),
|
||||
};
|
||||
let session_id = self.session_id.clone();
|
||||
ctx.link().send_future(async {
|
||||
match Self::auth_step(authreq, session_id).await {
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
}
|
||||
None => {
|
||||
console::log!("invalid allowed mech idx".to_string());
|
||||
self.state = LoginState::Error {
|
||||
emsg: "Invalid Continue Index".to_string(),
|
||||
kopid: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
console::log!("invalid state transition".to_string());
|
||||
self.state = LoginState::Error {
|
||||
emsg: "Invalid UI State Transition".to_string(),
|
||||
kopid: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
LoginAppMsg::Next(resp) => {
|
||||
// Clear any leftover input
|
||||
self.inputvalue = "".to_string();
|
||||
|
@ -664,8 +790,11 @@ impl Component for LoginApp {
|
|||
AuthAllowed::Totp => {
|
||||
self.state = LoginState::Totp(TotpState::Enabled);
|
||||
}
|
||||
AuthAllowed::Webauthn(challenge) => {
|
||||
self.state = LoginState::Webauthn(challenge.into())
|
||||
AuthAllowed::SecurityKey(challenge) => {
|
||||
self.state = LoginState::SecurityKey(challenge.into())
|
||||
}
|
||||
AuthAllowed::Passkey(challenge) => {
|
||||
self.state = LoginState::Passkey(challenge.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -707,8 +836,11 @@ impl Component for LoginApp {
|
|||
Some(AuthAllowed::Totp) => {
|
||||
self.state = LoginState::Totp(TotpState::Enabled);
|
||||
}
|
||||
Some(AuthAllowed::Webauthn(challenge)) => {
|
||||
self.state = LoginState::Webauthn(challenge.clone().into())
|
||||
Some(AuthAllowed::SecurityKey(challenge)) => {
|
||||
self.state = LoginState::SecurityKey(challenge.clone().into())
|
||||
}
|
||||
Some(AuthAllowed::Passkey(challenge)) => {
|
||||
self.state = LoginState::Passkey(challenge.clone().into())
|
||||
}
|
||||
None => {
|
||||
console::log!("invalid allowed mech idx".to_string());
|
||||
|
|
Loading…
Reference in a new issue