commit 407bd6d6d8592aa03f3c3ffa8eb478d0b4c0fb72
parent 5b5f26830d93e14fb5172c8db50e950a4de46112
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 7 Apr 2026 13:48:54 -0600
add immediate ui
Diffstat:
3 files changed, 133 insertions(+), 25 deletions(-)
diff --git a/src/request/auth.rs b/src/request/auth.rs
@@ -316,6 +316,12 @@ impl From<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>> for AllowedCredentials
creds
}
}
+/// [`CredentialUiMode`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialuimode)
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum CredentialUiMode {
+ /// [`immediate`](https://www.w3.org/TR/credential-management-1/#dom-credentialuimode-immediate).
+ Immediate,
+}
/// The [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions)
/// to send to the client when authenticating a discoverable credential.
///
@@ -324,12 +330,14 @@ impl From<Vec<PublicKeyCredentialDescriptor<Box<[u8]>>>> for AllowedCredentials
/// [`DiscoverableAuthentication`], it is validated using [`DiscoverableAuthenticationServerState::verify`].
#[derive(Debug)]
pub struct DiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
- /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-mediation).
///
/// Note if this is [`CredentialMediationRequirement::Conditional`], user agents are instructed to not
/// enforce any timeout; as result, one may want to set [`PublicKeyCredentialRequestOptions::timeout`] to
/// [`NonZeroU32::MAX`].
pub mediation: CredentialMediationRequirement,
+ /// [`uiMode`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-uimode).
+ pub ui_mode: Option<CredentialUiMode>,
/// `public-key` [credential type](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry).
pub public_key: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
}
@@ -355,6 +363,7 @@ impl<'rp_id, 'prf_first, 'prf_second>
pub fn passkey<'a: 'rp_id>(rp_id: &'a RpId) -> Self {
Self {
mediation: CredentialMediationRequirement::default(),
+ ui_mode: None,
public_key: PublicKeyCredentialRequestOptions::passkey(rp_id),
}
}
@@ -425,7 +434,7 @@ impl<'rp_id, 'prf_first, 'prf_second>
/// [`NonDiscoverableAuthentication`], it is validated using [`NonDiscoverableAuthenticationServerState::verify`].
#[derive(Debug)]
pub struct NonDiscoverableCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> {
- /// [`mediation`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialmediationrequirement).
+ /// [`mediation`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-mediation).
pub mediation: CredentialMediationRequirement,
/// [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions).
pub options: PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>,
diff --git a/src/request/auth/ser.rs b/src/request/auth/ser.rs
@@ -3,10 +3,10 @@ mod tests;
use super::{
super::{super::response::ser::Null, ser::PrfHelper},
AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement,
- Credentials as _, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions,
- Extension, ExtensionReq, FIVE_MINUTES, Hints, NonDiscoverableAuthenticationClientState,
- NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned,
- PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement,
+ CredentialUiMode, Credentials as _, DiscoverableAuthenticationClientState,
+ DiscoverableCredentialRequestOptions, Extension, ExtensionReq, FIVE_MINUTES, Hints,
+ NonDiscoverableAuthenticationClientState, NonDiscoverableCredentialRequestOptions, PrfInput,
+ PrfInputOwned, PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement,
};
use core::{
error::Error as E,
@@ -14,9 +14,33 @@ use core::{
num::NonZeroU32,
};
use serde::{
- de::{Deserialize, Deserializer, Error, MapAccess, Visitor},
+ de::{Deserialize, Deserializer, Error, MapAccess, Unexpected, Visitor},
ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer},
};
+/// `"immediate"`.
+const IMMEDIATE: &str = "immediate";
+impl Serialize for CredentialUiMode {
+ /// Serializes `self` as a [`prim@str`] conforming with
+ /// [`CredentialUiMode`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialuimode).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::auth::CredentialUiMode;
+ /// assert_eq!(
+ /// serde_json::to_string(&CredentialUiMode::Immediate)?,
+ /// r#""immediate""#
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(IMMEDIATE)
+ }
+}
impl Serialize for PrfInputOwned {
/// See [`PrfInput::serialize`]
#[inline]
@@ -279,6 +303,8 @@ impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> {
}
/// `"mediation"`.
const MEDIATION: &str = "mediation";
+/// `"uiMode"`.
+const UI_MODE: &str = "uiMode";
/// `"publicKey"`.
const PUBLIC_KEY: &str = "publicKey";
impl Serialize for DiscoverableCredentialRequestOptions<'_, '_, '_> {
@@ -289,24 +315,36 @@ impl Serialize for DiscoverableCredentialRequestOptions<'_, '_, '_> {
/// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
/// is serialized to conform to
/// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
+ /// Additionally [`uiMode`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-uiMode)
+ /// is not present iff [`Self::ui_mode`] is `None`.
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer
- .serialize_struct("DiscoverableCredentialRequestOptions", 2)
+ .serialize_struct(
+ "DiscoverableCredentialRequestOptions",
+ if self.ui_mode.is_none() { 2 } else { 3 },
+ )
.and_then(|mut ser| {
ser.serialize_field(MEDIATION, &self.mediation)
.and_then(|()| {
- ser.serialize_field(
- PUBLIC_KEY,
- &AuthenticationClientState(
- &self.public_key,
- &AllowedCredentials::with_capacity(0),
- ),
- )
- .and_then(|()| ser.end())
+ if self.ui_mode.is_none() {
+ Ok(())
+ } else {
+ ser.serialize_field(UI_MODE, &self.ui_mode)
+ }
+ .and_then(|()| {
+ ser.serialize_field(
+ PUBLIC_KEY,
+ &AuthenticationClientState(
+ &self.public_key,
+ &AllowedCredentials::with_capacity(0),
+ ),
+ )
+ .and_then(|()| ser.end())
+ })
})
})
}
@@ -316,7 +354,8 @@ impl Serialize for NonDiscoverableCredentialRequestOptions<'_, '_, '_> {
/// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions).
///
/// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal)
- /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
+ /// and [`uiMode`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-uiMode)
+ /// are not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry)
/// is serialized to conform to
/// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson).
#[inline]
@@ -515,6 +554,48 @@ impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> {
self.0.serialize(serializer)
}
}
+impl<'de> Deserialize<'de> for CredentialUiMode {
+ /// Deserializes a [`prim@str`] based on
+ /// [`CredentialUiMode`](https://www.w3.org/TR/credential-management-1/#enumdef-credentialuimode).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webauthn_rp::request::auth::CredentialUiMode;
+ /// assert_eq!(
+ /// serde_json::from_str::<CredentialUiMode>(r#""immediate""#)?,
+ /// CredentialUiMode::Immediate,
+ /// );
+ /// # Ok::<_, serde_json::Error>(())
+ /// ```
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct CredentialUiModeVisitor;
+ impl Visitor<'_> for CredentialUiModeVisitor {
+ type Value = CredentialUiMode;
+ fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ write!(formatter, "'{IMMEDIATE}'")
+ }
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: Error,
+ {
+ if v == IMMEDIATE {
+ Ok(CredentialUiMode::Immediate)
+ } else {
+ Err(E::invalid_value(
+ Unexpected::Str(v),
+ &format!("'{IMMEDIATE}'").as_str(),
+ ))
+ }
+ }
+ }
+ deserializer.deserialize_str(CredentialUiModeVisitor)
+ }
+}
/// Similar to [`Extension`] except [`PrfInputOwned`] is used.
///
/// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`].
@@ -974,6 +1055,8 @@ pub struct ClientCredentialRequestOptions {
/// See [`DiscoverableCredentialRequestOptions::mediation`] and
/// [`NonDiscoverableCredentialRequestOptions::mediation`].
pub mediation: CredentialMediationRequirement,
+ /// See [`DiscoverableCredentialRequestOptions::mediation`].
+ pub ui_mode: Option<CredentialUiMode>,
/// See [`DiscoverableCredentialRequestOptions::public_key`] and
/// See [`NonDiscoverableCredentialRequestOptions::options`].
pub public_key: PublicKeyCredentialRequestOptionsOwned,
@@ -984,6 +1067,7 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
/// ```json
/// {
/// "mediation": null | "required" | "conditional",
+ /// "uiMode": null | "immediate",
/// "publicKey": null | <PublicKeyCredentialRequestOptionsOwned>
/// }
/// ```
@@ -1013,6 +1097,8 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
enum Field {
/// `mediation`.
Mediation,
+ /// `uiMode`.
+ UiMode,
/// `publicKey`
PublicKey,
}
@@ -1026,7 +1112,7 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- write!(formatter, "'{MEDIATION}' or '{PUBLIC_KEY}'")
+ write!(formatter, "'{MEDIATION}', '{UI_MODE}', or '{PUBLIC_KEY}'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
@@ -1034,6 +1120,7 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
{
match v {
MEDIATION => Ok(Field::Mediation),
+ UI_MODE => Ok(Field::UiMode),
PUBLIC_KEY => Ok(Field::PublicKey),
_ => Err(E::unknown_field(v, FIELDS)),
}
@@ -1043,6 +1130,7 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
}
}
let mut med = None;
+ let mut ui = None;
let mut key = None;
while let Some(k) = map.next_key()? {
match k {
@@ -1052,6 +1140,12 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
}
med = map.next_value::<Option<_>>().map(Some)?;
}
+ Field::UiMode => {
+ if ui.is_some() {
+ return Err(Error::duplicate_field(UI_MODE));
+ }
+ ui = map.next_value().map(Some)?;
+ }
Field::PublicKey => {
if key.is_some() {
return Err(Error::duplicate_field(PUBLIC_KEY));
@@ -1062,12 +1156,13 @@ impl<'de> Deserialize<'de> for ClientCredentialRequestOptions {
}
Ok(ClientCredentialRequestOptions {
mediation: med.flatten().unwrap_or_default(),
+ ui_mode: ui.flatten(),
public_key: key.flatten().unwrap_or_default(),
})
}
}
/// Fields for `ClientCredentialRequestOptions`.
- const FIELDS: &[&str; 2] = &[MEDIATION, PUBLIC_KEY];
+ const FIELDS: &[&str; 3] = &[MEDIATION, UI_MODE, PUBLIC_KEY];
deserializer.deserialize_struct(
"ClientCredentialRequestOptions",
FIELDS,
diff --git a/src/request/auth/ser/tests.rs b/src/request/auth/ser/tests.rs
@@ -1,7 +1,8 @@
use super::{
super::{super::PublicKeyCredentialHint, ExtensionReq},
- ClientCredentialRequestOptions, CredentialMediationRequirement, ExtensionOwned, FIVE_MINUTES,
- Hints, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, UserVerificationRequirement,
+ ClientCredentialRequestOptions, CredentialMediationRequirement, CredentialUiMode,
+ ExtensionOwned, FIVE_MINUTES, Hints, NonZeroU32, PublicKeyCredentialRequestOptionsOwned,
+ UserVerificationRequirement,
};
use serde_json::Error;
#[expect(
@@ -15,8 +16,8 @@ fn client_options() -> Result<(), Error> {
let mut err =
serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"bob":true}"#).unwrap_err();
assert_eq!(
- err.to_string().get(..56),
- Some("unknown field `bob`, expected `mediation` or `publicKey`")
+ err.to_string().get(..71),
+ Some("unknown field `bob`, expected one of `mediation`, `uiMode`, `publicKey`")
);
err = serde_json::from_str::<ClientCredentialRequestOptions>(
r#"{"mediation":"required","mediation":"required"}"#,
@@ -31,6 +32,7 @@ fn client_options() -> Result<(), Error> {
options.mediation,
CredentialMediationRequirement::Required
));
+ assert!(options.ui_mode.is_none());
assert!(options.public_key.rp_id.is_none());
assert_eq!(options.public_key.timeout, FIVE_MINUTES);
assert!(matches!(
@@ -40,12 +42,13 @@ fn client_options() -> Result<(), Error> {
assert_eq!(options.public_key.hints, Hints::EMPTY);
assert!(options.public_key.extensions.prf.is_none());
options = serde_json::from_str::<ClientCredentialRequestOptions>(
- r#"{"mediation":null,"publicKey":null}"#,
+ r#"{"mediation":null,"uiMode":null,"publicKey":null}"#,
)?;
assert!(matches!(
options.mediation,
CredentialMediationRequirement::Required
));
+ assert!(options.ui_mode.is_none());
assert!(options.public_key.rp_id.is_none());
assert_eq!(options.public_key.timeout, FIVE_MINUTES);
assert!(matches!(
@@ -64,12 +67,13 @@ fn client_options() -> Result<(), Error> {
assert_eq!(options.public_key.hints, Hints::EMPTY);
assert!(options.public_key.extensions.prf.is_none());
options = serde_json::from_str::<ClientCredentialRequestOptions>(
- r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#,
+ r#"{"mediation":"conditional","uiMode":"immediate","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#,
)?;
assert!(matches!(
options.mediation,
CredentialMediationRequirement::Conditional
));
+ assert_eq!(options.ui_mode, Some(CredentialUiMode::Immediate));
assert!(
options
.public_key