mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
20250205 3369 firefox pin (#3403)
Improve error message when passkey is missing PIN Firefox still doesn't support setting a PIN on new devices. Because of this we need a way to return a better error message for devices that don't have UV configured.
This commit is contained in:
parent
43b7f80535
commit
ad3cf8828f
|
@ -160,6 +160,7 @@ pub enum CURegWarning {
|
||||||
AttestedResidentKeyRequired,
|
AttestedResidentKeyRequired,
|
||||||
Unsatisfiable,
|
Unsatisfiable,
|
||||||
WebauthnAttestationUnsatisfiable,
|
WebauthnAttestationUnsatisfiable,
|
||||||
|
WebauthnUserVerificationRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
|
|
@ -73,6 +73,7 @@ struct CredStatusView {
|
||||||
#[template(path = "credentials_update_partial.html")]
|
#[template(path = "credentials_update_partial.html")]
|
||||||
struct CredResetPartialView {
|
struct CredResetPartialView {
|
||||||
ext_cred_portal: CUExtPortal,
|
ext_cred_portal: CUExtPortal,
|
||||||
|
can_commit: bool,
|
||||||
warnings: Vec<CURegWarning>,
|
warnings: Vec<CURegWarning>,
|
||||||
attested_passkeys_state: CUCredState,
|
attested_passkeys_state: CUCredState,
|
||||||
passkeys_state: CUCredState,
|
passkeys_state: CUCredState,
|
||||||
|
@ -899,6 +900,7 @@ pub(crate) async fn view_reset_get(
|
||||||
fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
||||||
let CUStatus {
|
let CUStatus {
|
||||||
ext_cred_portal,
|
ext_cred_portal,
|
||||||
|
can_commit,
|
||||||
warnings,
|
warnings,
|
||||||
passkeys_state,
|
passkeys_state,
|
||||||
attested_passkeys_state,
|
attested_passkeys_state,
|
||||||
|
@ -913,6 +915,7 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
||||||
|
|
||||||
CredResetPartialView {
|
CredResetPartialView {
|
||||||
ext_cred_portal,
|
ext_cred_portal,
|
||||||
|
can_commit,
|
||||||
warnings,
|
warnings,
|
||||||
attested_passkeys_state,
|
attested_passkeys_state,
|
||||||
passkeys_state,
|
passkeys_state,
|
||||||
|
|
|
@ -49,6 +49,9 @@
|
||||||
(% when CURegWarning::Unsatisfiable %)
|
(% when CURegWarning::Unsatisfiable %)
|
||||||
An account policy conflict has occurred and you will not be able
|
An account policy conflict has occurred and you will not be able
|
||||||
to save your credentials.
|
to save your credentials.
|
||||||
|
(% when CURegWarning::WebauthnUserVerificationRequired %)
|
||||||
|
The passkey you attempted to register did not provide user verification. Please
|
||||||
|
ensure that you have a PIN or alternative configured on your authenticator.
|
||||||
(% endmatch %)
|
(% endmatch %)
|
||||||
|
|
||||||
(% if is_danger %)
|
(% if is_danger %)
|
||||||
|
@ -158,7 +161,7 @@
|
||||||
type="submit"
|
type="submit"
|
||||||
hx-post="/ui/api/cu_commit"
|
hx-post="/ui/api/cu_commit"
|
||||||
hx-boost="false"
|
hx-boost="false"
|
||||||
(% if !warnings.is_empty() %)disabled(% endif
|
(% if !can_commit %)disabled(% endif
|
||||||
%)>Save Changes</button>
|
%)>Save Changes</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -203,6 +203,7 @@ impl CredentialUpdateSession {
|
||||||
// Vec of the issues with the current session so that UI's can highlight properly how to proceed.
|
// Vec of the issues with the current session so that UI's can highlight properly how to proceed.
|
||||||
fn can_commit(&self) -> (bool, Vec<CredentialUpdateSessionStatusWarnings>) {
|
fn can_commit(&self) -> (bool, Vec<CredentialUpdateSessionStatusWarnings>) {
|
||||||
let mut warnings = Vec::with_capacity(0);
|
let mut warnings = Vec::with_capacity(0);
|
||||||
|
let mut can_proceed = true;
|
||||||
|
|
||||||
let cred_type_min = self.resolved_account_policy.credential_policy();
|
let cred_type_min = self.resolved_account_policy.credential_policy();
|
||||||
|
|
||||||
|
@ -219,6 +220,7 @@ impl CredentialUpdateSession {
|
||||||
// parts.
|
// parts.
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
|
can_proceed = false;
|
||||||
warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
|
warnings.push(CredentialUpdateSessionStatusWarnings::MfaRequired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,12 +228,14 @@ impl CredentialUpdateSession {
|
||||||
// NOTE: Technically this is unreachable, but we keep it for correctness.
|
// NOTE: Technically this is unreachable, but we keep it for correctness.
|
||||||
// Primary can't be set at all.
|
// Primary can't be set at all.
|
||||||
if self.primary.is_some() {
|
if self.primary.is_some() {
|
||||||
|
can_proceed = false;
|
||||||
warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
|
warnings.push(CredentialUpdateSessionStatusWarnings::PasskeyRequired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CredentialType::AttestedPasskey => {
|
CredentialType::AttestedPasskey => {
|
||||||
// Also unreachable - during these sessions, there will be no values present here.
|
// Also unreachable - during these sessions, there will be no values present here.
|
||||||
if !self.passkeys.is_empty() || self.primary.is_some() {
|
if !self.passkeys.is_empty() || self.primary.is_some() {
|
||||||
|
can_proceed = false;
|
||||||
warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
|
warnings.push(CredentialUpdateSessionStatusWarnings::AttestedPasskeyRequired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,12 +245,14 @@ impl CredentialUpdateSession {
|
||||||
|| !self.passkeys.is_empty()
|
|| !self.passkeys.is_empty()
|
||||||
|| self.primary.is_some()
|
|| self.primary.is_some()
|
||||||
{
|
{
|
||||||
|
can_proceed = false;
|
||||||
warnings
|
warnings
|
||||||
.push(CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired);
|
.push(CredentialUpdateSessionStatusWarnings::AttestedResidentKeyRequired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CredentialType::Invalid => {
|
CredentialType::Invalid => {
|
||||||
// special case, must always deny all changes.
|
// special case, must always deny all changes.
|
||||||
|
can_proceed = false;
|
||||||
warnings.push(CredentialUpdateSessionStatusWarnings::Unsatisfiable)
|
warnings.push(CredentialUpdateSessionStatusWarnings::Unsatisfiable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,7 +264,7 @@ impl CredentialUpdateSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(warnings.is_empty(), warnings)
|
(can_proceed, warnings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,6 +302,7 @@ pub enum CredentialUpdateSessionStatusWarnings {
|
||||||
AttestedResidentKeyRequired,
|
AttestedResidentKeyRequired,
|
||||||
Unsatisfiable,
|
Unsatisfiable,
|
||||||
WebauthnAttestationUnsatisfiable,
|
WebauthnAttestationUnsatisfiable,
|
||||||
|
WebauthnUserVerificationRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for CredentialUpdateSessionStatusWarnings {
|
impl Display for CredentialUpdateSessionStatusWarnings {
|
||||||
|
@ -319,6 +326,9 @@ impl From<CredentialUpdateSessionStatusWarnings> for CURegWarning {
|
||||||
CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable => {
|
CredentialUpdateSessionStatusWarnings::WebauthnAttestationUnsatisfiable => {
|
||||||
CURegWarning::WebauthnAttestationUnsatisfiable
|
CURegWarning::WebauthnAttestationUnsatisfiable
|
||||||
}
|
}
|
||||||
|
CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired => {
|
||||||
|
CURegWarning::WebauthnUserVerificationRequired
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,6 +360,13 @@ pub struct CredentialUpdateSessionStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CredentialUpdateSessionStatus {
|
impl CredentialUpdateSessionStatus {
|
||||||
|
/// Append a single warning to this session status, which will only be displayed to the
|
||||||
|
/// user once. This is different to other warnings that are derived from the state of the
|
||||||
|
/// session as a whole.
|
||||||
|
pub fn append_ephemeral_warning(&mut self, warning: CredentialUpdateSessionStatusWarnings) {
|
||||||
|
self.warnings.push(warning)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn can_commit(&self) -> bool {
|
pub fn can_commit(&self) -> bool {
|
||||||
self.can_commit
|
self.can_commit
|
||||||
}
|
}
|
||||||
|
@ -2172,30 +2189,36 @@ impl IdmServerCredUpdateTransaction<'_> {
|
||||||
|
|
||||||
match &session.mfaregstate {
|
match &session.mfaregstate {
|
||||||
MfaRegState::Passkey(_ccr, pk_reg) => {
|
MfaRegState::Passkey(_ccr, pk_reg) => {
|
||||||
let result = self
|
let reg_result = self.webauthn.finish_passkey_registration(reg, pk_reg);
|
||||||
.webauthn
|
|
||||||
.finish_passkey_registration(reg, pk_reg)
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(eclass=?e, emsg=%e, "Unable to complete passkey registration");
|
|
||||||
match e {
|
|
||||||
WebauthnError::UserNotVerified => {
|
|
||||||
OperationError::CU0003WebauthnUserNotVerified
|
|
||||||
}
|
|
||||||
_ => OperationError::CU0002WebauthnRegistrationError,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The reg is done. Clean up state before returning errors.
|
// Clean up state before returning results.
|
||||||
session.mfaregstate = MfaRegState::None;
|
session.mfaregstate = MfaRegState::None;
|
||||||
|
|
||||||
let passkey = result?;
|
match reg_result {
|
||||||
|
Ok(passkey) => {
|
||||||
|
let pk_id = Uuid::new_v4();
|
||||||
|
session.passkeys.insert(pk_id, (label, passkey));
|
||||||
|
|
||||||
let pk_id = Uuid::new_v4();
|
let cu_status: CredentialUpdateSessionStatus = session.deref().into();
|
||||||
session.passkeys.insert(pk_id, (label, passkey));
|
Ok(cu_status)
|
||||||
|
}
|
||||||
Ok(session.deref().into())
|
Err(WebauthnError::UserNotVerified) => {
|
||||||
|
let mut cu_status: CredentialUpdateSessionStatus = session.deref().into();
|
||||||
|
cu_status.append_ephemeral_warning(
|
||||||
|
CredentialUpdateSessionStatusWarnings::WebauthnUserVerificationRequired,
|
||||||
|
);
|
||||||
|
Ok(cu_status)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(eclass=?err, emsg=%err, "Unable to complete passkey registration");
|
||||||
|
Err(OperationError::CU0002WebauthnRegistrationError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalid_state => {
|
||||||
|
warn!(?invalid_state);
|
||||||
|
Err(OperationError::InvalidRequestState)
|
||||||
}
|
}
|
||||||
_ => Err(OperationError::InvalidRequestState),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,9 @@ pub(crate) fn handle_group_account_policy_error(response: ClientError, _output_m
|
||||||
use kanidm_proto::internal::OperationError::SchemaViolation;
|
use kanidm_proto::internal::OperationError::SchemaViolation;
|
||||||
use kanidm_proto::internal::SchemaError::AttributeNotValidForClass;
|
use kanidm_proto::internal::SchemaError::AttributeNotValidForClass;
|
||||||
|
|
||||||
if let ClientError::Http(_status, Some(SchemaViolation(AttributeNotValidForClass(att))), opid) = response {
|
if let ClientError::Http(_status, Some(SchemaViolation(AttributeNotValidForClass(att))), opid) =
|
||||||
|
response
|
||||||
|
{
|
||||||
error!("OperationId: {:?}", opid);
|
error!("OperationId: {:?}", opid);
|
||||||
error!("Cannot update account-policy attribute {att}. Is account-policy enabled on this group?");
|
error!("Cannot update account-policy attribute {att}. Is account-policy enabled on this group?");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1282,6 +1282,11 @@ fn display_warnings(warnings: &[CURegWarning]) {
|
||||||
CURegWarning::Unsatisfiable => {
|
CURegWarning::Unsatisfiable => {
|
||||||
println!("Account policy is unsatisfiable. Contact your administrator.");
|
println!("Account policy is unsatisfiable. Contact your administrator.");
|
||||||
}
|
}
|
||||||
|
CURegWarning::WebauthnUserVerificationRequired => {
|
||||||
|
println!(
|
||||||
|
"The passkey you attempted to register did not provide user verification, please ensure a PIN or equivalent is set."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue