webauthn_rp

WebAuthn RP library.
git clone https://git.philomathiclife.com/repos/webauthn_rp
Log | Files | Refs | README

commit 407bd6d6d8592aa03f3c3ffa8eb478d0b4c0fb72
parent 5b5f26830d93e14fb5172c8db50e950a4de46112
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue,  7 Apr 2026 13:48:54 -0600

add immediate ui

Diffstat:
Msrc/request/auth.rs | 13+++++++++++--
Msrc/request/auth/ser.rs | 129++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/request/auth/ser/tests.rs | 16++++++++++------
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