ser.rs (56295B)
1 use super::{ 2 super::{super::response::ser::Null, ser::PrfHelper}, 3 AllowedCredential, AllowedCredentials, Challenge, CredentialMediationRequirement, 4 Credentials as _, DiscoverableAuthenticationClientState, DiscoverableCredentialRequestOptions, 5 Extension, ExtensionReq, FIVE_MINUTES, Hint, NonDiscoverableAuthenticationClientState, 6 NonDiscoverableCredentialRequestOptions, PrfInput, PrfInputOwned, 7 PublicKeyCredentialRequestOptions, RpId, UserVerificationRequirement, 8 }; 9 use core::{ 10 error::Error as E, 11 fmt::{self, Display, Formatter}, 12 num::NonZeroU32, 13 }; 14 use serde::{ 15 de::{Deserialize, Deserializer, Error, MapAccess, Visitor}, 16 ser::{Serialize, SerializeMap as _, SerializeStruct as _, Serializer}, 17 }; 18 impl Serialize for PrfInputOwned { 19 /// See [`PrfInput::serialize`] 20 #[inline] 21 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 22 where 23 S: Serializer, 24 { 25 PrfInput { 26 first: self.first.as_slice(), 27 second: self.second.as_deref(), 28 } 29 .serialize(serializer) 30 } 31 } 32 impl Serialize for AllowedCredential { 33 /// Serializes `self` to conform with 34 /// [`PublicKeyCredentialDescriptorJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptorjson). 35 /// 36 /// # Examples 37 /// 38 /// ``` 39 /// # #[cfg(all(feature = "bin", feature = "custom"))] 40 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 41 /// # use webauthn_rp::{ 42 /// # request::{auth::AllowedCredential, PublicKeyCredentialDescriptor}, 43 /// # response::{AuthTransports, CredentialId}, 44 /// # }; 45 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 46 /// /// from the database. 47 /// # #[cfg(all(feature = "bin", feature = "custom"))] 48 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 49 /// // ⋮ 50 /// # AuthTransports::decode(32) 51 /// } 52 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 53 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 54 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 55 /// # #[cfg(all(feature = "bin", feature = "custom"))] 56 /// let id = CredentialId::try_from(vec![0; 16])?; 57 /// # #[cfg(all(feature = "bin", feature = "custom"))] 58 /// let transports = get_transports((&id).into())?; 59 /// # #[cfg(all(feature = "bin", feature = "custom"))] 60 /// assert_eq!( 61 /// serde_json::to_string(&AllowedCredential::from(PublicKeyCredentialDescriptor { 62 /// id, 63 /// transports 64 /// })).unwrap(), 65 /// r#"{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}"# 66 /// ); 67 /// # Ok::<_, webauthn_rp::AggErr>(()) 68 /// ``` 69 #[inline] 70 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 71 where 72 S: Serializer, 73 { 74 self.credential.serialize(serializer) 75 } 76 } 77 impl Serialize for AllowedCredentials { 78 /// Serializes `self` to conform with 79 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials). 80 /// 81 /// # Examples 82 /// 83 /// ``` 84 /// # #[cfg(all(feature = "bin", feature = "custom"))] 85 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 86 /// # use webauthn_rp::{ 87 /// # request::{auth::AllowedCredentials, PublicKeyCredentialDescriptor, Credentials}, 88 /// # response::{AuthTransports, CredentialId}, 89 /// # }; 90 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 91 /// /// from the database. 92 /// # #[cfg(all(feature = "bin", feature = "custom"))] 93 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 94 /// // ⋮ 95 /// # AuthTransports::decode(32) 96 /// } 97 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 98 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 99 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 100 /// # #[cfg(all(feature = "bin", feature = "custom"))] 101 /// let id = CredentialId::try_from(vec![0; 16])?; 102 /// # #[cfg(all(feature = "bin", feature = "custom"))] 103 /// let transports = get_transports((&id).into())?; 104 /// let mut creds = AllowedCredentials::with_capacity(1); 105 /// # #[cfg(all(feature = "bin", feature = "custom"))] 106 /// creds.push(PublicKeyCredentialDescriptor { id, transports }.into()); 107 /// # #[cfg(all(feature = "bin", feature = "custom"))] 108 /// assert_eq!( 109 /// serde_json::to_string(&creds).unwrap(), 110 /// r#"[{"type":"public-key","id":"AAAAAAAAAAAAAAAAAAAAAA","transports":["usb"]}]"# 111 /// ); 112 /// # Ok::<_, webauthn_rp::AggErr>(()) 113 /// ``` 114 #[inline] 115 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 116 where 117 S: Serializer, 118 { 119 self.creds.serialize(serializer) 120 } 121 } 122 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 123 struct PrfCreds<'a>(&'a AllowedCredentials); 124 impl Serialize for PrfCreds<'_> { 125 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 126 where 127 S: Serializer, 128 { 129 serializer 130 .serialize_map(Some(self.0.prf_count)) 131 .and_then(|mut ser| { 132 self.0 133 .creds 134 .iter() 135 .try_fold((), |(), cred| { 136 cred.extension.prf.as_ref().map_or(Ok(()), |input| { 137 ser.serialize_entry(&cred.credential.id, input) 138 }) 139 }) 140 .and_then(|()| ser.end()) 141 }) 142 } 143 } 144 /// [`AuthenticationExtensionsPRFInputs`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsprfinputs). 145 struct PrfInputs<'a, 'b, 'c> { 146 /// [`eval`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-eval). 147 eval: Option<PrfInput<'a, 'b>>, 148 /// [`evalByCredential`](https://www.w3.org/TR/webauthn-3/#dom-authenticationextensionsprfinputs-evalbycredential). 149 eval_by_credential: PrfCreds<'c>, 150 } 151 impl Serialize for PrfInputs<'_, '_, '_> { 152 #[expect( 153 clippy::arithmetic_side_effects, 154 reason = "comment explains how overflow is not possible" 155 )] 156 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 157 where 158 S: Serializer, 159 { 160 serializer 161 .serialize_struct( 162 "PrfInputs", 163 // The max is 1 + 1 = 2, so overflow is not an issue. 164 usize::from(self.eval.is_some()) 165 + usize::from(self.eval_by_credential.0.prf_count > 0), 166 ) 167 .and_then(|mut ser| { 168 self.eval 169 .map_or(Ok(()), |eval| ser.serialize_field("eval", &eval)) 170 .and_then(|()| { 171 if self.eval_by_credential.0.prf_count == 0 { 172 Ok(()) 173 } else { 174 ser.serialize_field("evalByCredential", &self.eval_by_credential) 175 } 176 }) 177 .and_then(|()| ser.end()) 178 }) 179 } 180 } 181 /// Serializes `self` to conform with 182 /// [`AuthenticationExtensionsClientInputsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientinputsjson). 183 struct ExtensionHelper<'a, 'b, 'c> { 184 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 185 extension: &'a Extension<'b, 'c>, 186 /// [`extension`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-extensions). 187 /// 188 /// Some extensions contain records, so we need both this and above. 189 allow_credentials: &'a AllowedCredentials, 190 } 191 impl Serialize for ExtensionHelper<'_, '_, '_> { 192 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 193 where 194 S: Serializer, 195 { 196 let ext_count = 197 usize::from(self.extension.prf.is_some() || self.allow_credentials.prf_count > 0); 198 serializer 199 .serialize_struct("Extension", ext_count) 200 .and_then(|mut ser| { 201 if ext_count == 0 { 202 Ok(()) 203 } else { 204 ser.serialize_field( 205 "prf", 206 &PrfInputs { 207 eval: self.extension.prf.map(|prf| prf.0), 208 eval_by_credential: PrfCreds(self.allow_credentials), 209 }, 210 ) 211 } 212 .and_then(|()| ser.end()) 213 }) 214 } 215 } 216 /// `"challenge"` 217 const CHALLENGE: &str = "challenge"; 218 /// `"timeout"` 219 const TIMEOUT: &str = "timeout"; 220 /// `"rpId"` 221 const RP_ID: &str = "rpId"; 222 /// `"allowCredentials"` 223 const ALLOW_CREDENTIALS: &str = "allowCredentials"; 224 /// `"extensions"` 225 const EXTENSIONS: &str = "extensions"; 226 /// `"hints"` 227 const HINTS: &str = "hints"; 228 /// `"userVerification"` 229 const USER_VERIFICATION: &str = "userVerification"; 230 /// Helper type that peforms the serialization for both [`DiscoverableAuthenticationClientState`] and 231 /// [`NonDiscoverableAuthenticationClientState`] and 232 struct AuthenticationClientState<'rp_id, 'prf_first, 'prf_second, 'opt, 'cred>( 233 &'opt PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second>, 234 &'cred AllowedCredentials, 235 ); 236 impl Serialize for AuthenticationClientState<'_, '_, '_, '_, '_> { 237 #[inline] 238 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 239 where 240 S: Serializer, 241 { 242 serializer 243 .serialize_struct("PublicKeyCredentialRequestOptions", 7) 244 .and_then(|mut ser| { 245 ser.serialize_field(CHALLENGE, &self.0.challenge) 246 .and_then(|()| { 247 ser.serialize_field(TIMEOUT, &self.0.timeout) 248 .and_then(|()| { 249 ser.serialize_field(RP_ID, &self.0.rp_id).and_then(|()| { 250 ser.serialize_field(ALLOW_CREDENTIALS, &self.1) 251 .and_then(|()| { 252 ser.serialize_field( 253 USER_VERIFICATION, 254 &self.0.user_verification, 255 ) 256 .and_then( 257 |()| { 258 ser.serialize_field(HINTS, &self.0.hints) 259 .and_then(|()| { 260 ser.serialize_field( 261 EXTENSIONS, 262 &ExtensionHelper { 263 extension: &self.0.extensions, 264 allow_credentials: self.1, 265 }, 266 ) 267 .and_then(|()| ser.end()) 268 }) 269 }, 270 ) 271 }) 272 }) 273 }) 274 }) 275 }) 276 } 277 } 278 /// `"mediation"`. 279 const MEDIATION: &str = "mediation"; 280 /// `"publicKey"`. 281 const PUBLIC_KEY: &str = "publicKey"; 282 impl Serialize for DiscoverableCredentialRequestOptions<'_, '_, '_> { 283 /// Serializes `self` to conform with 284 /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions). 285 /// 286 /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal) 287 /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry) 288 /// is serialized to conform to 289 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 290 #[inline] 291 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 292 where 293 S: Serializer, 294 { 295 serializer 296 .serialize_struct("DiscoverableCredentialRequestOptions", 2) 297 .and_then(|mut ser| { 298 ser.serialize_field(MEDIATION, &self.mediation) 299 .and_then(|()| { 300 ser.serialize_field( 301 PUBLIC_KEY, 302 &AuthenticationClientState( 303 &self.public_key, 304 &AllowedCredentials::with_capacity(0), 305 ), 306 ) 307 .and_then(|()| ser.end()) 308 }) 309 }) 310 } 311 } 312 impl Serialize for NonDiscoverableCredentialRequestOptions<'_, '_, '_> { 313 /// Serializes `self` to conform with 314 /// [`CredentialRequestOptions`](https://www.w3.org/TR/credential-management-1/#dictdef-credentialrequestoptions). 315 /// 316 /// Note [`signal`](https://www.w3.org/TR/credential-management-1/#dom-credentialrequestoptions-signal) 317 /// is not present, and [`publicKey`](https://www.w3.org/TR/credential-management-1/#sctn-cred-type-registry) 318 /// is serialized to conform to 319 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 320 #[inline] 321 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 322 where 323 S: Serializer, 324 { 325 serializer 326 .serialize_struct("NonDiscoverableCredentialRequestOptions", 2) 327 .and_then(|mut ser| { 328 ser.serialize_field(MEDIATION, &self.mediation) 329 .and_then(|()| { 330 ser.serialize_field( 331 PUBLIC_KEY, 332 &AuthenticationClientState(&self.options, &self.allow_credentials), 333 ) 334 .and_then(|()| ser.end()) 335 }) 336 }) 337 } 338 } 339 impl Serialize for DiscoverableAuthenticationClientState<'_, '_, '_> { 340 /// Serializes `self` according to [`DiscoverableCredentialRequestOptions::serialize`]. 341 /// 342 /// # Examples 343 /// 344 /// ``` 345 /// # use webauthn_rp::{ 346 /// # request::{ 347 /// # auth::{ 348 /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, 349 /// # PrfInputOwned, DiscoverableCredentialRequestOptions 350 /// # }, 351 /// # AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, 352 /// # }, 353 /// # response::{AuthTransports, CredentialId}, 354 /// # }; 355 /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 356 /// let mut options = DiscoverableCredentialRequestOptions::passkey(&rp_id); 357 /// options.public_key.hints = Hint::SecurityKey; 358 /// options.public_key.extensions = Extension { 359 /// prf: Some((PrfInput { 360 /// first: [0; 4].as_slice(), 361 /// second: None, 362 /// }, ExtensionReq::Require)), 363 /// }; 364 /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap(); 365 /// let json = serde_json::json!({ 366 /// "mediation":"required", 367 /// "publicKey":{ 368 /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA", 369 /// "timeout":300000, 370 /// "rpId":"example.com", 371 /// "allowCredentials":[], 372 /// "userVerification":"required", 373 /// "hints":[ 374 /// "security-key" 375 /// ], 376 /// "extensions":{ 377 /// "prf":{ 378 /// "eval":{ 379 /// "first":"AAAAAA" 380 /// }, 381 /// } 382 /// } 383 /// } 384 /// }).to_string(); 385 /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus 386 /// // we test the JSON string for everything except it. 387 /// # #[cfg(all(feature = "bin", feature = "custom"))] 388 /// assert_eq!(client_state.get(..50), json.get(..50)); 389 /// # #[cfg(all(feature = "bin", feature = "custom"))] 390 /// assert_eq!(client_state.get(72..), json.get(72..)); 391 /// # Ok::<_, webauthn_rp::AggErr>(()) 392 /// ``` 393 #[inline] 394 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 395 where 396 S: Serializer, 397 { 398 self.0.serialize(serializer) 399 } 400 } 401 impl Serialize for NonDiscoverableAuthenticationClientState<'_, '_, '_> { 402 /// Serializes `self` according to [`NonDiscoverableCredentialRequestOptions::serialize`]. 403 /// 404 /// # Examples 405 /// 406 /// ``` 407 /// # #[cfg(all(feature = "bin", feature = "custom"))] 408 /// # use webauthn_rp::{bin::Decode, response::bin::DecodeAuthTransportsErr}; 409 /// # use webauthn_rp::{ 410 /// # request::{ 411 /// # auth::{ 412 /// # AllowedCredential, AllowedCredentials, CredentialSpecificExtension, Extension, 413 /// # PrfInputOwned, NonDiscoverableCredentialRequestOptions 414 /// # }, 415 /// # AsciiDomain, ExtensionReq, Hint, PrfInput, RpId, PublicKeyCredentialDescriptor, Credentials, UserVerificationRequirement, 416 /// # }, 417 /// # response::{AuthTransports, CredentialId}, 418 /// # }; 419 /// /// Retrieves the `AuthTransports` associated with the unique `cred_id` 420 /// /// from the database. 421 /// # #[cfg(all(feature = "bin", feature = "custom"))] 422 /// fn get_transports(cred_id: CredentialId<&[u8]>) -> Result<AuthTransports, DecodeAuthTransportsErr> { 423 /// // ⋮ 424 /// # AuthTransports::decode(32) 425 /// } 426 /// let mut creds = AllowedCredentials::with_capacity(1); 427 /// // `CredentialId::try_from` only exists when `custom` is enabled; and even then, it is 428 /// // likely never needed since the `CredentialId` was originally sent from the client and is likely 429 /// // stored in a database which would be fetched by `UserHandle` or `Authentication::raw_id`. 430 /// # #[cfg(all(feature = "bin", feature = "custom"))] 431 /// let id = CredentialId::try_from(vec![0; 16])?; 432 /// # #[cfg(all(feature = "bin", feature = "custom"))] 433 /// let transports = get_transports((&id).into())?; 434 /// # #[cfg(all(feature = "bin", feature = "custom"))] 435 /// creds.push(AllowedCredential { 436 /// credential: PublicKeyCredentialDescriptor { id, transports }, 437 /// extension: CredentialSpecificExtension { 438 /// prf: Some(PrfInputOwned { 439 /// first: vec![2; 6], 440 /// second: Some(vec![3; 2]), 441 /// ext_req: ExtensionReq::Require, 442 /// }), 443 /// }, 444 /// }); 445 /// let rp_id = RpId::Domain(AsciiDomain::try_from("example.com".to_owned())?); 446 /// # #[cfg(all(feature = "bin", feature = "custom"))] 447 /// let mut options = NonDiscoverableCredentialRequestOptions::second_factor(&rp_id, creds); 448 /// # #[cfg(all(feature = "bin", feature = "custom"))] 449 /// let opts = &mut options.options; 450 /// # #[cfg(not(all(feature = "bin", feature = "custom")))] 451 /// # let mut opts = webauthn_rp::DiscoverableCredentialRequestOptions::passkey(&rp_id).public_key; 452 /// opts.hints = Hint::SecurityKey; 453 /// // This is actually useless since `CredentialSpecificExtension` takes priority 454 /// // when the client receives the payload. We set it for illustration purposes only. 455 /// // If `creds` contained an `AllowedCredential` that didn't set 456 /// // `CredentialSpecificExtension::prf`, then this would be used for it. 457 /// opts.extensions = Extension { 458 /// prf: Some((PrfInput { 459 /// first: [0; 4].as_slice(), 460 /// second: None, 461 /// }, ExtensionReq::Require)), 462 /// }; 463 /// // Since we are requesting the PRF extension, we must require user verification; otherwise 464 /// // `NonDiscoverableCredentialRequestOptions::start_ceremony` would error. 465 /// opts.user_verification = UserVerificationRequirement::Required; 466 /// # #[cfg(all(feature = "bin", feature = "custom"))] 467 /// let client_state = serde_json::to_string(&options.start_ceremony()?.1).unwrap(); 468 /// let json = serde_json::json!({ 469 /// "mediation":"required", 470 /// "publicKey":{ 471 /// "challenge":"AAAAAAAAAAAAAAAAAAAAAA", 472 /// "timeout":300000, 473 /// "rpId":"example.com", 474 /// "allowCredentials":[ 475 /// { 476 /// "type":"public-key", 477 /// "id":"AAAAAAAAAAAAAAAAAAAAAA", 478 /// "transports":["usb"] 479 /// } 480 /// ], 481 /// "userVerification":"required", 482 /// "hints":[ 483 /// "security-key" 484 /// ], 485 /// "extensions":{ 486 /// "prf":{ 487 /// "eval":{ 488 /// "first":"AAAAAA" 489 /// }, 490 /// "evalByCredential":{ 491 /// "AAAAAAAAAAAAAAAAAAAAAA":{ 492 /// "first":"AgICAgIC", 493 /// "second":"AwM" 494 /// } 495 /// } 496 /// } 497 /// } 498 /// } 499 /// }).to_string(); 500 /// // Since `Challenge`s are randomly generated, we don't know what it will be; thus 501 /// // we test the JSON string for everything except it. 502 /// # #[cfg(all(feature = "bin", feature = "custom"))] 503 /// assert_eq!(client_state.get(..50), json.get(..50)); 504 /// # #[cfg(all(feature = "bin", feature = "custom"))] 505 /// assert_eq!(client_state.get(72..), json.get(72..)); 506 /// # Ok::<_, webauthn_rp::AggErr>(()) 507 /// ``` 508 #[inline] 509 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 510 where 511 S: Serializer, 512 { 513 self.0.serialize(serializer) 514 } 515 } 516 /// Similar to [`Extension`] except [`PrfInputOwned`] is used. 517 /// 518 /// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`]. 519 #[derive(Debug, Default)] 520 pub struct ExtensionOwned { 521 /// See [`Extension::prf`]. 522 pub prf: Option<PrfInputOwned>, 523 } 524 impl ExtensionOwned { 525 /// Returns an `Extension` based on `self`. 526 #[inline] 527 #[must_use] 528 pub fn as_extension(&self) -> Extension<'_, '_> { 529 Extension { 530 prf: self.prf.as_ref().map(|prf| { 531 ( 532 PrfInput { 533 first: &prf.first, 534 second: prf.second.as_deref(), 535 }, 536 prf.ext_req, 537 ) 538 }), 539 } 540 } 541 /// Returns an `Extension` based on `self` and `prf`. 542 /// 543 /// Note `prf` is used _unconditionally_ regardless if [`Self::prf`] is `Some`. 544 #[inline] 545 #[must_use] 546 pub const fn with_prf<'prf_first, 'prf_second>( 547 &self, 548 prf: (PrfInput<'prf_first, 'prf_second>, ExtensionReq), 549 ) -> Extension<'prf_first, 'prf_second> { 550 Extension { prf: Some(prf) } 551 } 552 } 553 impl<'de> Deserialize<'de> for ExtensionOwned { 554 /// Deserializes a `struct` according to the following pseudo-schema: 555 /// 556 /// ```json 557 /// { 558 /// "prf": null | PRFJSON 559 /// } 560 /// // PRFJSON: 561 /// { 562 /// "eval": PRFInputs 563 /// } 564 /// // PRFInputs: 565 /// { 566 /// "first": <base64url-encoded string>, 567 /// "second": null | <base64url-encoded string> 568 /// } 569 /// ``` 570 /// 571 /// where the only required fields are `"eval"` and `"first"`. 572 /// 573 /// All extensions are not required to have a response sent back; but _if_ a response is sent back, its value 574 /// will be enforced. 575 /// 576 /// Unknown or duplicate fields lead to an error. 577 /// 578 /// # Examples 579 /// 580 /// ``` 581 /// # use webauthn_rp::request::{ExtensionReq, auth::ser::ExtensionOwned}; 582 /// let ext = serde_json::from_str::<ExtensionOwned>( 583 /// r#"{"prf":{"eval":{"first":"","second":null}}}"#, 584 /// )?; 585 /// assert!(ext.prf.map_or(false, |prf| prf.first.is_empty() 586 /// && prf.second.is_none() 587 /// && matches!(prf.ext_req, ExtensionReq::Allow))); 588 /// # Ok::<_, serde_json::Error>(()) 589 /// ``` 590 #[inline] 591 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 592 where 593 D: Deserializer<'de>, 594 { 595 /// `Visitor` for `ExtensionOwned`. 596 struct ExtensionOwnedVisitor; 597 impl<'d> Visitor<'d> for ExtensionOwnedVisitor { 598 type Value = ExtensionOwned; 599 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 600 formatter.write_str("ExtensionOwned") 601 } 602 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 603 where 604 A: MapAccess<'d>, 605 { 606 /// Field for `ExtensionOwned`. 607 struct Field; 608 impl<'e> Deserialize<'e> for Field { 609 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 610 where 611 D: Deserializer<'e>, 612 { 613 /// `Visitor` for `Field`. 614 struct FieldVisitor; 615 impl Visitor<'_> for FieldVisitor { 616 type Value = Field; 617 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 618 write!(formatter, "'{PRF}'") 619 } 620 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> 621 where 622 E: Error, 623 { 624 if v == PRF { 625 Ok(Field) 626 } else { 627 Err(E::unknown_field(v, FIELDS)) 628 } 629 } 630 } 631 deserializer.deserialize_identifier(FieldVisitor) 632 } 633 } 634 map.next_key::<Field>().and_then(|opt_key| { 635 opt_key 636 .map_or_else( 637 || Ok(None), 638 |_k| { 639 map.next_value::<Option<PrfHelper>>().and_then(|prf| { 640 map.next_key::<Field>().and_then(|opt_key2| { 641 opt_key2.map_or_else( 642 || Ok(prf.map(|val| val.0)), 643 |_k2| Err(Error::duplicate_field(PRF)), 644 ) 645 }) 646 }) 647 }, 648 ) 649 .map(|prf| ExtensionOwned { prf }) 650 }) 651 } 652 } 653 /// `"prf"`. 654 const PRF: &str = "prf"; 655 /// Fields for `ExtensionOwned`. 656 const FIELDS: &[&str; 1] = &[PRF]; 657 deserializer.deserialize_struct("ExtensionOwned", FIELDS, ExtensionOwnedVisitor) 658 } 659 } 660 /// Error returned by [`PublicKeyCredentialRequestOptionsOwned::as_options`] when 661 /// [`PublicKeyCredentialRequestOptionsOwned::rp_id`] is `None`. 662 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 663 pub struct PublicKeyCredentialRequestOptionsOwnedErr; 664 impl Display for PublicKeyCredentialRequestOptionsOwnedErr { 665 #[inline] 666 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 667 f.write_str("request options did not have an RP ID") 668 } 669 } 670 impl E for PublicKeyCredentialRequestOptionsOwnedErr {} 671 /// Similar to [`PublicKeyCredentialRequestOptions`] except the fields are based on owned data, and 672 /// [`Self::rp_id`] is optional. 673 /// 674 /// This is primarily useful to assist [`ClientCredentialRequestOptions::deserialize`], 675 #[derive(Debug)] 676 pub struct PublicKeyCredentialRequestOptionsOwned { 677 /// See [`PublicKeyCredentialRequestOptions::rp_id`]. 678 pub rp_id: Option<RpId>, 679 /// See [`PublicKeyCredentialRequestOptions::timeout`]. 680 pub timeout: NonZeroU32, 681 /// See [`PublicKeyCredentialRequestOptions::user_verification`]. 682 pub user_verification: UserVerificationRequirement, 683 /// See [`PublicKeyCredentialRequestOptions::hints`]. 684 pub hints: Hint, 685 /// See [`PublicKeyCredentialRequestOptions::extensions`]. 686 pub extensions: ExtensionOwned, 687 } 688 impl PublicKeyCredentialRequestOptionsOwned { 689 /// Returns a `PublicKeyCredentialRequestOptions` based on `self`. 690 /// 691 /// # Errors 692 /// 693 /// Errors iff [`Self::rp_id`] is `None`. 694 #[inline] 695 pub fn as_options( 696 &self, 697 ) -> Result< 698 PublicKeyCredentialRequestOptions<'_, '_, '_>, 699 PublicKeyCredentialRequestOptionsOwnedErr, 700 > { 701 self.rp_id 702 .as_ref() 703 .ok_or(PublicKeyCredentialRequestOptionsOwnedErr) 704 .map(|rp_id| PublicKeyCredentialRequestOptions { 705 challenge: Challenge::new(), 706 timeout: self.timeout, 707 rp_id, 708 user_verification: self.user_verification, 709 hints: self.hints, 710 extensions: self.extensions.as_extension(), 711 }) 712 } 713 /// Returns a `PublicKeyCredentialRequestOptions` based on `self` and `rp_id`. 714 /// 715 /// Note `rp_id` is used _unconditionally_ regardless if [`Self::rp_id`] is `Some`. 716 #[inline] 717 #[must_use] 718 pub fn with_rp_id<'rp_id>( 719 &self, 720 rp_id: &'rp_id RpId, 721 ) -> PublicKeyCredentialRequestOptions<'rp_id, '_, '_> { 722 PublicKeyCredentialRequestOptions { 723 challenge: Challenge::new(), 724 timeout: self.timeout, 725 rp_id, 726 user_verification: self.user_verification, 727 hints: self.hints, 728 extensions: self.extensions.as_extension(), 729 } 730 } 731 /// Returns a `PublicKeyCredentialRequestOptions` based on `self`, `exclude_credentials`, and `extensions`. 732 /// 733 /// Note `extensions` is used _unconditionally_ regardless of what [`Self::extensions`] is. 734 /// 735 /// # Errors 736 /// 737 /// Errors iff [`Self::rp_id`] is `None`. 738 #[inline] 739 pub fn with_extensions<'prf_first, 'prf_second>( 740 &self, 741 extensions: Extension<'prf_first, 'prf_second>, 742 ) -> Result< 743 PublicKeyCredentialRequestOptions<'_, 'prf_first, 'prf_second>, 744 PublicKeyCredentialRequestOptionsOwnedErr, 745 > { 746 self.rp_id 747 .as_ref() 748 .ok_or(PublicKeyCredentialRequestOptionsOwnedErr) 749 .map(|rp_id| PublicKeyCredentialRequestOptions { 750 challenge: Challenge::new(), 751 timeout: self.timeout, 752 rp_id, 753 user_verification: self.user_verification, 754 hints: self.hints, 755 extensions, 756 }) 757 } 758 /// Returns a `PublicKeyCredentialRequestOptions` based on `self`, `rp_id`, and `extensions`. 759 /// 760 /// Note `rp_id` and `extensions` are used _unconditionally_ regardless if [`Self::rp_id`] is `Some` or what 761 /// [`Self::extensions`] is. 762 #[inline] 763 #[must_use] 764 pub fn with_rp_id_and_extensions<'rp_id, 'prf_first, 'prf_second>( 765 &self, 766 rp_id: &'rp_id RpId, 767 extensions: Extension<'prf_first, 'prf_second>, 768 ) -> PublicKeyCredentialRequestOptions<'rp_id, 'prf_first, 'prf_second> { 769 PublicKeyCredentialRequestOptions { 770 challenge: Challenge::new(), 771 timeout: self.timeout, 772 rp_id, 773 user_verification: self.user_verification, 774 hints: self.hints, 775 extensions, 776 } 777 } 778 } 779 impl Default for PublicKeyCredentialRequestOptionsOwned { 780 #[inline] 781 fn default() -> Self { 782 Self { 783 rp_id: None, 784 timeout: FIVE_MINUTES, 785 user_verification: UserVerificationRequirement::Preferred, 786 hints: Hint::default(), 787 extensions: ExtensionOwned::default(), 788 } 789 } 790 } 791 impl<'de> Deserialize<'de> for PublicKeyCredentialRequestOptionsOwned { 792 /// Deserializes a `struct` based on 793 /// [`PublicKeyCredentialRequestOptionsJSON`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptionsjson). 794 /// 795 /// Note that none of the fields are required, and all are allowed to be `null`. 796 /// 797 /// If [`challenge`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-challenge) 798 /// exists, it must be `null`. If 799 /// [`allowCredentials`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-allowcredentials) 800 /// exists, it must be `null` or empty. 801 /// 802 /// If [`timeout`](https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-timeout) exists, 803 /// it must be `null` or positive. If `timeout` is missing or is `null`, then [`FIVE_MINUTES`] will be used. 804 /// 805 /// If `userVerification` is missing or is `null`, then [`UserVerificationRequirement::Required`] will be used. 806 /// 807 /// Unknown or duplicate fields lead to an error. 808 #[expect(clippy::too_many_lines, reason = "131 lines is fine")] 809 #[inline] 810 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 811 where 812 D: Deserializer<'de>, 813 { 814 /// `Visitor` for `PublicKeyCredentialRequestOptionsOwned`. 815 struct PublicKeyCredentialRequestOptionsOwnedVisitor; 816 impl<'d> Visitor<'d> for PublicKeyCredentialRequestOptionsOwnedVisitor { 817 type Value = PublicKeyCredentialRequestOptionsOwned; 818 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 819 formatter.write_str("PublicKeyCredentialRequestOptionsOwned") 820 } 821 #[expect(clippy::too_many_lines, reason = "104 lines is fine")] 822 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 823 where 824 A: MapAccess<'d>, 825 { 826 /// Field for `PublicKeyCredentialRequestOptionsOwned`. 827 enum Field { 828 /// `rpId`. 829 RpId, 830 /// `userVerification`. 831 UserVerification, 832 /// `challenge`. 833 Challenge, 834 /// `timeout`. 835 Timeout, 836 /// `allowCredentials`. 837 AllowCredentials, 838 /// `hints`. 839 Hints, 840 /// `extensions`. 841 Extensions, 842 } 843 impl<'e> Deserialize<'e> for Field { 844 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 845 where 846 D: Deserializer<'e>, 847 { 848 /// `Visitor` for `Field`. 849 struct FieldVisitor; 850 impl Visitor<'_> for FieldVisitor { 851 type Value = Field; 852 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 853 write!( 854 formatter, 855 "'{RP_ID}', '{USER_VERIFICATION}', '{CHALLENGE}', '{TIMEOUT}', '{ALLOW_CREDENTIALS}', '{HINTS}', or '{EXTENSIONS}'" 856 ) 857 } 858 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> 859 where 860 E: Error, 861 { 862 match v { 863 RP_ID => Ok(Field::RpId), 864 USER_VERIFICATION => Ok(Field::UserVerification), 865 CHALLENGE => Ok(Field::Challenge), 866 TIMEOUT => Ok(Field::Timeout), 867 ALLOW_CREDENTIALS => Ok(Field::AllowCredentials), 868 HINTS => Ok(Field::Hints), 869 EXTENSIONS => Ok(Field::Extensions), 870 _ => Err(E::unknown_field(v, FIELDS)), 871 } 872 } 873 } 874 deserializer.deserialize_identifier(FieldVisitor) 875 } 876 } 877 let mut rp = None; 878 let mut user_veri = None; 879 let mut chall = None; 880 let mut time = None; 881 let mut allow = None; 882 let mut hint = None; 883 let mut ext = None; 884 while let Some(key) = map.next_key()? { 885 match key { 886 Field::RpId => { 887 if rp.is_some() { 888 return Err(Error::duplicate_field(RP_ID)); 889 } 890 rp = map.next_value::<Option<_>>().map(Some)?; 891 } 892 Field::UserVerification => { 893 if user_veri.is_some() { 894 return Err(Error::duplicate_field(USER_VERIFICATION)); 895 } 896 user_veri = map.next_value::<Option<_>>().map(Some)?; 897 } 898 Field::Challenge => { 899 if chall.is_some() { 900 return Err(Error::duplicate_field(CHALLENGE)); 901 } 902 chall = map.next_value::<Null>().map(Some)?; 903 } 904 Field::Timeout => { 905 if time.is_some() { 906 return Err(Error::duplicate_field(TIMEOUT)); 907 } 908 time = map.next_value::<Option<_>>().map(Some)?; 909 } 910 Field::AllowCredentials => { 911 if allow.is_some() { 912 return Err(Error::duplicate_field(ALLOW_CREDENTIALS)); 913 } 914 allow = map.next_value::<Option<[(); 0]>>().map(Some)?; 915 } 916 Field::Hints => { 917 if hint.is_some() { 918 return Err(Error::duplicate_field(HINTS)); 919 } 920 hint = map.next_value::<Option<_>>().map(Some)?; 921 } 922 Field::Extensions => { 923 if ext.is_some() { 924 return Err(Error::duplicate_field(EXTENSIONS)); 925 } 926 ext = map.next_value::<Option<_>>().map(Some)?; 927 } 928 } 929 } 930 Ok(PublicKeyCredentialRequestOptionsOwned { 931 rp_id: rp.flatten(), 932 user_verification: user_veri 933 .flatten() 934 .unwrap_or(UserVerificationRequirement::Preferred), 935 timeout: time.flatten().unwrap_or(FIVE_MINUTES), 936 extensions: ext.flatten().unwrap_or_default(), 937 hints: hint.flatten().unwrap_or_default(), 938 }) 939 } 940 } 941 /// Fields for `PublicKeyCredentialRequestOptionsOwned`. 942 const FIELDS: &[&str; 7] = &[ 943 RP_ID, 944 USER_VERIFICATION, 945 CHALLENGE, 946 TIMEOUT, 947 ALLOW_CREDENTIALS, 948 HINTS, 949 EXTENSIONS, 950 ]; 951 deserializer.deserialize_struct( 952 "PublicKeyCredentialRequestOptionsOwned", 953 FIELDS, 954 PublicKeyCredentialRequestOptionsOwnedVisitor, 955 ) 956 } 957 } 958 /// Deserializes client-supplied data to assist in the creation of [`DiscoverableCredentialRequestOptions`] 959 /// and [`NonDiscoverableCredentialRequestOptions`]. 960 /// 961 /// It's common to tailor an authentication ceremony based on a user's environment. The options that should be 962 /// used are then sent to the server. To facilitate this, [`Self::deserialize`] can be used to deserialize the data 963 /// sent from the client. 964 /// 965 /// Note one may want to change some of the [`Extension`] data since [`ExtensionReq::Allow`] is unconditionally 966 /// used. Read [`ExtensionOwned::deserialize`] for more information. 967 /// 968 /// Additionally, one may want to change the value of [`PublicKeyCredentialRequestOptions::rp_id`] since 969 /// `"example.invalid"` is used in the event the RP ID was not supplied. 970 #[derive(Debug)] 971 pub struct ClientCredentialRequestOptions { 972 /// See [`DiscoverableCredentialRequestOptions::mediation`] and 973 /// [`NonDiscoverableCredentialRequestOptions::mediation`]. 974 pub mediation: CredentialMediationRequirement, 975 /// See [`DiscoverableCredentialRequestOptions::public_key`] and 976 /// See [`NonDiscoverableCredentialRequestOptions::options`]. 977 pub public_key: PublicKeyCredentialRequestOptionsOwned, 978 } 979 impl<'de> Deserialize<'de> for ClientCredentialRequestOptions { 980 /// Deserializes a `struct` according to the following pseudo-schema: 981 /// 982 /// ```json 983 /// { 984 /// "mediation": null | "required" | "conditional", 985 /// "publicKey": null | <PublicKeyCredentialRequestOptionsOwned> 986 /// } 987 /// ``` 988 /// 989 /// where none of the fields are required and `"publicKey"` is deserialized according to 990 /// [`PublicKeyCredentialRequestOptionsOwned::deserialize`]. If any field is missing or is `null`, then 991 /// the corresponding [`Default`] `impl` will be used. 992 /// 993 /// Unknown or duplicate fields lead to an error. 994 #[inline] 995 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 996 where 997 D: Deserializer<'de>, 998 { 999 /// `Visitor` for `ClientCredentialRequestOptions`. 1000 struct ClientCredentialRequestOptionsVisitor; 1001 impl<'d> Visitor<'d> for ClientCredentialRequestOptionsVisitor { 1002 type Value = ClientCredentialRequestOptions; 1003 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 1004 formatter.write_str("ClientCredentialRequestOptions") 1005 } 1006 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> 1007 where 1008 A: MapAccess<'d>, 1009 { 1010 /// Field in `ClientCredentialRequestOptions`. 1011 enum Field { 1012 /// `mediation`. 1013 Mediation, 1014 /// `publicKey` 1015 PublicKey, 1016 } 1017 impl<'e> Deserialize<'e> for Field { 1018 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 1019 where 1020 D: Deserializer<'e>, 1021 { 1022 /// `Visitor` for `Field`. 1023 struct FieldVisitor; 1024 impl Visitor<'_> for FieldVisitor { 1025 type Value = Field; 1026 fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 1027 write!(formatter, "'{MEDIATION}' or '{PUBLIC_KEY}'") 1028 } 1029 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> 1030 where 1031 E: Error, 1032 { 1033 match v { 1034 MEDIATION => Ok(Field::Mediation), 1035 PUBLIC_KEY => Ok(Field::PublicKey), 1036 _ => Err(E::unknown_field(v, FIELDS)), 1037 } 1038 } 1039 } 1040 deserializer.deserialize_identifier(FieldVisitor) 1041 } 1042 } 1043 let mut med = None; 1044 let mut key = None; 1045 while let Some(k) = map.next_key()? { 1046 match k { 1047 Field::Mediation => { 1048 if med.is_some() { 1049 return Err(Error::duplicate_field(MEDIATION)); 1050 } 1051 med = map.next_value::<Option<_>>().map(Some)?; 1052 } 1053 Field::PublicKey => { 1054 if key.is_some() { 1055 return Err(Error::duplicate_field(PUBLIC_KEY)); 1056 } 1057 key = map.next_value::<Option<_>>().map(Some)?; 1058 } 1059 } 1060 } 1061 Ok(ClientCredentialRequestOptions { 1062 mediation: med.flatten().unwrap_or_default(), 1063 public_key: key.flatten().unwrap_or_default(), 1064 }) 1065 } 1066 } 1067 /// Fields for `ClientCredentialRequestOptions`. 1068 const FIELDS: &[&str; 2] = &[MEDIATION, PUBLIC_KEY]; 1069 deserializer.deserialize_struct( 1070 "ClientCredentialRequestOptions", 1071 FIELDS, 1072 ClientCredentialRequestOptionsVisitor, 1073 ) 1074 } 1075 } 1076 #[cfg(test)] 1077 mod test { 1078 use super::{ 1079 super::ExtensionReq, ClientCredentialRequestOptions, CredentialMediationRequirement, 1080 ExtensionOwned, FIVE_MINUTES, Hint, NonZeroU32, PublicKeyCredentialRequestOptionsOwned, 1081 UserVerificationRequirement, 1082 }; 1083 use serde_json::Error; 1084 #[expect( 1085 clippy::panic_in_result_fn, 1086 clippy::unwrap_used, 1087 reason = "OK in tests" 1088 )] 1089 #[expect(clippy::cognitive_complexity, reason = "a lot to test")] 1090 #[test] 1091 fn client_options() -> Result<(), Error> { 1092 let mut err = 1093 serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"bob":true}"#).unwrap_err(); 1094 assert_eq!( 1095 err.to_string().get(..56), 1096 Some("unknown field `bob`, expected `mediation` or `publicKey`") 1097 ); 1098 err = serde_json::from_str::<ClientCredentialRequestOptions>( 1099 r#"{"mediation":"required","mediation":"required"}"#, 1100 ) 1101 .unwrap_err(); 1102 assert_eq!( 1103 err.to_string().get(..27), 1104 Some("duplicate field `mediation`") 1105 ); 1106 let mut options = serde_json::from_str::<ClientCredentialRequestOptions>("{}")?; 1107 assert!(matches!( 1108 options.mediation, 1109 CredentialMediationRequirement::Required 1110 )); 1111 assert!(options.public_key.rp_id.is_none()); 1112 assert_eq!(options.public_key.timeout, FIVE_MINUTES); 1113 assert!(matches!( 1114 options.public_key.user_verification, 1115 UserVerificationRequirement::Preferred 1116 )); 1117 assert!(matches!(options.public_key.hints, Hint::None)); 1118 assert!(options.public_key.extensions.prf.is_none()); 1119 options = serde_json::from_str::<ClientCredentialRequestOptions>( 1120 r#"{"mediation":null,"publicKey":null}"#, 1121 )?; 1122 assert!(matches!( 1123 options.mediation, 1124 CredentialMediationRequirement::Required 1125 )); 1126 assert!(options.public_key.rp_id.is_none()); 1127 assert_eq!(options.public_key.timeout, FIVE_MINUTES); 1128 assert!(matches!( 1129 options.public_key.user_verification, 1130 UserVerificationRequirement::Preferred 1131 )); 1132 assert!(matches!(options.public_key.hints, Hint::None)); 1133 assert!(options.public_key.extensions.prf.is_none()); 1134 options = serde_json::from_str::<ClientCredentialRequestOptions>(r#"{"publicKey":{}}"#)?; 1135 assert!(options.public_key.rp_id.is_none()); 1136 assert_eq!(options.public_key.timeout, FIVE_MINUTES); 1137 assert!(matches!( 1138 options.public_key.user_verification, 1139 UserVerificationRequirement::Preferred 1140 )); 1141 assert!(matches!(options.public_key.hints, Hint::None)); 1142 assert!(options.public_key.extensions.prf.is_none()); 1143 options = serde_json::from_str::<ClientCredentialRequestOptions>( 1144 r#"{"mediation":"conditional","publicKey":{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}}"#, 1145 )?; 1146 assert!(matches!( 1147 options.mediation, 1148 CredentialMediationRequirement::Conditional 1149 )); 1150 assert!( 1151 options 1152 .public_key 1153 .rp_id 1154 .is_some_and(|val| val.as_ref() == "example.com") 1155 ); 1156 assert_eq!(options.public_key.timeout, FIVE_MINUTES); 1157 assert!(matches!( 1158 options.public_key.user_verification, 1159 UserVerificationRequirement::Required 1160 )); 1161 assert!( 1162 options 1163 .public_key 1164 .extensions 1165 .prf 1166 .is_some_and(|prf| prf.first.is_empty() 1167 && prf.second.is_some_and(|p| p.is_empty()) 1168 && matches!(prf.ext_req, ExtensionReq::Allow)) 1169 ); 1170 Ok(()) 1171 } 1172 #[expect( 1173 clippy::panic_in_result_fn, 1174 clippy::unwrap_used, 1175 reason = "OK in tests" 1176 )] 1177 #[expect(clippy::cognitive_complexity, reason = "a lot to test")] 1178 #[test] 1179 fn key_options() -> Result<(), Error> { 1180 let mut err = 1181 serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"bob":true}"#) 1182 .unwrap_err(); 1183 assert_eq!( 1184 err.to_string().get(..130), 1185 Some( 1186 "unknown field `bob`, expected one of `rpId`, `userVerification`, `challenge`, `timeout`, `allowCredentials`, `hints`, `extensions`" 1187 ) 1188 ); 1189 err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1190 r#"{"rpId":"example.com","rpId":"example.com"}"#, 1191 ) 1192 .unwrap_err(); 1193 assert_eq!(err.to_string().get(..22), Some("duplicate field `rpId`")); 1194 err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1195 r#"{"challenge":"AAAAAAAAAAAAAAAAAAAAAA"}"#, 1196 ) 1197 .unwrap_err(); 1198 assert_eq!( 1199 err.to_string().get(..41), 1200 Some("invalid type: Option value, expected null") 1201 ); 1202 err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1203 r#"{"allowCredentials":[{"type":"public-key","transports":["usb"],"id":"AAAAAAAAAAAAAAAAAAAAAA"}]}"#, 1204 ) 1205 .unwrap_err(); 1206 assert_eq!(err.to_string().get(..19), Some("trailing characters")); 1207 err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>(r#"{"timeout":0}"#) 1208 .unwrap_err(); 1209 assert_eq!( 1210 err.to_string().get(..50), 1211 Some("invalid value: integer `0`, expected a nonzero u32") 1212 ); 1213 err = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1214 r#"{"timeout":4294967296}"#, 1215 ) 1216 .unwrap_err(); 1217 assert_eq!( 1218 err.to_string().get(..59), 1219 Some("invalid value: integer `4294967296`, expected a nonzero u32") 1220 ); 1221 let mut key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>("{}")?; 1222 assert!(key.rp_id.is_none()); 1223 assert_eq!(key.timeout, FIVE_MINUTES); 1224 assert!(matches!( 1225 key.user_verification, 1226 UserVerificationRequirement::Preferred 1227 )); 1228 assert!(key.extensions.prf.is_none()); 1229 assert!(matches!(key.hints, Hint::None)); 1230 key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1231 r#"{"rpId":null,"timeout":null,"allowCredentials":null,"userVerification":null,"extensions":null,"hints":null,"challenge":null}"#, 1232 )?; 1233 assert!(key.rp_id.is_none()); 1234 assert_eq!(key.timeout, FIVE_MINUTES); 1235 assert!(matches!( 1236 key.user_verification, 1237 UserVerificationRequirement::Preferred 1238 )); 1239 assert!(key.extensions.prf.is_none()); 1240 assert!(matches!(key.hints, Hint::None)); 1241 key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1242 r#"{"allowCredentials":[],"extensions":{},"hints":[]}"#, 1243 )?; 1244 assert!(matches!( 1245 key.user_verification, 1246 UserVerificationRequirement::Preferred 1247 )); 1248 assert!(matches!(key.hints, Hint::None)); 1249 assert!(key.extensions.prf.is_none()); 1250 key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1251 r#"{"extensions":{"prf":null}}"#, 1252 )?; 1253 assert!(key.extensions.prf.is_none()); 1254 key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1255 r#"{"rpId":"example.com","timeout":300000,"allowCredentials":[],"userVerification":"required","extensions":{"prf":{"eval":{"first":"","second":""}}},"hints":["security-key"],"challenge":null}"#, 1256 )?; 1257 assert!(key.rp_id.is_some_and(|val| val.as_ref() == "example.com")); 1258 assert_eq!(key.timeout, FIVE_MINUTES); 1259 assert!(matches!( 1260 key.user_verification, 1261 UserVerificationRequirement::Required 1262 )); 1263 assert!(matches!(key.hints, Hint::SecurityKey)); 1264 assert!(key.extensions.prf.is_some_and(|prf| prf.first.is_empty() 1265 && prf.second.is_some_and(|p| p.is_empty()) 1266 && matches!(prf.ext_req, ExtensionReq::Allow))); 1267 key = serde_json::from_str::<PublicKeyCredentialRequestOptionsOwned>( 1268 r#"{"timeout":4294967295}"#, 1269 )?; 1270 assert_eq!(key.timeout, NonZeroU32::MAX); 1271 Ok(()) 1272 } 1273 #[expect( 1274 clippy::panic_in_result_fn, 1275 clippy::unwrap_used, 1276 reason = "OK in tests" 1277 )] 1278 #[test] 1279 fn extension() -> Result<(), Error> { 1280 let mut err = serde_json::from_str::<ExtensionOwned>(r#"{"bob":true}"#).unwrap_err(); 1281 assert_eq!( 1282 err.to_string().get(..35), 1283 Some("unknown field `bob`, expected `prf`") 1284 ); 1285 err = serde_json::from_str::<ExtensionOwned>( 1286 r#"{"prf":{"eval":{"first":"","second":""}},"prf":{"eval":{"first":"","second":""}}}"#, 1287 ) 1288 .unwrap_err(); 1289 assert_eq!(err.to_string().get(..21), Some("duplicate field `prf`")); 1290 err = serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":null}}}"#) 1291 .unwrap_err(); 1292 assert_eq!( 1293 err.to_string().get(..51), 1294 Some("invalid type: null, expected base64url-encoded data") 1295 ); 1296 let mut ext = 1297 serde_json::from_str::<ExtensionOwned>(r#"{"prf":{"eval":{"first":"","second":""}}}"#)?; 1298 assert!(ext.prf.is_some_and(|prf| prf.first.is_empty() 1299 && prf.second.is_some_and(|v| v.is_empty()) 1300 && matches!(prf.ext_req, ExtensionReq::Allow))); 1301 ext = serde_json::from_str::<ExtensionOwned>(r#"{"prf":null}"#)?; 1302 assert!(ext.prf.is_none()); 1303 ext = serde_json::from_str::<ExtensionOwned>("{}")?; 1304 assert!(ext.prf.is_none()); 1305 Ok(()) 1306 } 1307 }