use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
enum WebAuthnErrorKind {
InvalidConfiguration(String),
ChallengeMismatch,
AttestationFailed(String),
OriginMismatch,
RpIdMismatch,
CredentialNotFound,
SignCountRollback,
UserVerificationFailed,
Internal(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[doc(alias = "webauthn_error")]
pub struct WebAuthnError {
kind: WebAuthnErrorKind,
}
impl WebAuthnError {
const fn new(kind: WebAuthnErrorKind) -> Self {
Self { kind }
}
pub fn invalid_configuration(detail: impl Into<String>) -> Self {
Self::new(WebAuthnErrorKind::InvalidConfiguration(detail.into()))
}
#[must_use]
pub fn challenge_mismatch() -> Self {
Self::new(WebAuthnErrorKind::ChallengeMismatch)
}
pub fn attestation_failed(detail: impl Into<String>) -> Self {
Self::new(WebAuthnErrorKind::AttestationFailed(detail.into()))
}
#[must_use]
pub fn origin_mismatch() -> Self {
Self::new(WebAuthnErrorKind::OriginMismatch)
}
#[must_use]
pub fn rp_id_mismatch() -> Self {
Self::new(WebAuthnErrorKind::RpIdMismatch)
}
#[must_use]
pub fn credential_not_found() -> Self {
Self::new(WebAuthnErrorKind::CredentialNotFound)
}
#[must_use]
pub fn sign_count_rollback() -> Self {
Self::new(WebAuthnErrorKind::SignCountRollback)
}
#[must_use]
pub fn user_verification_failed() -> Self {
Self::new(WebAuthnErrorKind::UserVerificationFailed)
}
pub fn internal(detail: impl Into<String>) -> Self {
Self::new(WebAuthnErrorKind::Internal(detail.into()))
}
}
impl WebAuthnError {
#[must_use]
#[inline]
pub fn is_invalid_configuration(&self) -> bool {
matches!(self.kind, WebAuthnErrorKind::InvalidConfiguration(_))
}
#[must_use]
#[inline]
pub fn is_challenge_mismatch(&self) -> bool {
self.kind == WebAuthnErrorKind::ChallengeMismatch
}
#[must_use]
#[inline]
pub fn is_attestation_failed(&self) -> bool {
matches!(self.kind, WebAuthnErrorKind::AttestationFailed(_))
}
#[must_use]
#[inline]
pub fn is_origin_mismatch(&self) -> bool {
self.kind == WebAuthnErrorKind::OriginMismatch
}
#[must_use]
#[inline]
pub fn is_rp_id_mismatch(&self) -> bool {
self.kind == WebAuthnErrorKind::RpIdMismatch
}
#[must_use]
#[inline]
pub fn is_credential_not_found(&self) -> bool {
self.kind == WebAuthnErrorKind::CredentialNotFound
}
#[must_use]
#[inline]
pub fn is_sign_count_rollback(&self) -> bool {
self.kind == WebAuthnErrorKind::SignCountRollback
}
#[must_use]
#[inline]
pub fn is_user_verification_failed(&self) -> bool {
self.kind == WebAuthnErrorKind::UserVerificationFailed
}
#[must_use]
#[inline]
pub fn is_internal(&self) -> bool {
matches!(self.kind, WebAuthnErrorKind::Internal(_))
}
#[must_use]
#[inline]
pub fn detail(&self) -> Option<&str> {
match &self.kind {
WebAuthnErrorKind::InvalidConfiguration(d)
| WebAuthnErrorKind::AttestationFailed(d)
| WebAuthnErrorKind::Internal(d) => Some(d),
WebAuthnErrorKind::ChallengeMismatch
| WebAuthnErrorKind::OriginMismatch
| WebAuthnErrorKind::RpIdMismatch
| WebAuthnErrorKind::CredentialNotFound
| WebAuthnErrorKind::SignCountRollback
| WebAuthnErrorKind::UserVerificationFailed => None,
}
}
}
impl fmt::Display for WebAuthnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
WebAuthnErrorKind::InvalidConfiguration(detail) => {
f.write_str("webauthn: invalid configuration: ")?;
f.write_str(detail)
}
WebAuthnErrorKind::ChallengeMismatch => {
f.write_str("webauthn: challenge state does not match response")
}
WebAuthnErrorKind::AttestationFailed(detail) => {
f.write_str("webauthn: attestation failed: ")?;
f.write_str(detail)
}
WebAuthnErrorKind::OriginMismatch => {
f.write_str("webauthn: origin not in relying-party allowlist")
}
WebAuthnErrorKind::RpIdMismatch => f.write_str("webauthn: rp-id hash mismatch"),
WebAuthnErrorKind::CredentialNotFound => f.write_str("webauthn: credential not found"),
WebAuthnErrorKind::SignCountRollback => {
f.write_str("webauthn: sign-count rollback rejected")
}
WebAuthnErrorKind::UserVerificationFailed => {
f.write_str("webauthn: user-verification required but not asserted")
}
WebAuthnErrorKind::Internal(detail) => {
f.write_str("webauthn: internal error: ")?;
f.write_str(detail)
}
}
}
}
impl std::error::Error for WebAuthnError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_configuration_display_and_query() {
let err = WebAuthnError::invalid_configuration("missing rp_id");
assert_eq!(
err.to_string(),
"webauthn: invalid configuration: missing rp_id"
);
assert!(err.is_invalid_configuration());
assert_eq!(err.detail(), Some("missing rp_id"));
}
#[test]
fn challenge_mismatch_display_and_query() {
let err = WebAuthnError::challenge_mismatch();
assert_eq!(
err.to_string(),
"webauthn: challenge state does not match response"
);
assert!(err.is_challenge_mismatch());
assert_eq!(err.detail(), None);
}
#[test]
fn attestation_failed_display_and_query() {
let err = WebAuthnError::attestation_failed("bad signature");
assert_eq!(
err.to_string(),
"webauthn: attestation failed: bad signature"
);
assert!(err.is_attestation_failed());
assert_eq!(err.detail(), Some("bad signature"));
}
#[test]
fn origin_mismatch_display_and_query() {
let err = WebAuthnError::origin_mismatch();
assert_eq!(
err.to_string(),
"webauthn: origin not in relying-party allowlist"
);
assert!(err.is_origin_mismatch());
assert_eq!(err.detail(), None);
}
#[test]
fn rp_id_mismatch_display_and_query() {
let err = WebAuthnError::rp_id_mismatch();
assert_eq!(err.to_string(), "webauthn: rp-id hash mismatch");
assert!(err.is_rp_id_mismatch());
assert_eq!(err.detail(), None);
}
#[test]
fn credential_not_found_display_and_query() {
let err = WebAuthnError::credential_not_found();
assert_eq!(err.to_string(), "webauthn: credential not found");
assert!(err.is_credential_not_found());
assert_eq!(err.detail(), None);
}
#[test]
fn sign_count_rollback_display_and_query() {
let err = WebAuthnError::sign_count_rollback();
assert_eq!(err.to_string(), "webauthn: sign-count rollback rejected");
assert!(err.is_sign_count_rollback());
assert_eq!(err.detail(), None);
}
#[test]
fn user_verification_failed_display_and_query() {
let err = WebAuthnError::user_verification_failed();
assert_eq!(
err.to_string(),
"webauthn: user-verification required but not asserted"
);
assert!(err.is_user_verification_failed());
assert_eq!(err.detail(), None);
}
#[test]
fn internal_display_and_query() {
let err = WebAuthnError::internal("upstream parsed a nan");
assert_eq!(
err.to_string(),
"webauthn: internal error: upstream parsed a nan"
);
assert!(err.is_internal());
assert_eq!(err.detail(), Some("upstream parsed a nan"));
}
#[test]
fn error_implements_std_error() {
let errors: Vec<Box<dyn std::error::Error>> = vec![
Box::new(WebAuthnError::invalid_configuration("x")),
Box::new(WebAuthnError::challenge_mismatch()),
Box::new(WebAuthnError::attestation_failed("x")),
Box::new(WebAuthnError::origin_mismatch()),
Box::new(WebAuthnError::rp_id_mismatch()),
Box::new(WebAuthnError::credential_not_found()),
Box::new(WebAuthnError::sign_count_rollback()),
Box::new(WebAuthnError::user_verification_failed()),
Box::new(WebAuthnError::internal("x")),
];
for err in &errors {
assert!(err.source().is_none(), "source() should be None for: {err}");
let _ = err.to_string();
}
}
#[test]
fn query_methods_are_exclusive() {
let cases: Vec<(WebAuthnError, &str)> = vec![
(
WebAuthnError::invalid_configuration("c"),
"invalid_configuration",
),
(WebAuthnError::challenge_mismatch(), "challenge_mismatch"),
(WebAuthnError::attestation_failed("c"), "attestation_failed"),
(WebAuthnError::origin_mismatch(), "origin_mismatch"),
(WebAuthnError::rp_id_mismatch(), "rp_id_mismatch"),
(
WebAuthnError::credential_not_found(),
"credential_not_found",
),
(WebAuthnError::sign_count_rollback(), "sign_count_rollback"),
(
WebAuthnError::user_verification_failed(),
"user_verification_failed",
),
(WebAuthnError::internal("c"), "internal"),
];
for (err, expected) in &cases {
let hits: Vec<&str> = [
err.is_invalid_configuration()
.then_some("invalid_configuration"),
err.is_challenge_mismatch().then_some("challenge_mismatch"),
err.is_attestation_failed().then_some("attestation_failed"),
err.is_origin_mismatch().then_some("origin_mismatch"),
err.is_rp_id_mismatch().then_some("rp_id_mismatch"),
err.is_credential_not_found()
.then_some("credential_not_found"),
err.is_sign_count_rollback()
.then_some("sign_count_rollback"),
err.is_user_verification_failed()
.then_some("user_verification_failed"),
err.is_internal().then_some("internal"),
]
.into_iter()
.flatten()
.collect();
assert_eq!(
hits,
vec![*expected],
"expected exactly one query match for {expected}, got {hits:?}",
);
}
}
#[test]
fn partial_eq_distinguishes_payload_strings() {
assert_eq!(
WebAuthnError::attestation_failed("a"),
WebAuthnError::attestation_failed("a")
);
assert_ne!(
WebAuthnError::attestation_failed("a"),
WebAuthnError::attestation_failed("b")
);
assert_ne!(
WebAuthnError::origin_mismatch(),
WebAuthnError::rp_id_mismatch()
);
}
}