commit 2acae1c69f2fddd68a09e6bdf5b7a9b349f09c39
parent 18f2066416892be8936c6677fb0f8dcd35213b1f
Author: Zack Newman <zack@philomathiclife.com>
Date: Sat, 29 Mar 2025 22:15:21 -0600
generalize authentication further based on user
Diffstat:
14 files changed, 518 insertions(+), 342 deletions(-)
diff --git a/README.md b/README.md
@@ -40,9 +40,8 @@ cannot be constructed when [`bin`](#bin) or [`serde`](#serde) is not enabled.
### `serde`
-For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the
-JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using
-this feature.
+This feature _strictly_ adheres to the JSON-motivated definitions. You _will_ encounter clients that send data that
+cannot be deserialized using this feature. For many [`serde_relaxed`](#serde_relaxed) should be used instead.
Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/)
based on the JSON-motivated definitions (e.g.,
diff --git a/src/lib.rs b/src/lib.rs
@@ -40,9 +40,9 @@
//!
//! ### `serde`
//!
-//! For many [`serde_relaxed`](#serde_relaxed) should be used instead. This feature _strictly_ adheres to the
-//! JSON-motivated definitions. You _will_ encounter clients that send data that cannot be deserialized using
-//! this feature.
+//! This feature _strictly_ adheres to the JSON-motivated definitions. You _will_ encounter clients that send data
+//! that cannot be deserialized using this feature. For many [`serde_relaxed`](#serde_relaxed) should be used
+//! instead.
//!
//! Enables (de)serialization of data sent to/from the client via [`serde`](https://docs.rs/serde/latest/serde/)
//! based on the JSON-motivated definitions (e.g.,
@@ -371,7 +371,12 @@ pub use crate::{
PublicKeyCredentialCreationOptions, RegistrationClientState, RegistrationServerState,
},
},
- response::{auth::Authentication, register::Registration},
+ response::{
+ auth::{
+ Authentication, PasskeyAuthentication, PasskeyAuthentication16, PasskeyAuthentication64,
+ },
+ register::Registration,
+ },
};
/// Error returned in [`RegCeremonyErr::Credential`] and [`AuthCeremonyErr::Credential`] as well as
/// from [`AuthenticatedCredential::new`].
diff --git a/src/request.rs b/src/request.rs
@@ -197,7 +197,7 @@ impl Challenge {
/// The number of bytes a `Challenge` takes to encode in base64url.
#[expect(clippy::unwrap_used, reason = "we want to crash when there is a bug")]
pub(super) const BASE64_LEN: usize = super::base64url_nopad_len(16).unwrap();
- /// Returns a new `Challenge` based on a randomly-generated `u128`.
+ /// Generates a random `Challenge`.
///
/// # Examples
///
@@ -1634,16 +1634,18 @@ impl BuildHasher for BuildIdentityHasher {
IdentityHasher(0)
}
}
-/// Prevent users from implementing [`ServerState`].
+/// Prevent users from implementing [`ServerState`] and [`super::register::User`].
mod private {
- /// Marker trait used as a supertrait of `ServerState`.
+ /// Marker trait used as a supertrait of `ServerState` and `User`.
pub trait Sealed {}
impl Sealed for super::AuthenticationServerState {}
impl Sealed for super::RegistrationServerState {}
+ impl<T> Sealed for super::register::UserHandle<T> {}
+ impl<T> Sealed for Option<super::register::UserHandle<T>> {}
}
/// Subset of data shared by both [`RegistrationServerState`] and [`AuthenticationServerState`].
///
-/// This trait is sealed and cannot be implemented for types outside of `webauthn_rp`.
+/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`.
pub trait ServerState: private::Sealed {
/// Returns the `Instant` the ceremony expires.
///
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -11,6 +11,7 @@ use super::{
use super::{
super::{
AuthenticatedCredential,
+ request::register::User,
response::{
AuthenticatorAttachment,
auth::{
@@ -873,7 +874,7 @@ impl<O, T> Default for AuthenticationVerificationOptions<'_, '_, O, T> {
/// [`Self::backup_requirement`] is `None`, [`Self::error_on_unsolicited_extensions`] is `true`,
/// [`Self::auth_attachment_enforcement`] is [`AuthenticatorAttachmentEnforcement::default`],
/// [`Self::update_uv`] is `false`, [`Self::sig_counter_enforcement`] is
- /// [`SignatureCounterEnforcement::Fail`], and [`Self::client_data_json_relaxed`] is `true`.
+ /// [`SignatureCounterEnforcement::default`], and [`Self::client_data_json_relaxed`] is `true`.
#[inline]
fn default() -> Self {
Self {
@@ -956,11 +957,11 @@ impl AuthenticationServerState {
P256Key: AsRef<[u8]>,
P384Key: AsRef<[u8]>,
RsaKey: AsRef<[u8]>,
- User: AsRef<[u8]>,
+ U: User,
>(
self,
rp_id: &RpId,
- response: &'a Authentication<User>,
+ response: &'a Authentication<U>,
cred: &mut AuthenticatedCredential<
'a,
'user,
@@ -1002,10 +1003,10 @@ impl AuthenticationServerState {
response
.response
.user_handle()
- .as_ref()
+ .is_same(cred.user_id)
.ok_or(AuthCeremonyErr::MissingUserHandle)
- .and_then(|user| {
- if cred.user_id() == user {
+ .and_then(|same| {
+ if same {
if cred.id == response.raw_id {
Ok(None)
} else {
@@ -1098,17 +1099,17 @@ impl AuthenticationServerState {
/// Errors iff [`AuthenticatedCredential::user_handle`] does not match [`Authentication::user_handle`] or
/// [`PublicKeyCredentialRequestOptions::allow_credentials`] does not have a [`CredInfo`] such that
/// [`CredInfo::id`] matches [`Authentication::raw_id`].
- fn verify_nondiscoverable<'a, PublicKey, User: AsRef<[u8]>>(
+ fn verify_nondiscoverable<'a, PublicKey, U: User>(
&self,
- response: &'a Authentication<User>,
+ response: &'a Authentication<U>,
cred: &AuthenticatedCredential<'a, '_, PublicKey>,
) -> Result<Option<ServerCredSpecificExtensionInfo>, AuthCeremonyErr> {
response
.response
.user_handle()
- .as_ref()
- .map_or(Ok(()), |user| {
- if user == cred.user_id() {
+ .is_same(cred.user_id())
+ .map_or(Ok(()), |same| {
+ if same {
Ok(())
} else {
Err(AuthCeremonyErr::UserHandleMismatch)
diff --git a/src/request/register.rs b/src/request/register.rs
@@ -19,7 +19,10 @@ use super::{
};
#[cfg(doc)]
use crate::{
- request::{AsciiDomain, DomainOrigin, Url, auth::PublicKeyCredentialRequestOptions},
+ request::{
+ AsciiDomain, DomainOrigin, Url, auth::AuthenticationServerState,
+ auth::PublicKeyCredentialRequestOptions,
+ },
response::{AuthTransports, AuthenticatorTransport, Backup, CollectedClientData},
};
use alloc::borrow::Cow;
@@ -228,29 +231,30 @@ impl<'a: 'b, 'b> From<Nickname<'a>> for Cow<'b, str> {
value.0
}
}
-impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> {
+impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Nickname<'b> {
type Error = NicknameErr;
/// # Examples
///
/// ```
+ /// # use std::borrow::Cow;
/// # use webauthn_rp::request::register::{error::NicknameErr, Nickname};
/// assert_eq!(
- /// Nickname::try_from("Srinivasa Ramanujan")?.as_ref(),
+ /// Nickname::try_from(Cow::Borrowed("Srinivasa Ramanujan"))?.as_ref(),
/// "Srinivasa Ramanujan"
/// );
/// assert_eq!(
- /// Nickname::try_from("श्रीनिवास रामानुजन्")?.as_ref(),
+ /// Nickname::try_from(Cow::Borrowed("श्रीनिवास रामानुजन्"))?.as_ref(),
/// "श्रीनिवास रामानुजन्"
/// );
/// // Empty strings are not valid.
- /// assert!(Nickname::try_from("").map_or_else(
+ /// assert!(Nickname::try_from(Cow::Borrowed("")).map_or_else(
/// |e| matches!(e, NicknameErr::Rfc8266),
/// |_| false
/// ));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
- fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
precis_profiles::Nickname::new()
.enforce(value)
.map_err(|_e| NicknameErr::Rfc8266)
@@ -263,6 +267,22 @@ impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> {
})
}
}
+impl<'a: 'b, 'b> TryFrom<&'a str> for Nickname<'b> {
+ type Error = NicknameErr;
+ /// Same as [`Nickname::try_from`] except the input is a `str`.
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ Self::try_from(Cow::Borrowed(value))
+ }
+}
+impl TryFrom<String> for Nickname<'_> {
+ type Error = NicknameErr;
+ /// Same as [`Nickname::try_from`] except the input is a `String`.
+ #[inline]
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::try_from(Cow::Owned(value))
+ }
+}
/// String returned from the
/// [UsernameCasePreserved Enforcement rule](https://www.rfc-editor.org/rfc/rfc8265#section-3.4.3) as defined in
/// RFC 8265.
@@ -294,25 +314,26 @@ impl Borrow<str> for Username<'_> {
self.0.as_ref()
}
}
-impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> {
+impl<'a: 'b, 'b> TryFrom<Cow<'a, str>> for Username<'b> {
type Error = UsernameErr;
/// # Examples
///
/// ```
+ /// # use std::borrow::Cow;
/// # use webauthn_rp::request::register::{error::UsernameErr, Username};
/// assert_eq!(
- /// Username::try_from("leonhard.euler")?.as_ref(),
+ /// Username::try_from(Cow::Borrowed("leonhard.euler"))?.as_ref(),
/// "leonhard.euler"
/// );
/// // Empty strings are not valid.
- /// assert!(Username::try_from("").map_or_else(
+ /// assert!(Username::try_from(Cow::Borrowed("")).map_or_else(
/// |e| matches!(e, UsernameErr::Rfc8265),
/// |_| false
/// ));
/// # Ok::<_, webauthn_rp::AggErr>(())
/// ```
#[inline]
- fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
UsernameCasePreserved::default()
.enforce(value)
.map_err(|_e| UsernameErr::Rfc8265)
@@ -325,6 +346,22 @@ impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> {
})
}
}
+impl<'a: 'b, 'b> TryFrom<&'a str> for Username<'b> {
+ type Error = UsernameErr;
+ /// Same as [`Username::try_from`] except the input is a `str`.
+ #[inline]
+ fn try_from(value: &'a str) -> Result<Self, Self::Error> {
+ Self::try_from(Cow::Borrowed(value))
+ }
+}
+impl TryFrom<String> for Username<'_> {
+ type Error = UsernameErr;
+ /// Same as [`Username::try_from`] except the input is a `String`.
+ #[inline]
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::try_from(Cow::Owned(value))
+ }
+}
impl<'a: 'b, 'b> From<Username<'a>> for Cow<'b, str> {
#[inline]
fn from(value: Username<'a>) -> Self {
@@ -783,7 +820,8 @@ impl UserHandle<Vec<u8>> {
#[inline]
#[must_use]
pub fn new() -> Self {
- Self::rand(64).unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand"))
+ Self::rand(USER_HANDLE_MAX_LEN)
+ .unwrap_or_else(|_e| unreachable!("there is a bug in UserHandle::rand"))
}
}
/// Implements [`Default`] for [`UserHandle`] of array of length of the passed `usize` literal.
@@ -923,6 +961,45 @@ impl<T: Hash> Hash for UserHandle<T> {
self.0.hash(state);
}
}
+/// `UserHandle` that is based on the [spec recommendation](https://www.w3.org/TR/webauthn-3/#user-handle).
+pub type UserHandle64 = UserHandle<[u8; USER_HANDLE_MAX_LEN]>;
+/// `UserHandle` that is based on 16 bytes.
+///
+/// While not the recommended size like [`UserHandle64`], 16 bytes is common for many deployments since
+/// it's the same size as [Universally Unique IDentifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562).
+pub type UserHandle16 = UserHandle<[u8; 16]>;
+/// Unifies `Option<UserHandle<T>>` and [`UserHandle`] such that both can be used for
+/// [`AuthenticationServerState::verify`].
+///
+/// This `trait` is sealed and cannot be implemented for types outside of `webauthn_rp`.
+pub trait User: super::private::Sealed {
+ /// Returns `true` iff [`UserHandle`] must exist.
+ fn must_exist() -> bool;
+ /// Returns `None` iff `self` cannot be compared to `other`.
+ /// Returns `Some(true)` iff `self` is equivalent to `other`.
+ /// Returns `Some(false)` iff `self` is not equivalent to `other`.
+ fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool>;
+}
+impl<T: AsRef<[u8]>> User for UserHandle<T> {
+ #[inline]
+ fn must_exist() -> bool {
+ true
+ }
+ #[inline]
+ fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> {
+ Some(self.as_slice() == other)
+ }
+}
+impl<T: AsRef<[u8]>> User for Option<UserHandle<T>> {
+ #[inline]
+ fn must_exist() -> bool {
+ false
+ }
+ #[inline]
+ fn is_same(&self, other: UserHandle<&[u8]>) -> Option<bool> {
+ self.as_ref().map(|val| val.as_slice() == other)
+ }
+}
/// [The `PublicKeyCredentialUserEntity`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity)
/// sent to the client.
#[derive(Clone, Debug)]
diff --git a/src/request/register/ser.rs b/src/request/register/ser.rs
@@ -961,7 +961,7 @@ where
D: Deserializer<'de>,
{
/// `Visitor` for `PublicKeyCredentialUserEntity`.
- #[expect(clippy::type_complexity, reason = "type alias doesn't fit well with lifetimes")]
+ #[expect(clippy::type_complexity, reason = "type aliases with bounds are even more problematic at least until lazy_type_alias is stable")]
struct PublicKeyCredentialUserEntityVisitor<'a, 'b, U>(PhantomData<fn() -> (&'a (), &'b (), U)>);
impl<'d: 'a + 'b, 'a, 'b, U> Visitor<'d> for PublicKeyCredentialUserEntityVisitor<'a, 'b, U>
where
@@ -994,7 +994,7 @@ where
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{NAME}', '{ID}', '{DISPLAY_NAME}'")
+ write!(formatter, "'{NAME}', '{ID}', or '{DISPLAY_NAME}'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
diff --git a/src/response.rs b/src/response.rs
@@ -35,7 +35,7 @@ use ser_relaxed::SerdeJsonErr;
/// # use webauthn_rp::{
/// # request::{auth::{error::RequestOptionsErr, AuthenticationClientState, PublicKeyCredentialRequestOptions, AuthenticationVerificationOptions}, error::AsciiDomainErr, register::{UserHandle, USER_HANDLE_MAX_LEN}, AsciiDomain, BackupReq, RpId},
/// # response::{auth::{error::AuthCeremonyErr, Authentication}, error::CollectedClientDataErr, register::{AuthenticatorExtensionOutputStaticState, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, CompressedPubKey, StaticState}, AuthenticatorAttachment, Backup, CollectedClientData, CredentialId},
-/// # AuthenticatedCredential, CredentialErr
+/// # AuthenticatedCredential, CredentialErr, PasskeyAuthentication64
/// # };
/// # #[derive(Debug)]
/// # enum E {
@@ -89,14 +89,13 @@ use ser_relaxed::SerdeJsonErr;
/// InsertResult::Success
/// ));
/// # #[cfg(feature = "serde")]
-/// let authentication = serde_json::from_str::<Authentication<[u8; USER_HANDLE_MAX_LEN]>>(get_authentication_json(client).as_str())?;
-/// // `UserHandle` must exist since we sent an empty `AllowedCredentials`.
+/// let authentication = serde_json::from_str::<PasskeyAuthentication64>(get_authentication_json(client).as_str())?;
/// # #[cfg(feature = "serde")]
-/// let user_handle = authentication.response().user_handle().ok_or(E::MissingUserHandle)?;
+/// let user_handle = authentication.response().user_handle();
/// # #[cfg(feature = "serde")]
-/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle).ok_or(E::UnknownCredential)?;
+/// let (static_state, dynamic_state) = get_credential(authentication.raw_id(), user_handle.into()).ok_or(E::UnknownCredential)?;
/// # #[cfg(all(feature = "custom", feature = "serde"))]
-/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), user_handle, static_state, dynamic_state)?;
+/// let mut cred = AuthenticatedCredential::new(authentication.raw_id(), user_handle.into(), static_state, dynamic_state)?;
/// # #[cfg(all(not(feature = "serializable_server_state"), feature = "custom", feature = "serde"))]
/// if ceremonies.take(&authentication.challenge()?).ok_or(E::MissingCeremony)?.verify(&rp_id, &authentication, &mut cred, &AuthenticationVerificationOptions::<&str, &str>::default())? {
/// update_cred(authentication.raw_id(), cred.dynamic_state());
diff --git a/src/response/auth.rs b/src/response/auth.rs
@@ -1,6 +1,9 @@
#[cfg(feature = "serde_relaxed")]
use self::{
- super::ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
+ super::{
+ super::request::register::User,
+ ser_relaxed::{RelaxedClientDataJsonParser, SerdeJsonErr},
+ },
ser_relaxed::{AuthenticationRelaxed, CustomAuthentication},
};
#[cfg(all(doc, feature = "serde_relaxed"))]
@@ -14,7 +17,10 @@ use super::super::{
},
};
use super::{
- super::UserHandle,
+ super::{
+ UserHandle,
+ request::register::{UserHandle16, UserHandle64},
+ },
AuthData, AuthDataContainer, AuthExtOutput, AuthRespErr, AuthResponse, AuthenticatorAttachment,
CborSuccess, ClientDataJsonParser as _, CollectedClientData, CredentialId, Flag, FromCbor,
LimitedVerificationParser, ParsedAuthData, Response, SentChallenge,
@@ -278,7 +284,7 @@ impl<'a: 'b, 'b> TryFrom<&'a [u8]> for AuthenticatorData<'b> {
}
/// [`AuthenticatorAssertionResponse`](https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse).
#[derive(Debug)]
-pub struct AuthenticatorAssertion<User> {
+pub struct AuthenticatorAssertion<U> {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
client_data_json: Vec<u8>,
/// [`authenticatorData`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata)
@@ -287,9 +293,9 @@ pub struct AuthenticatorAssertion<User> {
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature).
signature: Vec<u8>,
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
- user_handle: Option<UserHandle<User>>,
+ user_handle: U,
}
-impl<User> AuthenticatorAssertion<User> {
+impl<U> AuthenticatorAssertion<U> {
/// [`clientDataJSON`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson).
#[inline]
#[must_use]
@@ -317,6 +323,12 @@ impl<User> AuthenticatorAssertion<User> {
pub fn signature(&self) -> &[u8] {
self.signature.as_slice()
}
+ /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
+ #[inline]
+ #[must_use]
+ pub const fn user_handle(&self) -> &U {
+ &self.user_handle
+ }
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
@@ -325,7 +337,7 @@ impl<User> AuthenticatorAssertion<User> {
client_data_json: Vec<u8>,
mut authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: Option<UserHandle<User>>,
+ user_handle: U,
) -> Self {
authenticator_data
.extend_from_slice(Sha256::digest(client_data_json.as_slice()).as_slice());
@@ -337,39 +349,41 @@ impl<User> AuthenticatorAssertion<User> {
}
}
}
-impl<User: AsRef<[u8]>> AuthenticatorAssertion<User> {
- /// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle).
- #[inline]
- #[must_use]
- pub fn user_handle(&self) -> Option<UserHandle<&[u8]>> {
- self.user_handle.as_ref().map(UserHandle::as_slice)
- }
-}
-impl AuthenticatorAssertion<Vec<u8>> {
+impl<T> AuthenticatorAssertion<Option<UserHandle<T>>> {
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
/// of available capacity; if not, a reallocation will occur.
#[inline]
#[must_use]
- pub fn new_user_vec(
+ pub fn with_optional_user(
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: Option<UserHandle<Vec<u8>>>,
+ user_handle: Option<UserHandle<T>>,
) -> Self {
Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
}
- /// Same as [`Self::new_user_vec`] except `user_handle` is required.
+ /// Same as [`Self::with_optional_user`] with `None` used for `user_handle`.
+ #[inline]
+ #[must_use]
+ pub fn without_user(
+ client_data_json: Vec<u8>,
+ authenticator_data: Vec<u8>,
+ signature: Vec<u8>,
+ ) -> Self {
+ Self::with_optional_user(client_data_json, authenticator_data, signature, None)
+ }
+ /// Same as [`Self::with_optional_user`] with `Some(user_handle)` used for `user_handle`.
#[inline]
#[must_use]
- pub fn with_user_vec(
+ pub fn with_user(
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: UserHandle<Vec<u8>>,
+ user_handle: UserHandle<T>,
) -> Self {
- Self::new_inner(
+ Self::with_optional_user(
client_data_json,
authenticator_data,
signature,
@@ -377,7 +391,7 @@ impl AuthenticatorAssertion<Vec<u8>> {
)
}
}
-impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> {
+impl<T> AuthenticatorAssertion<UserHandle<T>> {
/// Constructs an instance of `Self` with the contained data.
///
/// Note calling code is encouraged to ensure `authenticator_data` has at least 32 bytes
@@ -388,28 +402,12 @@ impl<const USER_LEN: usize> AuthenticatorAssertion<[u8; USER_LEN]> {
client_data_json: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
- user_handle: Option<UserHandle<[u8; USER_LEN]>>,
+ user_handle: UserHandle<T>,
) -> Self {
Self::new_inner(client_data_json, authenticator_data, signature, user_handle)
}
- /// Same as [`Self::new`] except `user_handle` is required.
- #[inline]
- #[must_use]
- pub fn with_user(
- client_data_json: Vec<u8>,
- authenticator_data: Vec<u8>,
- signature: Vec<u8>,
- user_handle: UserHandle<[u8; USER_LEN]>,
- ) -> Self {
- Self::new_inner(
- client_data_json,
- authenticator_data,
- signature,
- Some(user_handle),
- )
- }
}
-impl<User> AuthResponse for AuthenticatorAssertion<User> {
+impl<U> AuthResponse for AuthenticatorAssertion<U> {
type Auth<'a>
= AuthenticatorData<'a>
where
@@ -449,7 +447,7 @@ impl<User> AuthResponse for AuthenticatorAssertion<User> {
.map_err(AuthRespErr::CollectedClientDataRelaxed)
}
if relaxed {
- get_client_collected_data::<'_, User>(self.client_data_json.as_slice())
+ get_client_collected_data::<'_, U>(self.client_data_json.as_slice())
} else {
CollectedClientData::from_client_data_json::<false>(self.client_data_json.as_slice())
.map_err(AuthRespErr::CollectedClientData)
@@ -517,6 +515,8 @@ impl<User> AuthResponse for AuthenticatorAssertion<User> {
})
}
}
+/// `AuthenticatorAssertion` with a required `UserHandle`.
+pub type PasskeyAuthenticatorAssertion<T> = AuthenticatorAssertion<UserHandle<T>>;
/// [`PublicKeyCredential`](https://www.w3.org/TR/webauthn-3/#iface-pkcredential) for authentication ceremonies.
#[expect(
clippy::field_scoped_visibility_modifiers,
@@ -531,7 +531,7 @@ pub struct Authentication<User> {
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
pub(crate) authenticator_attachment: AuthenticatorAttachment,
}
-impl<User> Authentication<User> {
+impl<U> Authentication<U> {
/// [`rawId`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid).
#[inline]
#[must_use]
@@ -541,7 +541,7 @@ impl<User> Authentication<User> {
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response).
#[inline]
#[must_use]
- pub const fn response(&self) -> &AuthenticatorAssertion<User> {
+ pub const fn response(&self) -> &AuthenticatorAssertion<U> {
&self.response
}
/// [`authenticatorAttachment`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment).
@@ -557,7 +557,7 @@ impl<User> Authentication<User> {
#[must_use]
pub const fn new(
raw_id: CredentialId<Vec<u8>>,
- response: AuthenticatorAssertion<User>,
+ response: AuthenticatorAssertion<U>,
authenticator_attachment: AuthenticatorAttachment,
) -> Self {
Self {
@@ -618,9 +618,9 @@ impl<User> Authentication<User> {
#[inline]
pub fn from_json_relaxed<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
where
- UserHandle<User>: Deserialize<'a>,
+ U: Deserialize<'a> + User + Default,
{
- serde_json::from_slice::<AuthenticationRelaxed<User>>(json).map(|val| val.0)
+ serde_json::from_slice::<AuthenticationRelaxed<U>>(json).map(|val| val.0)
}
/// Convenience function for [`CustomAuthentication::deserialize`].
///
@@ -632,14 +632,20 @@ impl<User> Authentication<User> {
#[inline]
pub fn from_json_custom<'a>(json: &'a [u8]) -> Result<Self, SerdeJsonErr>
where
- UserHandle<User>: Deserialize<'a>,
+ U: Deserialize<'a> + User + Default,
{
- serde_json::from_slice::<CustomAuthentication<User>>(json).map(|val| val.0)
+ serde_json::from_slice::<CustomAuthentication<U>>(json).map(|val| val.0)
}
}
-impl<User> Response for Authentication<User> {
- type Auth = AuthenticatorAssertion<User>;
+impl<U> Response for Authentication<U> {
+ type Auth = AuthenticatorAssertion<U>;
fn auth(&self) -> &Self::Auth {
&self.response
}
}
+/// `Authentication` with a required `UserHandle`.
+pub type PasskeyAuthentication<T> = Authentication<UserHandle<T>>;
+/// `Authentication` with a required `UserHandle64`.
+pub type PasskeyAuthentication64 = Authentication<UserHandle64>;
+/// `Authentication` with a required `UserHandle16`.
+pub type PasskeyAuthentication16 = Authentication<UserHandle16>;
diff --git a/src/response/auth/ser.rs b/src/response/auth/ser.rs
@@ -4,17 +4,20 @@
)]
use super::{
super::{
- super::response::ser::{Base64DecodedVal, PublicKeyCredential},
+ super::{
+ request::register::User,
+ response::ser::{Base64DecodedVal, PublicKeyCredential},
+ },
ser::{
AuthenticationExtensionsPrfOutputsHelper, AuthenticationExtensionsPrfValues,
ClientExtensions,
},
},
- Authentication, AuthenticatorAssertion, UserHandle,
+ Authentication, AuthenticatorAssertion,
error::UnknownCredentialOptions,
};
#[cfg(doc)]
-use super::{AuthenticatorAttachment, CredentialId};
+use super::{AuthenticatorAttachment, CredentialId, UserHandle};
use core::{
fmt::{self, Formatter},
marker::PhantomData,
@@ -82,23 +85,22 @@ impl<'e> Deserialize<'e> for AuthData {
///
/// Unknown fields are ignored and only `clientDataJSON`, `authenticatorData`, and `signature` are required iff
/// `RELAXED`.
-pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, User>(
- PhantomData<fn() -> User>,
-);
+pub(super) struct AuthenticatorAssertionVisitor<const RELAXED: bool, U>(PhantomData<fn() -> U>);
impl<const RELAXED: bool, USER> AuthenticatorAssertionVisitor<RELAXED, USER> {
/// Returns `Self`.
pub fn new() -> Self {
Self(PhantomData)
}
}
-impl<'d, const R: bool, User> Visitor<'d> for AuthenticatorAssertionVisitor<R, User>
+impl<'d, const R: bool, U> Visitor<'d> for AuthenticatorAssertionVisitor<R, U>
where
- UserHandle<User>: Deserialize<'d>,
+ U: Deserialize<'d> + User + Default,
{
- type Value = AuthenticatorAssertion<User>;
+ type Value = AuthenticatorAssertion<U>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("AuthenticatorAssertion")
}
+ #[expect(clippy::too_many_lines, reason = "107 lines is fine")]
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'d>,
@@ -200,13 +202,20 @@ where
auth.ok_or_else(|| Error::missing_field(AUTHENTICATOR_DATA))
.and_then(|authenticator_data| {
sig.ok_or_else(|| Error::missing_field(SIGNATURE))
- .map(|signature| {
- AuthenticatorAssertion::new_inner(
- client_data_json,
- authenticator_data,
- signature,
- user_handle.flatten(),
- )
+ .and_then(|signature| {
+ if U::must_exist() {
+ user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE))
+ } else {
+ user_handle.map_or_else(|| Ok(U::default()), Ok)
+ }
+ .map(|user| {
+ AuthenticatorAssertion::new_inner(
+ client_data_json,
+ authenticator_data,
+ signature,
+ user,
+ )
+ })
})
})
})
@@ -223,9 +232,9 @@ const USER_HANDLE: &str = "userHandle";
/// Fields in `AuthenticatorAssertionResponseJSON`.
pub(super) const AUTH_ASSERT_FIELDS: &[&str; 4] =
&[CLIENT_DATA_JSON, AUTHENTICATOR_DATA, SIGNATURE, USER_HANDLE];
-impl<'de, User> Deserialize<'de> for AuthenticatorAssertion<User>
+impl<'de, U> Deserialize<'de> for AuthenticatorAssertion<U>
where
- UserHandle<User>: Deserialize<'de>,
+ U: Deserialize<'de> + User + Default,
{
/// Deserializes a `struct` based on
/// [`AuthenticatorAssertionResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorassertionresponsejson).
@@ -237,8 +246,9 @@ where
/// [`signature`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-signature) are
/// base64url-decoded;
/// [`userHandle`](https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponsejson-userhandle) is
- /// `null` or deserialized via [`UserHandle::deserialize`]; and all `required` fields in the
- /// `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`).
+ /// based on `U`. The key is allowed to not exist or exist with the value `null` iff `U` is
+ /// `Option<UserHandle<_>>`; otherwise it is deserialized via [`UserHandle::deserialize`]. All `required`
+ /// fields in the `AuthenticatorAssertionResponseJSON` Web IDL `dictionary` exist (and are not `null`).
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -247,7 +257,7 @@ where
deserializer.deserialize_struct(
"AuthenticatorAssertion",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<false, User>::new(),
+ AuthenticatorAssertionVisitor::<false, U>::new(),
)
}
}
@@ -361,9 +371,9 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputs {
)
}
}
-impl<'de, User> Deserialize<'de> for Authentication<User>
+impl<'de, U> Deserialize<'de> for Authentication<U>
where
- UserHandle<User>: Deserialize<'de>,
+ U: Deserialize<'de> + User + Default,
{
/// Deserializes a `struct` based on
/// [`AuthenticationResponseJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson).
@@ -400,7 +410,7 @@ where
where
D: Deserializer<'de>,
{
- PublicKeyCredential::<false, false, AuthenticatorAssertion<User>, ClientExtensionsOutputs>::deserialize(
+ PublicKeyCredential::<false, false, AuthenticatorAssertion<U>, ClientExtensionsOutputs>::deserialize(
deserializer,
)
.map(|cred| Self {
@@ -453,7 +463,7 @@ impl Serialize for UnknownCredentialOptions<'_, '_> {
mod tests {
use super::super::{
super::super::request::register::USER_HANDLE_MIN_LEN, Authentication,
- AuthenticatorAttachment,
+ AuthenticatorAttachment, PasskeyAuthentication, UserHandle,
};
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
@@ -510,7 +520,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -550,7 +560,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -575,7 +585,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -597,11 +607,11 @@ mod tests {
err
);
// `null` `id`.
- err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -625,7 +635,7 @@ mod tests {
// missing `rawId`.
err = Error::missing_field("rawId").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -646,11 +656,11 @@ mod tests {
err
);
// `null` `rawId`.
- err = Error::invalid_type(Unexpected::Other("null"), &"rawId")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": null,
@@ -676,7 +686,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -701,7 +711,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -725,7 +735,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -750,7 +760,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -773,7 +783,7 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -792,7 +802,7 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<Authentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -812,7 +822,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -842,7 +852,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -869,7 +879,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -894,7 +904,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -918,7 +928,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -938,7 +948,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -959,7 +969,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -980,7 +990,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1008,7 +1018,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1032,7 +1042,7 @@ mod tests {
// Missing `type`.
err = Error::missing_field("type").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1053,11 +1063,11 @@ mod tests {
err
);
// `null` `type`.
- err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'")
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1083,7 +1093,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1109,7 +1119,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1120,7 +1130,7 @@ mod tests {
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1142,7 +1152,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1169,7 +1179,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1209,7 +1219,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1234,7 +1244,7 @@ mod tests {
// Duplicate field in `PublicKeyCredential`.
err = Error::duplicate_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1310,7 +1320,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1339,7 +1349,7 @@ mod tests {
);
// `null` `prf`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1364,7 +1374,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1390,7 +1400,7 @@ mod tests {
// Duplicate field.
err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1417,7 +1427,7 @@ mod tests {
);
// `null` `results`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1442,7 +1452,7 @@ mod tests {
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1472,7 +1482,7 @@ mod tests {
// Missing `first`.
err = Error::missing_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1499,7 +1509,7 @@ mod tests {
);
// `null` `first`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1525,7 +1535,7 @@ mod tests {
);
// `null` `second`.
assert!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1555,7 +1565,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1587,7 +1597,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1620,7 +1630,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1651,7 +1661,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1682,7 +1692,7 @@ mod tests {
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<Authentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/auth/ser_relaxed.rs b/src/response/auth/ser_relaxed.rs
@@ -6,6 +6,7 @@
use super::super::{Challenge, CredentialId};
use super::{
super::{
+ super::request::register::{User, UserHandle16, UserHandle64},
auth::ser::{
AUTH_ASSERT_FIELDS, AuthData, AuthenticatorAssertionVisitor, ClientExtensionsOutputs,
ClientExtensionsOutputsVisitor, EXT_FIELDS,
@@ -59,10 +60,10 @@ impl<'de> Deserialize<'de> for ClientExtensionsOutputsRelaxed {
}
/// `newtype` around `AuthenticatorAssertion` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticatorAssertionRelaxed<User>(pub AuthenticatorAssertion<User>);
-impl<'de, User> Deserialize<'de> for AuthenticatorAssertionRelaxed<User>
+pub struct AuthenticatorAssertionRelaxed<U>(pub AuthenticatorAssertion<U>);
+impl<'de, U> Deserialize<'de> for AuthenticatorAssertionRelaxed<U>
where
- UserHandle<User>: Deserialize<'de>,
+ U: Deserialize<'de> + User + Default,
{
/// Same as [`AuthenticatorAssertion::deserialize`] except unknown keys are ignored.
///
@@ -76,17 +77,17 @@ where
.deserialize_struct(
"AuthenticatorAssertionRelaxed",
AUTH_ASSERT_FIELDS,
- AuthenticatorAssertionVisitor::<true, User>::new(),
+ AuthenticatorAssertionVisitor::<true, U>::new(),
)
.map(Self)
}
}
/// `newtype` around `Authentication` with a "relaxed" [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct AuthenticationRelaxed<User>(pub Authentication<User>);
-impl<'de, User> Deserialize<'de> for AuthenticationRelaxed<User>
+pub struct AuthenticationRelaxed<U>(pub Authentication<U>);
+impl<'de, U> Deserialize<'de> for AuthenticationRelaxed<U>
where
- UserHandle<User>: Deserialize<'de>,
+ U: Deserialize<'de> + User + Default,
{
/// Same as [`Authentication::deserialize`] except unknown keys are ignored;
/// [`response`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-response) is deserialized
@@ -106,8 +107,8 @@ where
/// if it exists it must be `null`, and
/// [`second`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfvalues-second) can exist but
/// must be `null` if so; and only
- /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and `response` are required. For
- /// the other fields, they are allowed to not exist or be `null`.
+ /// [`id`](https://www.w3.org/TR/webauthn-3/#dom-authenticationresponsejson-id) and `response` are required.
+ /// `rawId` and `type` and allowed to not exist. For the other fields, they are allowed to not exist or be `null`.
///
/// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
#[expect(clippy::unreachable, reason = "when there is a bug, we want to crash")]
@@ -119,7 +120,7 @@ where
PublicKeyCredential::<
true,
false,
- AuthenticatorAssertionRelaxed<User>,
+ AuthenticatorAssertionRelaxed<U>,
ClientExtensionsOutputsRelaxed,
>::deserialize(deserializer)
.map(|cred| {
@@ -133,18 +134,24 @@ where
})
}
}
+/// `AuthenticationRelaxed` with a required `UserHandle`.
+pub type PasskeyAuthenticationRelaxed<T> = AuthenticationRelaxed<UserHandle<T>>;
+/// `AuthenticationRelaxed` with a required `UserHandle64`.
+pub type PasskeyAuthenticationRelaxed64 = AuthenticationRelaxed<UserHandle64>;
+/// `AuthenticationRelaxed` with a required `UserHandle16`.
+pub type PasskeyAuthenticationRelaxed16 = AuthenticationRelaxed<UserHandle16>;
/// `newtype` around `Authentication` with a custom [`Self::deserialize`] implementation.
#[derive(Debug)]
-pub struct CustomAuthentication<User>(pub Authentication<User>);
-impl<'de, User> Deserialize<'de> for CustomAuthentication<User>
+pub struct CustomAuthentication<U>(pub Authentication<U>);
+impl<'de, U> Deserialize<'de> for CustomAuthentication<U>
where
- UserHandle<User>: Deserialize<'de>,
+ U: Deserialize<'de> + User + Default,
{
/// Despite the spec having a
/// [pre-defined format](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson) that clients
/// can follow, the downside is the superfluous data it contains.
///
- /// There simply is no reason to send the [`CredentialId`] _two_ times. This redundant data puts RPs in
+ /// There simply is no reason to send the [`CredentialId`] twice. This redundant data puts RPs in
/// a position where they either ignore the data or parse the data to ensure no contradictions exist
/// (e.g., [FIDO conformance requires one to verify `id` and `rawId` exist and match](https://github.com/w3c/webauthn/issues/2119#issuecomment-2287875401)).
///
@@ -154,29 +161,43 @@ where
///
/// ```json
/// {
- /// "authenticatorAttachment":null|"platform"|"cross-platform",
- /// "authenticatorData":<base64url string>,
- /// "clientDataJSON":<base64url string>,
- /// "clientExtensionResults":{}|{"prf":null}|{"prf":{}}|{"prf":{"results":null}}|{"prf":{"results":{"first":null}}}|{"prf":{"results":{"first":null,"second":null}}},
- /// "id":<see CredentialId::deserialize>,
- /// "signature":<base64url string>,
- /// "type":null|"public-key",
- /// "userHandle":null|<see UserHandle::deserialize>
+ /// "authenticatorAttachment": null | "platform" | "cross-platform",
+ /// "authenticatorData": <base64url string>,
+ /// "clientDataJSON": <base64url string>,
+ /// "clientExtensionResults": {
+ /// "prf": null | PRFJSON
+ /// },
+ /// "id": <see CredentialId::deserialize>,
+ /// "signature": <base64url string>,
+ /// "type": "public-key",
+ /// "userHandle": null | <see UserHandle::deserialize>
+ /// }
+ /// // PRFJSON:
+ /// {
+ /// "results": null | PRFOutputsJSON
+ /// }
+ /// // PRFOutputsJSON:
+ /// {
+ /// "first": null,
+ /// "second": null
/// }
/// ```
///
- /// All of the above keys are required with the exceptions of `"authenticatorAttachment"`, `"type"`, and
- /// `"userHandle"`.
+ /// `"userHandle"` is required to exist and not be `null` iff `U` is [`UserHandle`]. When it does exist and
+ /// is not `null`, then it is deserialized via [`UserHandle::deserialize`]. All of the remaining keys are
+ /// required with the exceptions of `"authenticatorAttachment"` and `"type"`. `"prf"` is not required in the
+ /// `clientExtensionResults` object, `"results"` is required in the `PRFJSON` object, and `"first"`
+ /// (but not `"second"`) is required in `PRFOutputsJSON`.
///
/// # Examples
///
/// ```
- /// # use webauthn_rp::{request::register::USER_HANDLE_MIN_LEN, response::auth::ser_relaxed::CustomAuthentication};
+ /// # use webauthn_rp::{request::register::{UserHandle, USER_HANDLE_MIN_LEN}, response::auth::ser_relaxed::CustomAuthentication};
/// assert!(
/// // The below payload is technically valid, but `AuthenticationServerState::verify` will fail
/// // since the authenticatorData is not valid. This is true for `Authentication::deserialize`
/// // as well since authenticatorData parsing is always deferred.
- /// serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ /// serde_json::from_str::<CustomAuthentication<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>(
/// r#"{
/// "authenticatorData": "AA",
/// "authenticatorAttachment": "cross-platform",
@@ -199,12 +220,12 @@ where
D: Deserializer<'de>,
{
/// `Visitor` for `CustomAuthentication`.
- struct CustomAuthenticationVisitor<U>(PhantomData<fn() -> U>);
- impl<'d, U> Visitor<'d> for CustomAuthenticationVisitor<U>
+ struct CustomAuthenticationVisitor<UHand>(PhantomData<fn() -> UHand>);
+ impl<'d, UHand> Visitor<'d> for CustomAuthenticationVisitor<UHand>
where
- UserHandle<U>: Deserialize<'d>,
+ UHand: Deserialize<'d> + User + Default,
{
- type Value = CustomAuthentication<U>;
+ type Value = CustomAuthentication<UHand>;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("CustomAuthentication")
}
@@ -325,13 +346,13 @@ where
if typ {
return Err(Error::duplicate_field(TYPE));
}
- typ = map.next_value::<Option<Type>>().map(|_| true)?;
+ typ = map.next_value::<Type>().map(|_| true)?;
}
Field::UserHandle => {
if user_handle.is_some() {
return Err(Error::duplicate_field(USER_HANDLE));
}
- user_handle = map.next_value::<Option<_>>().map(Some)?;
+ user_handle = map.next_value().map(Some)?;
}
}
}
@@ -347,24 +368,30 @@ where
.ok_or_else(|| Error::missing_field(SIGNATURE))
.and_then(|sig| {
if ext {
- Ok(CustomAuthentication(Authentication {
- response: AuthenticatorAssertion::new_inner(
- c_data,
- auth_data,
- sig,
- user_handle.unwrap_or_default(),
- ),
- authenticator_attachment:
- authenticator_attachment.map_or(
- AuthenticatorAttachment::None,
- |auth_attach| {
- auth_attach.unwrap_or(
- AuthenticatorAttachment::None,
- )
- },
+ if UHand::must_exist() {
+ user_handle.ok_or_else(|| Error::missing_field(USER_HANDLE))
+ } else {
+ user_handle.map_or_else(|| Ok(UHand::default()), Ok)
+ }.map(|user| {
+ CustomAuthentication(Authentication {
+ response: AuthenticatorAssertion::new_inner(
+ c_data,
+ auth_data,
+ sig,
+ user,
),
- raw_id,
- }))
+ authenticator_attachment:
+ authenticator_attachment.map_or(
+ AuthenticatorAttachment::None,
+ |auth_attach| {
+ auth_attach.unwrap_or(
+ AuthenticatorAttachment::None,
+ )
+ },
+ ),
+ raw_id,
+ })
+ })
} else {
Err(Error::missing_field(
CLIENT_EXTENSION_RESULTS,
@@ -410,11 +437,21 @@ where
)
}
}
+/// `CustomAuthentication` with a required `UserHandle`.
+pub type PasskeyCustomAuthentication<T> = CustomAuthentication<UserHandle<T>>;
+/// `CustomAuthentication` with a required `UserHandle64`.
+pub type PasskeyCustomAuthentication64 = CustomAuthentication<UserHandle64>;
+/// `CustomAuthentication` with a required `UserHandle16`.
+pub type PasskeyCustomAuthentication16 = CustomAuthentication<UserHandle16>;
#[cfg(test)]
mod tests {
use super::{
- super::{super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment},
- AuthenticationRelaxed, CustomAuthentication,
+ super::{
+ super::super::request::register::USER_HANDLE_MIN_LEN, AuthenticatorAttachment,
+ UserHandle,
+ },
+ AuthenticationRelaxed, CustomAuthentication, PasskeyAuthenticationRelaxed,
+ PasskeyCustomAuthentication,
};
use data_encoding::BASE64URL_NOPAD;
use rsa::sha2::{Digest as _, Sha256};
@@ -471,7 +508,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -511,7 +548,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "ABABABABABABABABABABAA",
@@ -536,7 +573,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -558,11 +595,11 @@ mod tests {
err
);
// `null` `id`.
- err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -585,7 +622,7 @@ mod tests {
);
// missing `rawId`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
@@ -603,8 +640,11 @@ mod tests {
.is_ok()
);
// `null` `rawId`.
- assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": null,
@@ -620,14 +660,17 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Missing `authenticatorData`.
err = Error::missing_field("authenticatorData")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -652,7 +695,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -676,7 +719,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -701,7 +744,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -724,7 +767,9 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<
+ AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
+ >(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -743,7 +788,9 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<
+ AuthenticationRelaxed<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
+ >(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -763,7 +810,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -793,7 +840,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -820,7 +867,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -845,7 +892,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -869,7 +916,7 @@ mod tests {
// Missing `response`.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -889,7 +936,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -910,7 +957,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -928,7 +975,7 @@ mod tests {
);
// Missing `clientExtensionResults`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -947,7 +994,7 @@ mod tests {
);
// `null` `clientExtensionResults`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -967,7 +1014,7 @@ mod tests {
);
// Missing `type`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -985,8 +1032,11 @@ mod tests {
.is_ok()
);
// `null` `type`.
- assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1002,14 +1052,17 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1035,7 +1088,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1046,7 +1099,7 @@ mod tests {
// Empty.
err = Error::missing_field("response").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1056,7 +1109,7 @@ mod tests {
);
// Unknown field in `response`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1080,7 +1133,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1106,7 +1159,7 @@ mod tests {
);
// Unknown field in `PublicKeyCredential`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1128,7 +1181,7 @@ mod tests {
// Duplicate field in `PublicKeyCredential`.
err = Error::duplicate_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1154,7 +1207,7 @@ mod tests {
);
// Base case is valid.
assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1181,7 +1234,7 @@ mod tests {
// missing `id`.
err = Error::missing_field("id").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"clientDataJSON": b64_cdata,
"authenticatorData": b64_adata,
@@ -1204,7 +1257,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": null,
"clientDataJSON": b64_cdata,
@@ -1227,7 +1280,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1249,7 +1302,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1270,7 +1323,7 @@ mod tests {
// Missing `signature`.
err = Error::missing_field("signature").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1292,7 +1345,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1312,7 +1365,9 @@ mod tests {
);
// Missing `userHandle`.
assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<
+ CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
+ >(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1328,7 +1383,9 @@ mod tests {
);
// `null` `userHandle`.
assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<
+ CustomAuthentication<Option<UserHandle<[u8; USER_HANDLE_MIN_LEN]>>>,
+ >(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1345,7 +1402,7 @@ mod tests {
);
// `null` `authenticatorAttachment`.
assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1372,7 +1429,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1396,7 +1453,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"authenticatorData": b64_adata,
@@ -1418,7 +1475,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": null,
@@ -1441,7 +1498,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({}).to_string().as_str()
)
.unwrap_err()
@@ -1454,7 +1511,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1476,7 +1533,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1495,7 +1552,7 @@ mod tests {
err
);
assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1509,8 +1566,12 @@ mod tests {
)
.is_ok()
);
- assert!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ // `null` `type`.
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1523,14 +1584,17 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1553,7 +1617,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!(null).to_string().as_str()
)
.unwrap_err()
@@ -1579,7 +1643,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"clientDataJSON": b64_cdata,
@@ -1603,7 +1667,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<CustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyCustomAuthentication<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1676,7 +1740,7 @@ mod tests {
let b64_user = BASE64URL_NOPAD.encode(b"\x00".as_slice());
// Base case is valid.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1705,7 +1769,7 @@ mod tests {
);
// `null` `prf`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1727,7 +1791,7 @@ mod tests {
);
// Unknown `clientExtensionResults`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1750,7 +1814,7 @@ mod tests {
// Duplicate field.
let mut err = Error::duplicate_field("prf").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1777,7 +1841,7 @@ mod tests {
);
// `null` `results`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1802,7 +1866,7 @@ mod tests {
// Duplicate field in `prf`.
err = Error::duplicate_field("results").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
@@ -1831,7 +1895,7 @@ mod tests {
);
// Missing `first`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1855,7 +1919,7 @@ mod tests {
);
// `null` `first`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1881,7 +1945,7 @@ mod tests {
);
// `null` `second`.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1911,7 +1975,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1943,7 +2007,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1976,7 +2040,7 @@ mod tests {
.to_string()
.into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2004,7 +2068,7 @@ mod tests {
);
// Unknown `prf` field.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2029,7 +2093,7 @@ mod tests {
);
// Unknown `results` field.
assert!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -2057,7 +2121,7 @@ mod tests {
// Duplicate field in `results`.
err = Error::duplicate_field("first").to_string().into_bytes();
assert_eq!(
- serde_json::from_str::<AuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
+ serde_json::from_str::<PasskeyAuthenticationRelaxed<[u8; USER_HANDLE_MIN_LEN]>>(
format!(
"{{
\"id\": \"AAAAAAAAAAAAAAAAAAAAAA\",
diff --git a/src/response/register.rs b/src/response/register.rs
@@ -3091,6 +3091,7 @@ mod tests {
AggErr,
request::{AsciiDomain, RpId},
},
+ UserHandle,
auth::{AuthenticatorAssertion, AuthenticatorData},
},
AttestationFormat, AttestationObject, AuthDataContainer as _, AuthTransports,
@@ -3154,11 +3155,10 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87".as_slice()).unwrap();
- let auth_assertion = AuthenticatorAssertion::new_user_vec(
+ let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user(
client_data_json_2,
authenticator_data,
signature,
- None,
);
let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?;
assert_eq!(
@@ -3236,11 +3236,10 @@ mod tests {
.unwrap();
let client_data_json_2 = HEXLOWER.decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d".as_slice()).unwrap();
let signature = HEXLOWER.decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744".as_slice()).unwrap();
- let auth_assertion = AuthenticatorAssertion::new_user_vec(
+ let auth_assertion = AuthenticatorAssertion::<Option<UserHandle<Vec<u8>>>>::without_user(
client_data_json_2,
authenticator_data,
signature,
- None,
);
let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?;
assert_eq!(
diff --git a/src/response/register/ser.rs b/src/response/register/ser.rs
@@ -1792,7 +1792,7 @@ mod tests {
err
);
// `null` `id`.
- err = Error::invalid_type(Unexpected::Other("null"), &"id")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
.to_string()
.into_bytes();
assert_eq!(
@@ -1845,7 +1845,7 @@ mod tests {
err
);
// `null` `rawId`.
- err = Error::invalid_type(Unexpected::Other("null"), &"rawId")
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
.to_string()
.into_bytes();
assert_eq!(
@@ -2585,7 +2585,7 @@ mod tests {
err
);
// `null` `type`.
- err = Error::invalid_type(Unexpected::Other("null"), &"type to be 'public-key'")
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
.to_string()
.into_bytes();
assert_eq!(
diff --git a/src/response/register/ser_relaxed.rs b/src/response/register/ser_relaxed.rs
@@ -175,7 +175,8 @@ impl<'de> Deserialize<'de> for RegistrationRelaxed {
/// via [`AuthenticatorAttestationRelaxed::deserialize`],
/// [`clientExtensionResults`](https://www.w3.org/TR/webauthn-3/#dom-registrationresponsejson-clientextensionresults)
/// is `null` or deserialized via [`ClientExtensionsOutputsRelaxed::deserialize`], and only `response` is required.
- /// For the other fields, they are allowed to not exist or be `null`.
+ /// `id`, `rawId`, and `type` are allowed to not exist. For the other fields, they are allowed to not exist or
+ /// be `null`.
///
/// Note that duplicate keys are still forbidden, and data matching still applies when applicable.
#[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
@@ -229,12 +230,12 @@ impl<'de> Deserialize<'de> for CustomRegistration {
///
/// ```json
/// {
- /// "attestationObject":<base64url string>,
- /// "authenticatorAttachment":null|"platform"|"cross-platform",
- /// "clientDataJSON":<base64url string>,
- /// "clientExtensionResults":<see ClientExtensionsOutputs::deserialize>,
- /// "transports":<see AuthTransports::deserialize>,
- /// "type":null|"public-key"
+ /// "attestationObject": <base64url string>,
+ /// "authenticatorAttachment": null | "platform" | "cross-platform",
+ /// "clientDataJSON": <base64url string>,
+ /// "clientExtensionResults": <see ClientExtensionsOutputs::deserialize>,
+ /// "transports": <see AuthTransports::deserialize>,
+ /// "type": "public-key"
/// }
/// ```
///
@@ -376,7 +377,7 @@ impl<'de> Deserialize<'de> for CustomRegistration {
if typ {
return Err(Error::duplicate_field(TYPE));
}
- typ = map.next_value::<Option<Type>>().map(|_| true)?;
+ typ = map.next_value::<Type>().map(|_| true)?;
}
}
}
@@ -719,13 +720,16 @@ mod tests {
.is_ok()
);
// `null` `id`.
- assert!(
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
serde_json::from_str::<RegistrationRelaxed>(
serde_json::json!({
"id": null,
"rawId": "AAAAAAAAAAAAAAAAAAAAAA",
"response": {
- "clientDataJSON": b64_cdata,
+ "clientDataJSON": null,
"authenticatorData": b64_adata,
"transports": [],
"publicKey": b64_key,
@@ -738,7 +742,10 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Missing `rawId`.
assert!(
@@ -762,7 +769,10 @@ mod tests {
.is_ok()
);
// `null` `rawId`.
- assert!(
+ err = Error::invalid_type(Unexpected::Other("null"), &"CredentialId")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
serde_json::from_str::<RegistrationRelaxed>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -781,7 +791,10 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// `id` and the credential id in authenticator data mismatch.
err = Error::invalid_value(
@@ -1433,7 +1446,10 @@ mod tests {
.is_ok()
);
// `null` `type`.
- assert!(
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
serde_json::from_str::<RegistrationRelaxed>(
serde_json::json!({
"id": "AAAAAAAAAAAAAAAAAAAAAA",
@@ -1452,7 +1468,10 @@ mod tests {
.to_string()
.as_str()
)
- .is_ok()
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
@@ -1902,7 +1921,10 @@ mod tests {
.map_or(false, |_| true)
);
// `null` `type`.
- assert!(
+ err = Error::invalid_type(Unexpected::Other("null"), &"public-key")
+ .to_string()
+ .into_bytes();
+ assert_eq!(
serde_json::from_str::<CustomRegistration>(
serde_json::json!({
"attestationObject": b64_aobj,
@@ -1914,7 +1936,10 @@ mod tests {
.to_string()
.as_str()
)
- .map_or(false, |_| true)
+ .unwrap_err()
+ .to_string()
+ .into_bytes()[..err.len()],
+ err
);
// Not exactly `public-type` `type`.
err = Error::invalid_value(Unexpected::Str("Public-key"), &"public-key")
diff --git a/src/response/ser.rs b/src/response/ser.rs
@@ -647,7 +647,7 @@ where
}
}
let mut opt_id = None;
- let mut typ = None;
+ let mut typ = false;
let mut raw = None;
let mut resp = None;
let mut attach = None;
@@ -658,19 +658,19 @@ where
if opt_id.is_some() {
return Err(Error::duplicate_field(ID));
}
- opt_id = map.next_value::<Option<_>>().map(Some)?;
+ opt_id = map.next_value::<CredentialId<_>>().map(Some)?;
}
Field::Type => {
- if typ.is_some() {
+ if typ {
return Err(Error::duplicate_field(TYPE));
}
- typ = map.next_value::<Option<Type>>().map(Some)?;
+ typ = map.next_value::<Type>().map(|_| true)?;
}
Field::RawId => {
if raw.is_some() {
return Err(Error::duplicate_field(RAW_ID));
}
- raw = map.next_value::<Option<CredentialId<_>>>().map(Some)?;
+ raw = map.next_value::<CredentialId<_>>().map(Some)?;
}
Field::Response => {
if resp.is_some() {
@@ -695,12 +695,10 @@ where
}
resp.ok_or_else(|| Error::missing_field(RESPONSE))
.and_then(|response| {
- opt_id.ok_or(false).and_then(|id| id.ok_or(true)).map_or_else(
- |flag| {
+ opt_id.map_or_else(
+ || {
if REL && REGI {
Ok(None)
- } else if flag {
- Err(Error::invalid_type(Unexpected::Other("null"), &format!("{ID} to be a base64url-encoded CredentialId").as_str()))
} else {
Err(Error::missing_field(ID))
}
@@ -708,12 +706,10 @@ where
|id| Ok(Some(id)),
)
.and_then(|id| {
- raw.ok_or(false).and_then(|opt_raw_id| opt_raw_id.ok_or(true)).map_or_else(
- |flag| {
+ raw.map_or_else(
+ || {
if REL {
Ok(())
- } else if flag {
- Err(Error::invalid_type(Unexpected::Other("null"), &format!("{RAW_ID} to be a base64url-encoded CredentialId").as_str()))
} else {
Err(Error::missing_field(RAW_ID))
}
@@ -748,23 +744,16 @@ where
Ok
)
.and_then(|client_extension_results| {
- typ.ok_or(false).and_then(|opt_typ| opt_typ.ok_or(true)).map_or_else(
- |flag| {
- if REL {
- Ok(())
- } else if flag {
- Err(Error::invalid_type(Unexpected::Other("null"), &format!("{TYPE} to be '{PUBLIC_KEY}'").as_str()))
- } else {
- Err(Error::missing_field(TYPE))
- }
- },
- |_| Ok(()),
- ).map(|()| PublicKeyCredential {
- id,
- response,
- authenticator_attachment: attach.flatten().unwrap_or(AuthenticatorAttachment::None),
- client_extension_results,
- })
+ if typ || REL {
+ Ok(PublicKeyCredential {
+ id,
+ response,
+ authenticator_attachment: attach.flatten().unwrap_or(AuthenticatorAttachment::None),
+ client_extension_results,
+ })
+ } else {
+ Err(Error::missing_field(TYPE))
+ }
})
})
})